feat: add KPI dense lane relocation

This commit is contained in:
zgw
2026-07-16 10:35:03 +08:00
parent 94d14d538e
commit dd82132205
11 changed files with 633 additions and 2 deletions

View File

@@ -59,6 +59,16 @@
@click="loadChartData"
>刷新</el-button>
</el-form-item>
<el-form-item>
<el-button
type="primary"
plain
icon="el-icon-rank"
size="small"
data-testid="dense-relocation-open"
@click="openRelocationDialog"
>指定移库</el-button>
</el-form-item>
</template>
<template v-else>
@@ -102,6 +112,123 @@
:loading="trayLoading"
/>
</div>
<el-dialog
title="指定同层移库"
:visible.sync="relocationDialogVisible"
width="720px"
append-to-body
:close-on-click-modal="false"
custom-class="dense-relocation-dialog"
@closed="resetRelocationDialog"
>
<div class="relocation-context">
<span>仓库 <strong>03 密集库</strong></span>
<span>当前层 <strong>{{ selectedDenseFloor }} 层</strong></span>
<span>规则 <strong>外侧优先 · 同层异通道</strong></span>
</div>
<el-form
ref="relocationForm"
:model="relocationForm"
:rules="relocationRules"
label-width="92px"
class="relocation-form"
>
<el-row :gutter="14">
<el-col :span="10">
<el-form-item label="源通道" prop="sourceGroupCoding">
<el-select
v-model="relocationForm.sourceGroupCoding"
filterable
placeholder="选择有满托盘的通道"
style="width: 100%"
@change="handleRelocationConditionChange"
>
<el-option
v-for="item in relocationSourceLanes"
:key="item.groupCoding"
:label="laneLabel(item, 'source')"
:value="item.groupCoding"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="托盘数量" prop="trayCount">
<el-input-number
v-model="relocationForm.trayCount"
:min="1"
:max="selectedSourceCapacity"
controls-position="right"
style="width: 100%"
@change="handleRelocationConditionChange"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="目标通道" prop="targetGroupCoding">
<el-select
v-model="relocationForm.targetGroupCoding"
filterable
placeholder="选择容量足够的通道"
style="width: 100%"
@change="invalidateRelocationPreview"
>
<el-option
v-for="item in relocationTargetLanes"
:key="item.groupCoding"
:label="laneLabel(item, 'target')"
:value="item.groupCoding"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="relocation-preview-head">
<div>
<strong>外侧托盘预览</strong>
<span v-if="relocationPreview.trays.length">
{{ relocationPreview.trays.length }} 托 / 库存总量 {{ relocationPreview.totalQuantity || 0 }}
</span>
<span v-else>选择通道后先预览,提交时后端会再次校验</span>
</div>
<el-button
size="mini"
icon="el-icon-view"
:loading="relocationPreviewLoading"
@click="handlePreviewRelocation"
>刷新预览</el-button>
</div>
<el-table
:data="relocationPreview.trays"
height="248"
size="mini"
border
empty-text="暂无预览数据"
data-testid="dense-relocation-preview"
>
<el-table-column type="index" label="顺序" width="58" align="center" />
<el-table-column prop="trayCode" label="托盘码" min-width="145" show-overflow-tooltip />
<el-table-column prop="cargoSpaceName" label="源货位" min-width="170" show-overflow-tooltip>
<template slot-scope="scope">
{{ scope.row.cargoSpaceName || scope.row.cargoSpaceId }}
</template>
</el-table-column>
<el-table-column prop="groupCoding" label="源通道" min-width="130" show-overflow-tooltip />
<el-table-column prop="stockQuantity" label="库存数量" width="100" align="right" />
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="relocationDialogVisible = false">取消</el-button>
<el-button
type="primary"
:disabled="!relocationPreviewValid"
:loading="relocationGenerating"
@click="handleGenerateRelocation"
>生成移库作业</el-button>
</span>
</el-dialog>
</div>
</template>
@@ -109,7 +236,13 @@
import { listWarehouse } from '@/api/bas/warehouse'
import { listRoadway } from '@/api/bas/roadway'
import { getPlatoons } from '@/api/bas/cargospace'
import { getChartData, getDenseChartData } from '@/api/kpi/kpi'
import {
getChartData,
getDenseChartData,
getDenseRelocationOptions,
previewDenseRelocation,
generateDenseRelocation
} from '@/api/kpi/kpi'
import DenseCargoSpaceMap from './components/DenseCargoSpaceMap.vue'
import TrayCargoSpaceMap from './components/TrayCargoSpaceMap.vue'
@@ -135,6 +268,26 @@ export default {
denseFloors: [],
denseLocations: [],
selectedDenseFloor: 1,
relocationDialogVisible: false,
relocationOptionsLoading: false,
relocationPreviewLoading: false,
relocationGenerating: false,
relocationSourceLanes: [],
relocationTargetLanes: [],
relocationPreview: { trays: [], totalQuantity: 0 },
relocationPreviewSignature: '',
relocationForm: {
warehouseId: '03',
theFloor: 1,
sourceGroupCoding: '',
targetGroupCoding: '',
trayCount: 1
},
relocationRules: {
sourceGroupCoding: [{ required: true, message: '请选择源通道', trigger: 'change' }],
targetGroupCoding: [{ required: true, message: '请选择目标通道', trigger: 'change' }],
trayCount: [{ required: true, message: '请输入托盘数量', trigger: 'change' }]
},
trayLoading: false,
trayLocations: [],
trayColumns: [],
@@ -151,6 +304,16 @@ export default {
computed: {
isDenseWarehouse() {
return String(this.queryParams.warehouseId) === '03'
},
selectedSourceCapacity() {
const selected = this.relocationSourceLanes.find(
item => item.groupCoding === this.relocationForm.sourceGroupCoding
)
return selected && selected.fullTrayCount ? Number(selected.fullTrayCount) : 1
},
relocationPreviewValid() {
return this.relocationPreview.trays.length === Number(this.relocationForm.trayCount) &&
this.relocationPreviewSignature === this.buildRelocationSignature()
}
},
mounted() {
@@ -291,6 +454,90 @@ export default {
}).finally(() => {
this.trayLoading = false
})
},
openRelocationDialog() {
this.relocationForm = {
warehouseId: '03',
theFloor: Number(this.selectedDenseFloor),
sourceGroupCoding: '',
targetGroupCoding: '',
trayCount: 1
}
this.invalidateRelocationPreview()
this.relocationDialogVisible = true
this.loadRelocationOptions()
},
resetRelocationDialog() {
this.relocationSourceLanes = []
this.relocationTargetLanes = []
this.invalidateRelocationPreview()
if (this.$refs.relocationForm) this.$refs.relocationForm.clearValidate()
},
loadRelocationOptions() {
this.relocationOptionsLoading = true
getDenseRelocationOptions(this.relocationForm).then(response => {
const data = response.data || {}
this.relocationSourceLanes = data.sourceLanes || []
this.relocationTargetLanes = data.targetLanes || []
if (this.relocationForm.targetGroupCoding &&
!this.relocationTargetLanes.some(item => item.groupCoding === this.relocationForm.targetGroupCoding)) {
this.relocationForm.targetGroupCoding = ''
}
}).finally(() => {
this.relocationOptionsLoading = false
})
},
handleRelocationConditionChange() {
if (this.relocationForm.trayCount > this.selectedSourceCapacity) {
this.relocationForm.trayCount = this.selectedSourceCapacity
}
this.relocationForm.targetGroupCoding = ''
this.invalidateRelocationPreview()
this.loadRelocationOptions()
},
invalidateRelocationPreview() {
this.relocationPreview = { trays: [], totalQuantity: 0 }
this.relocationPreviewSignature = ''
},
buildRelocationSignature() {
const form = this.relocationForm
return [form.warehouseId, form.theFloor, form.sourceGroupCoding,
form.targetGroupCoding, form.trayCount].join('|')
},
laneLabel(item, type) {
const count = type === 'source' ? item.fullTrayCount : item.emptyLocationCount
return `${item.groupCoding}(${type === 'source' ? '满托' : '空位'} ${count || 0})`
},
handlePreviewRelocation() {
this.$refs.relocationForm.validate(valid => {
if (!valid) return
this.relocationPreviewLoading = true
previewDenseRelocation(this.relocationForm).then(response => {
this.relocationPreview = response.data || { trays: [], totalQuantity: 0 }
this.relocationPreviewSignature = this.buildRelocationSignature()
}).finally(() => {
this.relocationPreviewLoading = false
})
})
},
handleGenerateRelocation() {
if (!this.relocationPreviewValid) {
this.$modal.msgWarning('预览已失效,请重新预览')
return
}
this.$modal.confirm(
`确认将外侧 ${this.relocationForm.trayCount} 个托盘移至通道 ${this.relocationForm.targetGroupCoding}?`
).then(() => {
this.relocationGenerating = true
return generateDenseRelocation(this.relocationForm)
}).then(response => {
const ids = response.data || []
this.$modal.msgSuccess(`已生成 ${ids.length} 个移库作业`)
this.relocationDialogVisible = false
this.loadDenseChartData()
}).finally(() => {
this.relocationGenerating = false
})
}
}
}
@@ -320,4 +567,53 @@ export default {
.chart-container--tray {
background: transparent;
}
.relocation-context {
display: flex;
align-items: center;
gap: 18px;
margin: -6px 0 18px;
padding: 9px 12px;
border-left: 3px solid #2c7a8f;
color: #627789;
background: #f2f7f9;
font-size: 12px;
}
.relocation-context strong {
margin-left: 4px;
color: #183f52;
}
.relocation-form ::v-deep .el-form-item {
margin-bottom: 16px;
}
.relocation-preview-head {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 38px;
padding: 0 2px;
border-top: 1px solid #e3e9ee;
}
.relocation-preview-head strong {
margin-right: 10px;
color: #213f52;
font-size: 13px;
}
.relocation-preview-head span {
color: #7a8d9c;
font-size: 12px;
}
@media (max-width: 760px) {
.relocation-context {
align-items: flex-start;
flex-direction: column;
gap: 5px;
}
}
</style>