feat: add dense lane outbound workflow
This commit is contained in:
553
ruoyi-ui/src/views/out/denseLaneOutbound/index.vue
Normal file
553
ruoyi-ui/src/views/out/denseLaneOutbound/index.vue
Normal file
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<div class="app-container dense-lane-outbound">
|
||||
<el-form
|
||||
ref="queryForm"
|
||||
:model="queryParams"
|
||||
size="small"
|
||||
:inline="true"
|
||||
v-show="showSearch"
|
||||
label-width="76px"
|
||||
>
|
||||
<el-form-item label="仓库">
|
||||
<el-input value="03仓密集库" disabled class="warehouse-input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="通道编码" prop="groupCoding">
|
||||
<el-input
|
||||
v-model="queryParams.groupCoding"
|
||||
placeholder="请输入通道组编码"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="层" prop="theFloor">
|
||||
<el-input-number
|
||||
v-model="queryParams.theFloor"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
placeholder="全部"
|
||||
class="floor-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-alert
|
||||
title="仅显示03仓有托盘的密集通道;生成时严格按当前端头顺序选择整托。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="lane-hint"
|
||||
/>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="laneList"
|
||||
row-key="laneKey"
|
||||
border
|
||||
stripe
|
||||
@expand-change="handleExpandChange"
|
||||
>
|
||||
<el-table-column type="expand" width="44">
|
||||
<template slot-scope="scope">
|
||||
<div class="inventory-expand" v-loading="scope.row.inventoryLoading">
|
||||
<div class="inventory-heading">
|
||||
<span>当前最外侧托盘库存</span>
|
||||
<span class="inventory-subtitle">
|
||||
{{ scope.row.outermostTrayCode || '-' }} / {{ scope.row.outermostCargoSpaceId || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="scope.row.inventoryDetails || []" size="mini" border max-height="260">
|
||||
<el-table-column label="项目号" prop="projectNo" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="物料编码" prop="productId" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="物料名称" prop="productName" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="批次" prop="lotNo" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="库存数量" prop="stockQuantity" width="110" align="right" />
|
||||
<el-table-column label="入库日期" prop="inDate" width="160" />
|
||||
<template slot="empty">
|
||||
<div class="empty-detail">{{ scope.row.inventoryLoaded ? '当前端头托盘无库存明细' : '正在读取库存明细' }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通道组编码" prop="groupCoding" min-width="140" fixed="left" show-overflow-tooltip />
|
||||
<el-table-column label="货区" prop="cargoArea" width="90" align="center" />
|
||||
<el-table-column label="排" prop="thePlatoon" width="72" align="center" />
|
||||
<el-table-column label="层" prop="theFloor" width="72" align="center" />
|
||||
<el-table-column label="出库方向" prop="directionType" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" effect="plain">{{ directionLabel(scope.row.directionType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="满托盘数" prop="fullTrayCount" width="105" align="right" />
|
||||
<el-table-column label="可出库托盘" prop="availableTrayCount" width="120" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :class="{ 'available-count': Number(scope.row.availableTrayCount) > 0 }">
|
||||
{{ scope.row.availableTrayCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总库存" prop="totalQuantity" min-width="110" align="right" />
|
||||
<el-table-column label="当前最外侧托盘" prop="outermostTrayCode" min-width="145" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.outermostTrayCode || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="端头货位" prop="outermostCargoSpaceId" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-s-promotion"
|
||||
:disabled="Number(scope.row.availableTrayCount || 0) < 1"
|
||||
v-hasPermi="['out:denseLaneOutbound:generate']"
|
||||
@click="openGenerateDialog(scope.row)"
|
||||
>整托出库</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
title="指定通道整托出库"
|
||||
:visible.sync="dialogVisible"
|
||||
width="920px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetDialog"
|
||||
>
|
||||
<el-descriptions v-if="currentLane" :column="4" size="small" border class="lane-summary">
|
||||
<el-descriptions-item label="通道">{{ currentLane.groupCoding }}</el-descriptions-item>
|
||||
<el-descriptions-item label="货区 / 排">{{ currentLane.cargoArea || '-' }} / {{ currentLane.thePlatoon || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="层">{{ currentLane.theFloor }}</el-descriptions-item>
|
||||
<el-descriptions-item label="可出库">{{ currentLane.availableTrayCount || 0 }} 托</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-form :model="generateForm" size="small" :inline="true" label-width="86px" class="generate-form">
|
||||
<el-form-item label="整托数量" required>
|
||||
<el-input-number
|
||||
v-model="generateForm.trayCount"
|
||||
:min="1"
|
||||
:max="currentLane ? Number(currentLane.availableTrayCount || 1) : 1"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
@change="handleTrayCountChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-view"
|
||||
:loading="previewLoading"
|
||||
v-hasPermi="['out:denseLaneOutbound:preview']"
|
||||
@click="loadPreview"
|
||||
>刷新预览</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="出库口" required class="exit-form-item">
|
||||
<el-select
|
||||
v-model="generateForm.exitId"
|
||||
placeholder="请选择与托盘类型匹配的出库口"
|
||||
filterable
|
||||
clearable
|
||||
:loading="exitLoading"
|
||||
class="exit-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="point in compatibleExitOptions"
|
||||
:key="point.pointId"
|
||||
:label="formatExitLabel(point)"
|
||||
:value="point.pointId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="preview-header">
|
||||
<div>
|
||||
<span class="preview-title">本次准确出库托盘</span>
|
||||
<span class="preview-note">提交前会再次校验端头顺序和库存</span>
|
||||
</div>
|
||||
<div class="preview-total">合计库存:<strong>{{ previewData.totalQuantity || 0 }}</strong></div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="previewLoading"
|
||||
:data="previewData.trays || []"
|
||||
row-key="cargoSpaceId"
|
||||
border
|
||||
max-height="390"
|
||||
>
|
||||
<el-table-column type="expand" width="42">
|
||||
<template slot-scope="scope">
|
||||
<el-table :data="scope.row.stockDetails || []" size="mini" border class="preview-detail-table">
|
||||
<el-table-column label="项目号" prop="projectNo" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="物料编码" prop="productId" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="物料名称" prop="productName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="批次" prop="lotNo" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="库存数量" prop="stockQuantity" width="105" align="right" />
|
||||
</el-table>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="顺序" type="index" width="62" align="center" />
|
||||
<el-table-column label="托盘号" prop="trayCode" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="货位编码" prop="cargoSpaceId" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="货位名称" prop="cargoSpaceName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="列" prop="theColumn" width="72" align="center" />
|
||||
<el-table-column label="库存数量" prop="stockQuantity" width="115" align="right" />
|
||||
</el-table>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="generating"
|
||||
:disabled="!canGenerate"
|
||||
@click="confirmGenerate"
|
||||
>确认生成 {{ generateForm.trayCount || 0 }} 托出库作业</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listDenseLaneOutbound,
|
||||
previewDenseLaneOutbound,
|
||||
generateDenseLaneOutbound
|
||||
} from '@/api/out/denseLaneOutbound'
|
||||
import { listStationPoint } from '@/api/bas/stationpoint'
|
||||
|
||||
const emptyPreview = () => ({ trays: [], totalQuantity: 0, previewToken: null })
|
||||
|
||||
export default {
|
||||
name: 'DenseLaneOutbound',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showSearch: true,
|
||||
laneList: [],
|
||||
queryParams: {
|
||||
warehouseId: '03',
|
||||
groupCoding: null,
|
||||
theFloor: null
|
||||
},
|
||||
dialogVisible: false,
|
||||
currentLane: null,
|
||||
previewLoading: false,
|
||||
generating: false,
|
||||
exitLoading: false,
|
||||
exitOptions: [],
|
||||
previewData: emptyPreview(),
|
||||
generateForm: {
|
||||
warehouseId: '03',
|
||||
groupCoding: null,
|
||||
theFloor: null,
|
||||
trayCount: 1,
|
||||
exitId: null,
|
||||
previewToken: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
compatibleExitOptions() {
|
||||
const trayTypes = Array.from(new Set((this.previewData.trays || [])
|
||||
.map(tray => this.trayType(tray.trayCode))
|
||||
.filter(Boolean)))
|
||||
return this.exitOptions.filter(point => trayTypes.every(type => this.pointSupportsTrayType(point, type)))
|
||||
},
|
||||
canGenerate() {
|
||||
return Boolean(
|
||||
this.previewData.previewToken &&
|
||||
this.generateForm.exitId &&
|
||||
(this.previewData.trays || []).length === Number(this.generateForm.trayCount)
|
||||
)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
laneKey(row) {
|
||||
return `${row.groupCoding}-${row.theFloor}`
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listDenseLaneOutbound(this.queryParams).then(response => {
|
||||
this.laneList = (response.data || []).map(row => ({
|
||||
...row,
|
||||
inventoryLoading: false,
|
||||
inventoryLoaded: false,
|
||||
inventoryDetails: []
|
||||
}))
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleQuery() {
|
||||
this.getList()
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetForm('queryForm')
|
||||
this.queryParams.warehouseId = '03'
|
||||
this.getList()
|
||||
},
|
||||
handleExpandChange(row, expandedRows) {
|
||||
const expanded = expandedRows.some(item => this.laneKey(item) === this.laneKey(row))
|
||||
if (!expanded || row.inventoryLoaded || row.inventoryLoading || !row.outermostTrayCode) {
|
||||
return
|
||||
}
|
||||
this.$set(row, 'inventoryLoading', true)
|
||||
previewDenseLaneOutbound(this.previewParams(row, 1)).then(response => {
|
||||
const tray = ((response.data || {}).trays || [])[0]
|
||||
this.$set(row, 'inventoryDetails', tray ? (tray.stockDetails || []) : [])
|
||||
this.$set(row, 'inventoryLoaded', true)
|
||||
this.$set(row, 'inventoryLoading', false)
|
||||
}).catch(() => {
|
||||
this.$set(row, 'inventoryLoading', false)
|
||||
})
|
||||
},
|
||||
openGenerateDialog(row) {
|
||||
if (Number(row.availableTrayCount || 0) < 1) {
|
||||
this.$modal.msgWarning('当前通道没有可出库的端头托盘')
|
||||
return
|
||||
}
|
||||
this.currentLane = row
|
||||
this.generateForm = {
|
||||
warehouseId: '03',
|
||||
groupCoding: row.groupCoding,
|
||||
theFloor: row.theFloor,
|
||||
trayCount: 1,
|
||||
exitId: null,
|
||||
previewToken: null
|
||||
}
|
||||
this.previewData = emptyPreview()
|
||||
this.dialogVisible = true
|
||||
this.loadExitOptions()
|
||||
this.loadPreview()
|
||||
},
|
||||
resetDialog() {
|
||||
this.currentLane = null
|
||||
this.previewData = emptyPreview()
|
||||
this.exitOptions = []
|
||||
this.generateForm.previewToken = null
|
||||
},
|
||||
handleTrayCountChange() {
|
||||
this.previewData = emptyPreview()
|
||||
this.generateForm.previewToken = null
|
||||
this.generateForm.exitId = null
|
||||
},
|
||||
previewParams(row, trayCount) {
|
||||
return {
|
||||
warehouseId: '03',
|
||||
groupCoding: row.groupCoding,
|
||||
theFloor: row.theFloor,
|
||||
trayCount: trayCount
|
||||
}
|
||||
},
|
||||
loadPreview() {
|
||||
if (!this.currentLane || !this.generateForm.trayCount) {
|
||||
return
|
||||
}
|
||||
this.previewLoading = true
|
||||
this.generateForm.exitId = null
|
||||
previewDenseLaneOutbound(this.previewParams(this.currentLane, this.generateForm.trayCount)).then(response => {
|
||||
this.previewData = response.data || emptyPreview()
|
||||
this.generateForm.previewToken = this.previewData.previewToken
|
||||
this.selectOnlyCompatibleExit()
|
||||
this.previewLoading = false
|
||||
}).catch(() => {
|
||||
this.previewData = emptyPreview()
|
||||
this.generateForm.previewToken = null
|
||||
this.previewLoading = false
|
||||
})
|
||||
},
|
||||
loadExitOptions() {
|
||||
this.exitLoading = true
|
||||
listStationPoint({ warehouseId: '03', status: '0' }).then(response => {
|
||||
const rows = response.rows || response.data || []
|
||||
this.exitOptions = rows.filter(point => ['2', '3'].includes(String(point.pointType)) &&
|
||||
String(point.pointStatus || '0') === '0')
|
||||
this.selectOnlyCompatibleExit()
|
||||
this.exitLoading = false
|
||||
}).catch(() => {
|
||||
this.exitOptions = []
|
||||
this.exitLoading = false
|
||||
})
|
||||
},
|
||||
selectOnlyCompatibleExit() {
|
||||
this.$nextTick(() => {
|
||||
if (this.compatibleExitOptions.length === 1) {
|
||||
this.generateForm.exitId = this.compatibleExitOptions[0].pointId
|
||||
}
|
||||
})
|
||||
},
|
||||
trayType(trayCode) {
|
||||
return trayCode ? String(trayCode).trim().slice(0, 1).toUpperCase() : null
|
||||
},
|
||||
pointSupportsTrayType(point, trayType) {
|
||||
if (!trayType || !point.trayType) {
|
||||
return true
|
||||
}
|
||||
return String(point.trayType).split(',').map(item => item.trim().toUpperCase()).includes(trayType)
|
||||
},
|
||||
formatExitLabel(point) {
|
||||
const typeName = String(point.pointType) === '3' ? '入出库' : '出库'
|
||||
const trayType = point.trayType ? `,${point.trayType}类托盘` : ''
|
||||
return `${point.pointName || point.pointId}(${point.pointId},${typeName}${trayType})`
|
||||
},
|
||||
directionLabel(directionType) {
|
||||
if (String(directionType) === '1') return '大列号端'
|
||||
if (String(directionType) === '2') return '小列号端'
|
||||
if (String(directionType) === '3') return '最近端头'
|
||||
return '-'
|
||||
},
|
||||
confirmGenerate() {
|
||||
if (!this.canGenerate) {
|
||||
this.$modal.msgWarning('请先刷新预览并选择匹配的出库口')
|
||||
return
|
||||
}
|
||||
const trays = (this.previewData.trays || []).map(item => item.trayCode).join('、')
|
||||
this.$modal.confirm(`确认按当前端头顺序生成 ${this.generateForm.trayCount} 托出库作业?托盘:${trays}`).then(() => {
|
||||
this.generating = true
|
||||
return generateDenseLaneOutbound({
|
||||
...this.generateForm,
|
||||
previewToken: this.previewData.previewToken
|
||||
})
|
||||
}).then(response => {
|
||||
const ids = response.data || []
|
||||
this.$modal.msgSuccess(`已生成 ${ids.length} 条出库作业`)
|
||||
this.generating = false
|
||||
this.dialogVisible = false
|
||||
this.getList()
|
||||
}).catch(error => {
|
||||
this.generating = false
|
||||
if (error && error.message && error.message.includes('预览已失效')) {
|
||||
this.$modal.msgWarning('端头托盘或库存已变化,已为你刷新预览,请重新确认')
|
||||
this.loadPreview()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dense-lane-outbound .warehouse-input {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.dense-lane-outbound .floor-input {
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
.lane-hint {
|
||||
width: auto;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.available-count {
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inventory-expand {
|
||||
padding: 8px 18px 14px 58px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.inventory-heading,
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inventory-subtitle,
|
||||
.preview-note {
|
||||
margin-left: 10px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.empty-detail {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.lane-summary {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.generate-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 12px 12px 0;
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.exit-form-item {
|
||||
flex: 1;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.exit-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.preview-total {
|
||||
color: #606266;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.preview-total strong {
|
||||
color: #1890ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.preview-detail-table {
|
||||
width: calc(100% - 48px);
|
||||
margin: 4px 18px 10px 48px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.inventory-expand {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.generate-form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.exit-form-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user