feat: add KPI dense lane relocation
This commit is contained in:
@@ -70,6 +70,39 @@ export function getDenseChartData(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 密集库指定移库通道选项
|
||||
*/
|
||||
export function getDenseRelocationOptions(query) {
|
||||
return request({
|
||||
url: '/kpi/denseRelocation/options',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览密集库指定移库托盘
|
||||
*/
|
||||
export function previewDenseRelocation(data) {
|
||||
return request({
|
||||
url: '/kpi/denseRelocation/preview',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成密集库指定移库作业
|
||||
*/
|
||||
export function generateDenseRelocation(data) {
|
||||
return request({
|
||||
url: '/kpi/denseRelocation/generate',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 货位平面图-货位详情
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user