feat: 完善WCS调度、库存追溯和出库分配
This commit is contained in:
@@ -59,6 +59,17 @@ export function getChartData(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 密集库货位平面图数据
|
||||
*/
|
||||
export function getDenseChartData(query) {
|
||||
return request({
|
||||
url: '/kpi/denseCargoSpacePlan',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 货位平面图-货位详情
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="货位编码" prop="cargoSpaceId">
|
||||
<el-input
|
||||
v-model="queryParams.cargoSpaceId"
|
||||
placeholder="请输入货位编码"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="仓库id" prop="warehouseId">
|
||||
<el-input
|
||||
v-model="queryParams.warehouseId"
|
||||
@@ -17,10 +25,10 @@
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="货区id" prop="cargoAreaId">
|
||||
<el-form-item label="货区" prop="thisCargoArea">
|
||||
<el-input
|
||||
v-model="queryParams.cargoAreaId"
|
||||
placeholder="请输入货区id"
|
||||
v-model="queryParams.thisCargoArea"
|
||||
placeholder="请输入货区"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
@@ -161,11 +169,11 @@
|
||||
|
||||
<el-table v-loading="loading" :data="cargospaceList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="货位id" align="center" prop="cargoSpaceId" />
|
||||
<el-table-column label="货位编码" align="center" prop="cargoSpaceId" />
|
||||
<el-table-column label="仓库id" align="center" prop="warehouseId" />
|
||||
<el-table-column label="巷道id" align="center" prop="roadwayId" />
|
||||
<el-table-column label="货区id" align="center" prop="cargoAreaId" />
|
||||
<el-table-column label="货位名称" align="center" prop="cargoSpaceName" />
|
||||
<el-table-column label="货位名称" align="center" prop="cargoSpaceName" min-width="140" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="排" align="center" prop="thePlatoon" />
|
||||
<el-table-column label="列" align="center" prop="theColumn" />
|
||||
<el-table-column label="层" align="center" prop="theFloor" />
|
||||
@@ -176,6 +184,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="是否锁定" align="center" prop="locked" />
|
||||
<el-table-column label="托盘条码" align="center" prop="trayCode" />
|
||||
<el-table-column label="货位分组" align="center" prop="groupCoding" min-width="120" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="部门名称" align="center" prop="deptName" />
|
||||
<el-table-column label="重量" align="center" prop="theWeight" />
|
||||
<el-table-column label="高度" align="center" prop="theHeight" />
|
||||
@@ -207,122 +216,160 @@
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 添加或修改货位信息对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<!-- 修改货位信息对话框 -->
|
||||
<el-dialog title="修改货位信息" :visible.sync="openEdit" width="500px" append-to-body>
|
||||
<el-form ref="editForm" :model="form" :rules="editRules" label-width="100px" class="edit-form">
|
||||
<el-form-item label="货位状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择货位状态" style="width: 100%">
|
||||
<el-option
|
||||
v-for="dict in dict.type.bas_cargo_space_status"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否锁定" prop="locked">
|
||||
<el-switch
|
||||
v-model="form.locked"
|
||||
active-value="Y"
|
||||
inactive-value="N"
|
||||
active-text="锁定"
|
||||
inactive-text="未锁定"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="托盘条码" prop="trayCode">
|
||||
<el-input v-model="form.trayCode" placeholder="请输入托盘条码" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitEditForm">确 定</el-button>
|
||||
<el-button @click="cancelEdit">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增货位信息对话框 -->
|
||||
<el-dialog title="添加货位信息" :visible.sync="openAdd" width="720px" append-to-body>
|
||||
<el-form ref="addForm" :model="form" :rules="addRules" label-width="120px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="仓库id" prop="warehouseId">
|
||||
<el-input v-model="form.warehouseId" placeholder="请输入仓库id" />
|
||||
<el-input v-model="form.warehouseId" placeholder="请输入仓库id" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="巷道id" prop="roadwayId">
|
||||
<el-input v-model="form.roadwayId" placeholder="请输入巷道id" />
|
||||
<el-input v-model="form.roadwayId" placeholder="请输入巷道id" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="货区id" prop="cargoAreaId">
|
||||
<el-input v-model="form.cargoAreaId" placeholder="请输入货区id" />
|
||||
<el-input v-model="form.cargoAreaId" placeholder="请输入货区id" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="货位类型名称" prop="theTypeName">
|
||||
<el-input v-model="form.theTypeName" placeholder="请输入货位类型名称" />
|
||||
<el-input v-model="form.theTypeName" placeholder="请输入货位类型名称" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="货位名称" prop="cargoSpaceName">
|
||||
<el-input v-model="form.cargoSpaceName" placeholder="请输入货位名称" />
|
||||
<el-input v-model="form.cargoSpaceName" placeholder="请输入货位名称" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排" prop="thePlatoon">
|
||||
<el-input v-model="form.thePlatoon" placeholder="请输入排" />
|
||||
<el-input v-model="form.thePlatoon" placeholder="请输入排" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="列" prop="theColumn">
|
||||
<el-input v-model="form.theColumn" placeholder="请输入列" />
|
||||
<el-input v-model="form.theColumn" placeholder="请输入列" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="层" prop="theFloor">
|
||||
<el-input v-model="form.theFloor" placeholder="请输入层" />
|
||||
<el-input v-model="form.theFloor" placeholder="请输入层" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="货位状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择货位状态">
|
||||
<el-select v-model="form.status" placeholder="请选择货位状态" style="width: 100%">
|
||||
<el-option
|
||||
v-for="dict in dict.type.bas_cargo_space_status"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
></el-option>
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否锁定" prop="locked">
|
||||
<el-input v-model="form.locked" placeholder="请输入是否锁定" />
|
||||
<el-switch
|
||||
v-model="form.locked"
|
||||
active-value="Y"
|
||||
inactive-value="N"
|
||||
active-text="锁定"
|
||||
inactive-text="未锁定"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="托盘条码" prop="trayCode">
|
||||
<el-input v-model="form.trayCode" placeholder="请输入托盘条码" />
|
||||
<el-input v-model="form.trayCode" placeholder="请输入托盘条码" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否双伸" prop="issDouble">
|
||||
<el-input v-model="form.issDouble" placeholder="请输入是否双伸" />
|
||||
<el-input v-model="form.issDouble" placeholder="请输入是否双伸" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="双货位分组编码" prop="groupCodingNo">
|
||||
<el-input v-model="form.groupCodingNo" placeholder="请输入双货位分组编码" />
|
||||
<el-input v-model="form.groupCodingNo" placeholder="请输入双货位分组编码" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="双货位分组编码" prop="groupCoding">
|
||||
<el-input v-model="form.groupCoding" placeholder="请输入双货位分组编码" />
|
||||
<el-col :span="12">
|
||||
<el-form-item label="双货位分组" prop="groupCoding">
|
||||
<el-input v-model="form.groupCoding" placeholder="请输入双货位分组编码" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="双货位标识" prop="cargoSpaceLogo">
|
||||
<el-input v-model="form.cargoSpaceLogo" placeholder="请输入双货位标识" />
|
||||
<el-input v-model="form.cargoSpaceLogo" placeholder="请输入双货位标识" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="本仓库区域" prop="thisCargoArea">
|
||||
<el-input v-model="form.thisCargoArea" placeholder="请输入本仓库区域" />
|
||||
<el-input v-model="form.thisCargoArea" placeholder="请输入本仓库区域" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门" prop="deptId">
|
||||
<el-input v-model="form.deptId" placeholder="请输入部门" />
|
||||
<el-input v-model="form.deptId" placeholder="请输入部门" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门名称" prop="deptName">
|
||||
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
|
||||
<el-input v-model="form.deptName" placeholder="请输入部门名称" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="重量" prop="theWeight">
|
||||
<el-input v-model="form.theWeight" placeholder="请输入重量" />
|
||||
<el-input v-model="form.theWeight" placeholder="请输入重量" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="高度" prop="theHeight">
|
||||
<el-input v-model="form.theHeight" placeholder="请输入高度" />
|
||||
<el-input v-model="form.theHeight" placeholder="请输入高度" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" @click="submitAddForm">确 定</el-button>
|
||||
<el-button @click="cancelAdd">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -350,17 +397,18 @@ export default {
|
||||
total: 0,
|
||||
// 货位信息表格数据
|
||||
cargospaceList: [],
|
||||
// 弹出层标题
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 是否显示新增弹出层
|
||||
openAdd: false,
|
||||
// 是否显示修改弹出层
|
||||
openEdit: false,
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
cargoSpaceId: null,
|
||||
warehouseId: null,
|
||||
roadwayId: null,
|
||||
cargoAreaId: null,
|
||||
thisCargoArea: null,
|
||||
cargoSpaceName: null,
|
||||
thePlatoon: null,
|
||||
theColumn: null,
|
||||
@@ -374,8 +422,8 @@ export default {
|
||||
},
|
||||
// 表单参数
|
||||
form: {},
|
||||
// 表单校验
|
||||
rules: {
|
||||
// 新增表单校验
|
||||
addRules: {
|
||||
warehouseId: [
|
||||
{ required: true, message: "仓库id不能为空", trigger: "blur" }
|
||||
],
|
||||
@@ -398,8 +446,17 @@ export default {
|
||||
{ required: true, message: "货位状态不能为空", trigger: "change" }
|
||||
],
|
||||
locked: [
|
||||
{ required: true, message: "是否锁定不能为空", trigger: "blur" }
|
||||
{ required: true, message: "是否锁定不能为空", trigger: "change" }
|
||||
]
|
||||
},
|
||||
// 修改表单校验
|
||||
editRules: {
|
||||
status: [
|
||||
{ required: true, message: "货位状态不能为空", trigger: "change" }
|
||||
],
|
||||
locked: [
|
||||
{ required: true, message: "是否锁定不能为空", trigger: "change" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -416,9 +473,14 @@ export default {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
// 取消按钮
|
||||
cancel() {
|
||||
this.open = false
|
||||
// 取消新增
|
||||
cancelAdd() {
|
||||
this.openAdd = false
|
||||
this.reset()
|
||||
},
|
||||
// 取消修改
|
||||
cancelEdit() {
|
||||
this.openEdit = false
|
||||
this.reset()
|
||||
},
|
||||
// 表单重置
|
||||
@@ -435,7 +497,7 @@ export default {
|
||||
theColumn: null,
|
||||
theFloor: null,
|
||||
status: null,
|
||||
locked: null,
|
||||
locked: "N",
|
||||
trayCode: null,
|
||||
issDouble: null,
|
||||
groupCodingNo: null,
|
||||
@@ -447,7 +509,8 @@ export default {
|
||||
theWeight: null,
|
||||
theHeight: null
|
||||
}
|
||||
this.resetForm("form")
|
||||
this.resetForm("addForm")
|
||||
this.resetForm("editForm")
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
@@ -468,36 +531,46 @@ export default {
|
||||
/** 新增按钮操作 */
|
||||
handleAdd() {
|
||||
this.reset()
|
||||
this.open = true
|
||||
this.title = "添加货位信息"
|
||||
this.openAdd = true
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.reset()
|
||||
const cargoSpaceId = row.cargoSpaceId || this.ids
|
||||
getCargospace(cargoSpaceId).then(response => {
|
||||
this.form = response.data
|
||||
this.open = true
|
||||
this.title = "修改货位信息"
|
||||
this.form = {
|
||||
...response.data,
|
||||
locked: response.data.locked || "N"
|
||||
}
|
||||
this.openEdit = true
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
/** 提交新增 */
|
||||
submitAddForm() {
|
||||
this.$refs["addForm"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.cargoSpaceId != null) {
|
||||
updateCargospace(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addCargospace(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
}
|
||||
addCargospace(this.form).then(() => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.openAdd = false
|
||||
this.getList()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 提交修改 */
|
||||
submitEditForm() {
|
||||
this.$refs["editForm"].validate(valid => {
|
||||
if (valid) {
|
||||
updateCargospace({
|
||||
cargoSpaceId: this.form.cargoSpaceId,
|
||||
status: this.form.status,
|
||||
locked: this.form.locked,
|
||||
trayCode: this.form.trayCode
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.openEdit = false
|
||||
this.getList()
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -520,3 +593,14 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.edit-form .el-form-item {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.edit-form .el-select,
|
||||
.edit-form .el-input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -321,6 +321,7 @@
|
||||
<el-table-column label="批次号" align="center" prop="lotNo" />
|
||||
<el-table-column label="物料编码" align="center" prop="productId" />
|
||||
<el-table-column label="物料名称" align="center" prop="productName" show-overflow-tooltip />
|
||||
<el-table-column label="项目号" align="center" prop="projectNo" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="库存数量" align="center" prop="quantity" />
|
||||
<el-table-column label="出库数量" align="center" prop="outQuantity" />
|
||||
<el-table-column label="子作业" align="center" prop="jobDetailId" width="160" />
|
||||
@@ -474,14 +475,13 @@
|
||||
<el-input v-model="detailForm.stockId" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="物料状态" prop="productStatus">
|
||||
<el-select v-model="detailForm.productStatus" placeholder="请选择物料状态">
|
||||
<el-option
|
||||
v-for="dict in dict.type.bas_stock_product_status"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-switch
|
||||
v-model="detailForm.productStatus"
|
||||
active-value="1"
|
||||
inactive-value="2"
|
||||
active-text="正常"
|
||||
inactive-text="异常"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="批次号" prop="lotNo">
|
||||
<el-input v-model="detailForm.lotNo" placeholder="请输入批次号" />
|
||||
@@ -499,10 +499,10 @@
|
||||
<el-input v-model="detailForm.outQuantity" placeholder="请输入出库数量" />
|
||||
</el-form-item>
|
||||
<el-form-item label="子作业id" prop="jobDetailId">
|
||||
<el-input v-model="detailForm.jobDetailId" placeholder="请输入子作业id" />
|
||||
<el-input v-model="detailForm.jobDetailId" placeholder="系统自动记录" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="入库子单id" prop="stockinDetailId">
|
||||
<el-input v-model="detailForm.stockinDetailId" placeholder="请输入入库子单id" />
|
||||
<el-input v-model="detailForm.stockinDetailId" placeholder="系统自动记录" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="detailForm.remark" type="textarea" placeholder="请输入内容" />
|
||||
@@ -705,7 +705,7 @@ export default {
|
||||
this.detailForm = {
|
||||
detailId: null,
|
||||
stockId: this.currentStock ? this.currentStock.stockId : null,
|
||||
productStatus: null,
|
||||
productStatus: '1',
|
||||
lotNo: null,
|
||||
productId: null,
|
||||
productName: null,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<i class="el-icon-tickets"></i> 基本信息
|
||||
</div>
|
||||
<el-form ref="mainForm" :model="mainForm" :rules="mainRules" label-width="100px" size="small">
|
||||
<el-row :gutter="24">
|
||||
<el-row :gutter="24" class="stable-form-grid">
|
||||
<!-- 仓库 -->
|
||||
<el-col :span="8">
|
||||
<el-form-item label="仓库" prop="warehouseId">
|
||||
@@ -101,6 +101,12 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="项目号" prop="projectNo">
|
||||
<el-input v-model="mainForm.projectNo" placeholder="请输入项目号" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="8">
|
||||
<el-form-item label="单据日期" prop="documentDat">
|
||||
<el-date-picker
|
||||
@@ -184,6 +190,11 @@
|
||||
<el-table-column label="物料名称" prop="productName" align="center" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="规格" prop="spec" align="center" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="单位" prop="unit" align="center" width="80" />
|
||||
<el-table-column label="项目号" align="center" width="140" show-overflow-tooltip>
|
||||
<template>
|
||||
<span>{{ mainForm.projectNo || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="批次" align="center" width="160">
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
@@ -335,9 +346,10 @@ export default {
|
||||
// ---------- 主单表单 ----------
|
||||
mainForm: {
|
||||
inType: null,
|
||||
stockSource: null,
|
||||
stockSource: '1',
|
||||
erpBill: null,
|
||||
documentN: null,
|
||||
projectNo: null,
|
||||
documentDat: null,
|
||||
counterpart: null,
|
||||
receivingOr: null,
|
||||
@@ -536,6 +548,7 @@ export default {
|
||||
locatio: line.locatio || null,
|
||||
inventoryStatus: line.inventoryStatus || null,
|
||||
sourceDocN: line.sourceDocN || null,
|
||||
projectNo: this.mainForm.projectNo || null,
|
||||
remar: line.remar || line.remark || null,
|
||||
remark: line.remark || line.remar || null
|
||||
}))
|
||||
@@ -586,6 +599,35 @@ export default {
|
||||
.section-card {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stable-form-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stable-form-grid > .el-col {
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.create-stockin ::v-deep .el-form-item {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.create-stockin ::v-deep .el-select,
|
||||
.create-stockin ::v-deep .el-input,
|
||||
.create-stockin ::v-deep .el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-stockin ::v-deep .el-form-item__content {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.create-stockin ::v-deep .el-form-item__error {
|
||||
line-height: 16px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="巷道ID" prop="roadwayId">
|
||||
<el-form-item label="巷道/层" prop="roadwayId">
|
||||
<el-input
|
||||
v-model="queryParams.roadwayId"
|
||||
placeholder="请输入巷道ID"
|
||||
placeholder="请输入巷道/层"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
@@ -124,7 +124,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="托盘码" align="center" prop="trayCode" />
|
||||
<el-table-column label="仓库名称" align="center" prop="warehouseName" />
|
||||
<el-table-column label="巷道ID" align="center" prop="roadwayId" />
|
||||
<el-table-column label="巷道/层" align="center" prop="roadwayId" />
|
||||
<el-table-column label="货位ID" align="center" prop="cargoSpaceId" />
|
||||
<el-table-column label="货位名称" align="center" prop="cargoSpaceName" />
|
||||
<!-- <el-table-column label="高度" align="center" prop="height" />-->
|
||||
@@ -175,6 +175,7 @@
|
||||
<el-table-column label="子任务ID" align="center" prop="jobDetailId" width="180" />
|
||||
<el-table-column label="物料编码" align="center" prop="productId" width="120" />
|
||||
<el-table-column label="物料名称" align="center" prop="productName" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="项目号" align="center" prop="projectNo" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="批次" align="center" prop="lotNo" width="120" />
|
||||
<el-table-column label="子单编号" align="center" prop="inOrOutDId" width="160" />
|
||||
<el-table-column label="入库数量" align="center" prop="inQuantity" width="100" />
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form
|
||||
:model="queryParams"
|
||||
ref="queryForm"
|
||||
class="stockin-query-form"
|
||||
size="small"
|
||||
:inline="true"
|
||||
v-show="showSearch"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="入库类型" prop="inType">
|
||||
<el-select v-model="queryParams.inType" placeholder="请选择入库类型" clearable>
|
||||
<el-option
|
||||
@@ -76,7 +84,6 @@
|
||||
<el-form-item label="创建时间">
|
||||
<el-date-picker
|
||||
v-model="daterangeCreateDate"
|
||||
style="width: 240px"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
@@ -87,7 +94,6 @@
|
||||
<el-form-item label="开始时间">
|
||||
<el-date-picker
|
||||
v-model="daterangeStartDate"
|
||||
style="width: 240px"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
@@ -98,7 +104,6 @@
|
||||
<el-form-item label="结束时间">
|
||||
<el-date-picker
|
||||
v-model="daterangeFinishDate"
|
||||
style="width: 240px"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
@@ -184,6 +189,7 @@
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="上位单号" align="center" prop="erpBill"/>
|
||||
<el-table-column label="项目号" align="center" prop="projectNo" width="140" show-overflow-tooltip/>
|
||||
<el-table-column label="仓库名称" align="center" prop="warehouseName"/>
|
||||
<el-table-column label="用户名称" align="center" prop="userName"/>
|
||||
<el-table-column label="部门名称" align="center" prop="deptName"/>
|
||||
@@ -234,6 +240,7 @@
|
||||
>
|
||||
<el-table-column label="子单ID" align="center" prop="stockinDetailId" width="180"/>
|
||||
<el-table-column label="批次" align="center" prop="lotNo" width="150"/>
|
||||
<el-table-column label="项目号" align="center" prop="projectNo" width="140" show-overflow-tooltip/>
|
||||
<el-table-column label="物料编码" align="center" prop="productId" width="150"/>
|
||||
<el-table-column label="物料名称" align="center" prop="productName" min-width="150" show-overflow-tooltip/>
|
||||
<el-table-column label="计划数量" align="center" prop="quantity" width="100"/>
|
||||
@@ -567,7 +574,7 @@ export default {
|
||||
this.form = {
|
||||
stockinId: null,
|
||||
inType: null,
|
||||
stockSource: null,
|
||||
stockSource: '1',
|
||||
status: null,
|
||||
createDate: null,
|
||||
startDate: null,
|
||||
@@ -832,3 +839,21 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stockin-query-form ::v-deep .el-form-item__content > .el-input,
|
||||
.stockin-query-form ::v-deep .el-form-item__content > .el-select,
|
||||
.stockin-query-form ::v-deep .el-form-item__content > .el-date-editor {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.stockin-query-form ::v-deep .el-select .el-input,
|
||||
.stockin-query-form ::v-deep .el-select .el-input__inner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stockin-query-form ::v-deep .el-date-editor.el-input__inner,
|
||||
.stockin-query-form ::v-deep .el-date-editor.el-range-editor {
|
||||
width: 240px;
|
||||
}
|
||||
</style>
|
||||
|
||||
925
ruoyi-ui/src/views/kpi/plan/components/DenseCargoSpaceMap.vue
Normal file
925
ruoyi-ui/src/views/kpi/plan/components/DenseCargoSpaceMap.vue
Normal file
@@ -0,0 +1,925 @@
|
||||
<template>
|
||||
<section class="dense-map" v-loading="loading" data-testid="dense-cargo-space-map">
|
||||
<div class="dense-map__summary">
|
||||
<div v-for="item in summaryItems" :key="item.key" class="summary-card">
|
||||
<span class="summary-card__mark" :class="'summary-card__mark--' + item.key"></span>
|
||||
<span class="summary-card__label">{{ item.label }}</span>
|
||||
<strong>{{ item.count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card summary-card--total">
|
||||
<span class="summary-card__label">本层节点</span>
|
||||
<strong>{{ currentLocations.length }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dense-map__legend" aria-label="图例">
|
||||
<span v-for="item in legendItems" :key="item.status" class="legend-item">
|
||||
<i :class="'legend-item__swatch legend-item__swatch--' + item.status"></i>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="dense-map__viewport" ref="viewport">
|
||||
<div class="dense-grid" :aria-label="'03仓第' + currentFloor + '层货位平面图'">
|
||||
<div class="axis-title axis-title--column" style="grid-column: 1; grid-row: 1">列</div>
|
||||
<div class="axis-title axis-title--platoon" style="grid-column: 1; grid-row: 26">排</div>
|
||||
|
||||
<div
|
||||
v-for="column in columnLabels"
|
||||
:key="'column-' + column"
|
||||
class="axis-label axis-label--column"
|
||||
:style="axisColumnStyle(column)"
|
||||
>{{ pad2(column) }}</div>
|
||||
|
||||
<div
|
||||
v-for="platoon in platoonLabels"
|
||||
:key="'platoon-' + platoon"
|
||||
class="axis-label axis-label--platoon"
|
||||
:style="axisPlatoonStyle(platoon)"
|
||||
>{{ pad2(platoon) }}</div>
|
||||
|
||||
<div
|
||||
v-for="cell in scaffoldCells"
|
||||
:key="cell.key"
|
||||
class="dense-grid__scaffold"
|
||||
:style="cell.style"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<button
|
||||
v-for="location in currentLocations"
|
||||
:key="location.cargoSpaceId"
|
||||
type="button"
|
||||
class="dense-location"
|
||||
:class="locationClass(location)"
|
||||
:style="cellStyle(location)"
|
||||
:data-status="String(location.status)"
|
||||
:data-platoon="String(location.thePlatoon)"
|
||||
:data-column="String(location.theColumn)"
|
||||
data-testid="dense-location"
|
||||
@mouseenter="handleLocationEnter($event, location)"
|
||||
@mousemove="handleLocationMove($event)"
|
||||
@mouseleave="handleLocationLeave"
|
||||
@click="selectLocation(location)"
|
||||
>
|
||||
<span>{{ location.cargoSpaceId }}</span>
|
||||
<i v-if="String(location.locked) === '1'" class="el-icon-lock dense-location__lock"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="dense-map__footer">
|
||||
<div class="location-readout location-readout--empty">
|
||||
鼠标悬停货位查看详情与库存
|
||||
</div>
|
||||
<span class="dense-map__hint">列号由下向上递增,排号由左向右递增</span>
|
||||
</footer>
|
||||
|
||||
<!-- 实际挂载到 document.body,避免被平面图 overflow/flex 裁切 -->
|
||||
<div
|
||||
v-show="tooltip.visible"
|
||||
ref="stockTooltip"
|
||||
class="stock-tooltip cargo-space-stock-tooltip"
|
||||
:class="{ 'is-multi': tooltip.stocks.length > 3 }"
|
||||
:style="tooltipStyle"
|
||||
data-testid="dense-stock-tooltip"
|
||||
@mouseenter="handleTooltipEnter"
|
||||
@mouseleave="handleLocationLeave"
|
||||
>
|
||||
<div class="stock-tooltip__head">
|
||||
<strong>{{ tooltip.cargoSpaceId || '-' }}</strong>
|
||||
<span class="stock-tooltip__status">{{ tooltip.statusName || '-' }}</span>
|
||||
</div>
|
||||
<div class="stock-tooltip__meta">
|
||||
<div v-if="tooltip.coordText"><label>坐标</label><span>{{ tooltip.coordText }}</span></div>
|
||||
<div v-if="tooltip.groupCoding"><label>通道</label><span>{{ tooltip.groupCoding }}</span></div>
|
||||
<div v-if="tooltip.thisCargoArea"><label>货区</label><span>{{ tooltip.thisCargoArea }}</span></div>
|
||||
<div v-if="tooltip.trayCode"><label>托盘</label><span>{{ tooltip.trayCode }}</span></div>
|
||||
</div>
|
||||
<div v-if="tooltip.loading" class="stock-tooltip__loading">
|
||||
<i class="el-icon-loading"></i> 正在联查库存...
|
||||
</div>
|
||||
<div v-else-if="tooltip.stocks.length" class="stock-tooltip__body">
|
||||
<div class="stock-tooltip__count">库存 {{ tooltip.stocks.length }} 条</div>
|
||||
<div class="stock-tooltip__list" :class="{ 'is-cols': tooltip.stocks.length > 3 }">
|
||||
<div
|
||||
v-for="(stock, index) in tooltip.stocks"
|
||||
:key="index"
|
||||
class="stock-tooltip__item"
|
||||
>
|
||||
<div class="stock-tooltip__title">
|
||||
<span class="stock-tooltip__name">{{ stock.productName || stock.productId || '-' }}</span>
|
||||
<em>×{{ formatQty(stock.quantity) }}</em>
|
||||
</div>
|
||||
<div class="stock-tooltip__tags">
|
||||
<span>编码 {{ stock.productId || '-' }}</span>
|
||||
<span>批次 {{ stock.lotNo || '-' }}</span>
|
||||
<span>项目 {{ stock.projectNo || '-' }}</span>
|
||||
<span v-if="stock.trayCode">托盘 {{ stock.trayCode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="stock-tooltip__empty">当前货位无库存记录</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getChartMag } from '@/api/kpi/kpi'
|
||||
|
||||
export default {
|
||||
name: 'DenseCargoSpaceMap',
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
locations: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
warehouseId: {
|
||||
type: [String, Number],
|
||||
default: '03'
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stockCache: {},
|
||||
hoverTimer: null,
|
||||
hideTimer: null,
|
||||
requestSeq: 0,
|
||||
tooltip: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cursorX: 0,
|
||||
cursorY: 0,
|
||||
maxHeight: 360,
|
||||
maxWidth: 360,
|
||||
cargoSpaceId: '',
|
||||
cargoSpaceName: '',
|
||||
statusName: '',
|
||||
coordText: '',
|
||||
groupCoding: '',
|
||||
thisCargoArea: '',
|
||||
trayCode: '',
|
||||
stocks: [],
|
||||
cacheKey: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentFloor() {
|
||||
return Number(this.value)
|
||||
},
|
||||
columnLabels() {
|
||||
return Array.from({ length: 24 }, (_, index) => 24 - index)
|
||||
},
|
||||
platoonLabels() {
|
||||
return Array.from({ length: 13 }, (_, index) => index + 1)
|
||||
},
|
||||
currentLocations() {
|
||||
return this.locations.filter(item => Number(item.theFloor) === Number(this.currentFloor))
|
||||
},
|
||||
scaffoldCells() {
|
||||
const cells = []
|
||||
this.columnLabels.forEach(column => {
|
||||
this.platoonLabels.forEach(platoon => {
|
||||
cells.push({
|
||||
key: `${platoon}-${column}`,
|
||||
style: {
|
||||
gridColumn: String(platoon + 1),
|
||||
gridRow: String(26 - column)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
return cells
|
||||
},
|
||||
summaryItems() {
|
||||
const countStatus = status => this.currentLocations.filter(item => String(item.status) === status).length
|
||||
const infrastructureCount = ['7', '8', '9', '10'].reduce((total, status) => total + countStatus(status), 0)
|
||||
return [
|
||||
{ key: 'storage', label: '普通货位', count: this.currentLocations.length - infrastructureCount },
|
||||
{ key: 'mother', label: '母通道', count: countStatus('9') },
|
||||
{ key: 'lift', label: '提升机', count: countStatus('7') },
|
||||
{ key: 'conveyor', label: '输送机', count: countStatus('8') },
|
||||
{ key: 'charging', label: '充电桩', count: countStatus('10') }
|
||||
]
|
||||
},
|
||||
legendItems() {
|
||||
return [
|
||||
{ status: '1', label: '空货位' },
|
||||
{ status: '2', label: '满货位' },
|
||||
{ status: '3', label: '入库分配' },
|
||||
{ status: '4', label: '出库分配' },
|
||||
{ status: '7', label: '提升机' },
|
||||
{ status: '8', label: '输送机' },
|
||||
{ status: '9', label: '母通道' },
|
||||
{ status: '10', label: '充电桩' }
|
||||
]
|
||||
},
|
||||
tooltipStyle() {
|
||||
return {
|
||||
position: 'fixed',
|
||||
left: this.tooltip.x + 'px',
|
||||
top: this.tooltip.y + 'px',
|
||||
zIndex: 5000,
|
||||
maxHeight: this.tooltip.maxHeight + 'px',
|
||||
maxWidth: this.tooltip.maxWidth + 'px'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.resetSelection()
|
||||
},
|
||||
locations() {
|
||||
this.resetSelection()
|
||||
},
|
||||
warehouseId() {
|
||||
this.resetSelection()
|
||||
this.stockCache = {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.mountTooltip()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearTimers()
|
||||
this.unmountTooltip()
|
||||
},
|
||||
methods: {
|
||||
pad2(value) {
|
||||
return String(value).padStart(2, '0')
|
||||
},
|
||||
formatQty(value) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
return value
|
||||
},
|
||||
locationKey(location) {
|
||||
return `${location.cargoSpaceId || ''}-${location.thePlatoon}-${location.theColumn}-${location.theFloor}`
|
||||
},
|
||||
axisColumnStyle(column) {
|
||||
return {
|
||||
gridColumn: '1',
|
||||
gridRow: String(26 - column)
|
||||
}
|
||||
},
|
||||
axisPlatoonStyle(platoon) {
|
||||
return {
|
||||
gridColumn: String(platoon + 1),
|
||||
gridRow: '26'
|
||||
}
|
||||
},
|
||||
cellStyle(location) {
|
||||
return {
|
||||
gridColumn: String(Number(location.thePlatoon) + 1),
|
||||
gridRow: String(26 - Number(location.theColumn))
|
||||
}
|
||||
},
|
||||
locationClass(location) {
|
||||
return [
|
||||
`dense-location--status-${location.status}`,
|
||||
{ 'dense-location--locked': String(location.locked) === '1' }
|
||||
]
|
||||
},
|
||||
statusName(status) {
|
||||
const names = {
|
||||
'1': '空货位',
|
||||
'2': '满货位',
|
||||
'3': '入库分配',
|
||||
'4': '出库分配',
|
||||
'5': '异常:放货有货',
|
||||
'6': '异常:取货无货',
|
||||
'7': '提升机',
|
||||
'8': '输送机',
|
||||
'9': '母通道',
|
||||
'10': '充电桩'
|
||||
}
|
||||
return names[String(status)] || `状态${status}`
|
||||
},
|
||||
mountTooltip() {
|
||||
const el = this.$refs.stockTooltip
|
||||
if (el && el.parentNode !== document.body) {
|
||||
document.body.appendChild(el)
|
||||
}
|
||||
},
|
||||
unmountTooltip() {
|
||||
const el = this.$refs.stockTooltip
|
||||
if (el && el.parentNode === document.body) {
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
},
|
||||
clearTimers() {
|
||||
if (this.hoverTimer) {
|
||||
clearTimeout(this.hoverTimer)
|
||||
this.hoverTimer = null
|
||||
}
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer)
|
||||
this.hideTimer = null
|
||||
}
|
||||
},
|
||||
resetSelection() {
|
||||
this.tooltip.visible = false
|
||||
this.clearTimers()
|
||||
},
|
||||
updateTooltipPosition(event) {
|
||||
this.tooltip.cursorX = event.clientX
|
||||
this.tooltip.cursorY = event.clientY
|
||||
this.repositionTooltip()
|
||||
},
|
||||
/** 按视口与内容动态计算宽高与位置,避免贴底/裁切 */
|
||||
repositionTooltip() {
|
||||
this.mountTooltip()
|
||||
const gap = 14
|
||||
const margin = 8
|
||||
const tip = this.$refs.stockTooltip
|
||||
if (!tip) return
|
||||
|
||||
const vw = window.innerWidth
|
||||
const vh = window.innerHeight
|
||||
const cursorX = this.tooltip.cursorX || 0
|
||||
const cursorY = this.tooltip.cursorY || 0
|
||||
const stockCount = (this.tooltip.stocks && this.tooltip.stocks.length) || 0
|
||||
const preferWide = stockCount > 3
|
||||
|
||||
// 可用宽度:优先鼠标右侧,不够则左侧
|
||||
const rightSpace = vw - cursorX - gap - margin
|
||||
const leftSpace = cursorX - gap - margin
|
||||
let maxWidth = preferWide ? 560 : 360
|
||||
maxWidth = Math.min(maxWidth, Math.max(rightSpace, leftSpace, 240), vw - margin * 2)
|
||||
this.tooltip.maxWidth = Math.max(240, maxWidth)
|
||||
|
||||
// 可用高度:优先下方,不够则上方,最终取视口最大可用
|
||||
const belowSpace = vh - cursorY - gap - margin
|
||||
const aboveSpace = cursorY - gap - margin
|
||||
const maxHeight = Math.max(belowSpace, aboveSpace, 160)
|
||||
this.tooltip.maxHeight = Math.min(vh - margin * 2, Math.max(160, maxHeight))
|
||||
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.stockTooltip
|
||||
if (!el) return
|
||||
const tipWidth = el.offsetWidth || 280
|
||||
const tipHeight = el.offsetHeight || 120
|
||||
let x = cursorX + gap
|
||||
let y = cursorY + gap
|
||||
|
||||
if (x + tipWidth > vw - margin) {
|
||||
x = cursorX - tipWidth - gap
|
||||
}
|
||||
if (y + tipHeight > vh - margin) {
|
||||
y = cursorY - tipHeight - gap
|
||||
}
|
||||
if (x < margin) x = margin
|
||||
if (y < margin) y = margin
|
||||
// 超高内容贴顶并依赖内部滚动
|
||||
if (tipHeight >= vh - margin * 2) {
|
||||
y = margin
|
||||
}
|
||||
this.tooltip.x = Math.round(x)
|
||||
this.tooltip.y = Math.round(y)
|
||||
})
|
||||
},
|
||||
fillTooltipBase(location) {
|
||||
this.tooltip.cargoSpaceId = location.cargoSpaceId || ''
|
||||
this.tooltip.cargoSpaceName = location.cargoSpaceName || ''
|
||||
this.tooltip.statusName = this.statusName(location.status)
|
||||
this.tooltip.coordText = `${this.pad2(location.thePlatoon)}排 ${this.pad2(location.theColumn)}列 ${this.pad2(location.theFloor)}层`
|
||||
this.tooltip.groupCoding = location.groupCoding || ''
|
||||
this.tooltip.thisCargoArea = location.thisCargoArea || ''
|
||||
this.tooltip.trayCode = location.trayCode || ''
|
||||
this.tooltip.cacheKey = this.locationKey(location)
|
||||
},
|
||||
handleLocationEnter(event, location) {
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer)
|
||||
this.hideTimer = null
|
||||
}
|
||||
this.fillTooltipBase(location)
|
||||
this.tooltip.visible = true
|
||||
this.mountTooltip()
|
||||
this.updateTooltipPosition(event)
|
||||
const cached = this.stockCache[this.tooltip.cacheKey]
|
||||
if (cached) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = cached
|
||||
this.$nextTick(() => this.repositionTooltip())
|
||||
return
|
||||
}
|
||||
this.tooltip.loading = true
|
||||
this.tooltip.stocks = []
|
||||
if (this.hoverTimer) clearTimeout(this.hoverTimer)
|
||||
this.hoverTimer = setTimeout(() => {
|
||||
this.fetchStockInfo(location, this.tooltip.cacheKey)
|
||||
}, 120)
|
||||
},
|
||||
handleLocationMove(event) {
|
||||
if (this.tooltip.visible) this.updateTooltipPosition(event)
|
||||
},
|
||||
handleTooltipEnter() {
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer)
|
||||
this.hideTimer = null
|
||||
}
|
||||
},
|
||||
handleLocationLeave() {
|
||||
// 允许鼠标移入悬浮框滚动查看(短延迟关闭)
|
||||
if (this.hideTimer) clearTimeout(this.hideTimer)
|
||||
this.hideTimer = setTimeout(() => {
|
||||
this.tooltip.visible = false
|
||||
this.clearTimers()
|
||||
}, 120)
|
||||
},
|
||||
selectLocation(location) {
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer)
|
||||
this.hideTimer = null
|
||||
}
|
||||
this.fillTooltipBase(location)
|
||||
this.tooltip.visible = true
|
||||
this.mountTooltip()
|
||||
// 点击时以货位中心附近为锚点
|
||||
const key = this.locationKey(location)
|
||||
if (this.stockCache[key]) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = this.stockCache[key]
|
||||
this.$nextTick(() => this.repositionTooltip())
|
||||
return
|
||||
}
|
||||
this.tooltip.loading = true
|
||||
this.tooltip.stocks = []
|
||||
this.fetchStockInfo(location, key)
|
||||
},
|
||||
fetchStockInfo(location, cacheKey) {
|
||||
if (!this.warehouseId) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = []
|
||||
return
|
||||
}
|
||||
const seq = ++this.requestSeq
|
||||
getChartMag({
|
||||
warehouseId: this.warehouseId,
|
||||
thePlatoon: String(location.thePlatoon),
|
||||
theColumn: String(location.theColumn),
|
||||
theFloor: String(location.theFloor)
|
||||
}).then(response => {
|
||||
if (seq !== this.requestSeq) return
|
||||
const data = response.data || {}
|
||||
const stocks = data.stocks || []
|
||||
this.stockCache[cacheKey] = stocks
|
||||
if (this.tooltip.cacheKey === cacheKey) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = stocks
|
||||
if (data.cargoSpaceId) this.tooltip.cargoSpaceId = data.cargoSpaceId
|
||||
if (data.status != null && data.status !== '') {
|
||||
this.tooltip.statusName = this.statusName(data.status)
|
||||
} else if (data.statusName) {
|
||||
this.tooltip.statusName = data.statusName
|
||||
}
|
||||
if (data.trayCode) this.tooltip.trayCode = data.trayCode
|
||||
if (data.groupCoding) this.tooltip.groupCoding = data.groupCoding
|
||||
if (data.thisCargoArea) this.tooltip.thisCargoArea = data.thisCargoArea
|
||||
this.$nextTick(() => this.repositionTooltip())
|
||||
}
|
||||
}).catch(() => {
|
||||
if (this.tooltip.cacheKey === cacheKey) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = []
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dense-map {
|
||||
--dense-ink: #10243a;
|
||||
--dense-muted: #60758a;
|
||||
--dense-line: #cbd7e2;
|
||||
--dense-panel: #f5f8fb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 190px);
|
||||
min-height: 470px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d7e1ea;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 34px rgba(20, 48, 76, 0.1);
|
||||
font-family: "Bahnschrift", "DIN Alternate", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.dense-map__summary {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
border-bottom: 1px solid #e1e8ef;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
border-right: 1px solid #e1e8ef;
|
||||
color: var(--dense-muted);
|
||||
}
|
||||
|
||||
.summary-card strong {
|
||||
margin-left: auto;
|
||||
color: var(--dense-ink);
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.summary-card__label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.summary-card__mark {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 1px #98aabd;
|
||||
}
|
||||
|
||||
.summary-card__mark--mother { background: #00b050; box-shadow: none; }
|
||||
.summary-card__mark--lift { background: #9a652f; box-shadow: none; }
|
||||
.summary-card__mark--conveyor { background: #ffd428; box-shadow: none; }
|
||||
.summary-card__mark--charging { background: #00a6b2; box-shadow: none; }
|
||||
.summary-card--total { border-right: 0; background: #edf5f8; }
|
||||
|
||||
.dense-map__legend {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: clamp(7px, 1.1vw, 15px);
|
||||
min-height: 32px;
|
||||
padding: 6px 12px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #e5ebf0;
|
||||
color: #52687b;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.legend-item__swatch {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border: 1px solid #9eafbf;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legend-item__swatch--1 { background: #fff; }
|
||||
.legend-item__swatch--2 { background: #ed9c58; border-color: #d8833d; }
|
||||
.legend-item__swatch--3 { background: #ffe38a; border-color: #e3bd3c; }
|
||||
.legend-item__swatch--4 { background: #8ec5ff; border-color: #599bdc; }
|
||||
.legend-item__swatch--7 { background: #9a652f; border-color: #73481f; }
|
||||
.legend-item__swatch--8 { background: #ffd428; border-color: #cda600; }
|
||||
.legend-item__swatch--9 { background: #00b050; border-color: #008b3e; }
|
||||
.legend-item__swatch--10 { background: #00a6b2; border-color: #087985; }
|
||||
|
||||
.dense-map__viewport {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(rgba(24, 62, 89, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(24, 62, 89, 0.025) 1px, transparent 1px),
|
||||
var(--dense-panel);
|
||||
background-size: 18px 18px;
|
||||
}
|
||||
|
||||
.dense-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 36px repeat(13, minmax(0, 1fr));
|
||||
grid-template-rows: 22px repeat(24, minmax(13px, 1fr)) 26px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid #aebdca;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
box-shadow: 0 6px 18px rgba(28, 55, 77, 0.08);
|
||||
}
|
||||
|
||||
.dense-grid__scaffold {
|
||||
z-index: 0;
|
||||
border-right: 1px solid #d7e0e8;
|
||||
border-bottom: 1px solid #d7e0e8;
|
||||
background: #fbfcfd;
|
||||
}
|
||||
|
||||
.axis-title,
|
||||
.axis-label {
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #29465e;
|
||||
background: #e9f0f5;
|
||||
font-size: clamp(8px, 0.72vw, 11px);
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.axis-title--column {
|
||||
border-right: 1px solid #b9c7d2;
|
||||
border-bottom: 1px solid #b9c7d2;
|
||||
}
|
||||
|
||||
.axis-title--platoon {
|
||||
border-top: 1px solid #aebdca;
|
||||
border-right: 1px solid #b9c7d2;
|
||||
}
|
||||
|
||||
.axis-label--column {
|
||||
border-right: 1px solid #b9c7d2;
|
||||
border-bottom: 1px solid #d7e0e8;
|
||||
}
|
||||
|
||||
.axis-label--platoon {
|
||||
border-top: 1px solid #aebdca;
|
||||
border-right: 1px solid #d7e0e8;
|
||||
}
|
||||
|
||||
.dense-location {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 0 2px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #b7c5d0;
|
||||
color: #213c52;
|
||||
background: #fff;
|
||||
font-family: "Bahnschrift", "DIN Alternate", monospace;
|
||||
font-size: clamp(7px, 0.68vw, 10px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: z-index 0s, transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
|
||||
}
|
||||
|
||||
.dense-location:hover,
|
||||
.dense-location:focus {
|
||||
z-index: 5;
|
||||
outline: none;
|
||||
transform: translateY(-1px) scale(1.035);
|
||||
box-shadow: 0 4px 10px rgba(14, 45, 69, 0.24);
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
|
||||
.dense-location--status-2 { border-color: #d8833d; background: #ed9c58; }
|
||||
.dense-location--status-3 { border-color: #e3bd3c; background: #ffe38a; }
|
||||
.dense-location--status-4 { border-color: #599bdc; background: #8ec5ff; }
|
||||
.dense-location--status-5 { border-color: #d95858; color: #6e1111; background: #ffb7b7; }
|
||||
.dense-location--status-6 { border-color: #8562c7; color: #321866; background: #cbb7f4; }
|
||||
.dense-location--status-7 { border-color: #73481f; color: #fff8ee; background: #9a652f; }
|
||||
.dense-location--status-8 { border-color: #cda600; color: #4c3c00; background: #ffd428; }
|
||||
.dense-location--status-9 { border-color: #008b3e; color: #042d16; background: #00b050; }
|
||||
.dense-location--status-10 { border-color: #087985; color: #fff; background: #00a6b2; }
|
||||
.dense-location--locked { box-shadow: inset 0 0 0 2px #d92d20; }
|
||||
|
||||
.dense-location__lock {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.dense-map__footer {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 34px;
|
||||
padding: 5px 12px;
|
||||
border-top: 1px solid #e2e9ef;
|
||||
color: #577084;
|
||||
background: #f8fafc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.location-readout {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px 14px;
|
||||
}
|
||||
|
||||
.location-readout span:not(:first-child) {
|
||||
padding-left: 14px;
|
||||
border-left: 1px solid #ccd8e1;
|
||||
}
|
||||
|
||||
.location-readout__id {
|
||||
color: #153e59;
|
||||
font-family: "Bahnschrift", monospace;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.location-readout--empty,
|
||||
.dense-map__hint {
|
||||
color: #8293a1;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.dense-map__hint {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 非 scoped:悬浮层挂到 body 后仍可生效 */
|
||||
.cargo-space-stock-tooltip.stock-tooltip {
|
||||
position: fixed !important;
|
||||
z-index: 5000 !important;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
min-width: 240px;
|
||||
overflow: hidden;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #c9d7e3;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 12px 28px rgba(16, 42, 64, 0.22);
|
||||
color: #29465e;
|
||||
pointer-events: auto;
|
||||
font-family: "Microsoft YaHei", "Bahnschrift", sans-serif;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip.is-multi {
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__head {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e4ebf1;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__head strong {
|
||||
color: #153e59;
|
||||
font-family: "Bahnschrift", monospace;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.3px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__status {
|
||||
flex: 0 0 auto;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
background: #eef4f8;
|
||||
color: #4d667a;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__meta {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
color: #5f7386;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__meta > div {
|
||||
display: grid;
|
||||
grid-template-columns: 36px 1fr;
|
||||
gap: 6px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__meta label {
|
||||
color: #8aa0b2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__meta span {
|
||||
color: #3d5569;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__loading,
|
||||
.cargo-space-stock-tooltip .stock-tooltip__empty {
|
||||
flex: 0 0 auto;
|
||||
color: #6b8296;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__count {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 6px;
|
||||
color: #7d93a6;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__list.is-cols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__list:not(.is-cols) .stock-tooltip__item + .stock-tooltip__item {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #dce5ec;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__list.is-cols .stock-tooltip__item {
|
||||
padding: 8px;
|
||||
border: 1px solid #e4ebf1;
|
||||
border-radius: 6px;
|
||||
background: #f8fbfd;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: #1f3d56;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__name {
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__title em {
|
||||
flex: 0 0 auto;
|
||||
color: #c65a12;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 8px;
|
||||
margin-top: 4px;
|
||||
color: #6b8296;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
953
ruoyi-ui/src/views/kpi/plan/components/TrayCargoSpaceMap.vue
Normal file
953
ruoyi-ui/src/views/kpi/plan/components/TrayCargoSpaceMap.vue
Normal file
@@ -0,0 +1,953 @@
|
||||
<template>
|
||||
<section class="tray-map" v-loading="loading" data-testid="tray-cargo-space-map">
|
||||
<div class="tray-map__summary">
|
||||
<div v-for="item in summaryItems" :key="item.key" class="summary-card">
|
||||
<span class="summary-card__mark" :class="'summary-card__mark--' + item.key"></span>
|
||||
<span class="summary-card__label">{{ item.label }}</span>
|
||||
<strong>{{ item.count }}</strong>
|
||||
</div>
|
||||
<div class="summary-card summary-card--total">
|
||||
<span class="summary-card__label">本排货位</span>
|
||||
<strong>{{ locations.length }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tray-map__legend" aria-label="图例">
|
||||
<span v-for="item in legendItems" :key="item.status" class="legend-item">
|
||||
<i :class="'legend-item__swatch legend-item__swatch--' + item.status"></i>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="tray-map__viewport" ref="viewport">
|
||||
<div
|
||||
class="tray-grid"
|
||||
:style="gridStyle"
|
||||
:aria-label="platoonLabel + '货位平面图'"
|
||||
>
|
||||
<div class="axis-title axis-title--floor" style="grid-column: 1; grid-row: 1">层</div>
|
||||
<div class="axis-title axis-title--column" :style="bottomAxisTitleStyle">列</div>
|
||||
|
||||
<div
|
||||
v-for="floor in floorLabels"
|
||||
:key="'floor-' + floor"
|
||||
class="axis-label axis-label--floor"
|
||||
:style="axisFloorStyle(floor)"
|
||||
>{{ pad2(floor) }}</div>
|
||||
|
||||
<div
|
||||
v-for="column in columnLabels"
|
||||
:key="'column-' + column"
|
||||
class="axis-label axis-label--column"
|
||||
:style="axisColumnStyle(column)"
|
||||
>{{ pad2(column) }}</div>
|
||||
|
||||
<div
|
||||
v-for="cell in scaffoldCells"
|
||||
:key="cell.key"
|
||||
class="tray-grid__scaffold"
|
||||
:style="cell.style"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<button
|
||||
v-for="location in plotLocations"
|
||||
:key="location.cargoSpaceId || locationKey(location)"
|
||||
type="button"
|
||||
class="tray-location"
|
||||
:class="locationClass(location)"
|
||||
:style="cellStyle(location)"
|
||||
:data-status="String(location.status)"
|
||||
:data-column="String(location.theColumn)"
|
||||
:data-floor="String(location.theFloor)"
|
||||
data-testid="tray-location"
|
||||
@mouseenter="handleLocationEnter($event, location)"
|
||||
@mousemove="handleLocationMove($event)"
|
||||
@mouseleave="handleLocationLeave"
|
||||
@click="selectLocation(location)"
|
||||
>
|
||||
<span>{{ location.cargoSpaceId || shortLabel(location) }}</span>
|
||||
<i v-if="String(location.locked) === '1'" class="el-icon-lock tray-location__lock"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="tray-map__footer">
|
||||
<div class="location-readout location-readout--empty">
|
||||
鼠标悬停货位查看详情与库存
|
||||
</div>
|
||||
<span class="tray-map__hint">横向为列号(左→右递增),纵向为层号(下→上递增)</span>
|
||||
</footer>
|
||||
|
||||
<!-- 实际挂载到 document.body,避免被平面图 overflow/flex 裁切 -->
|
||||
<div
|
||||
v-show="tooltip.visible"
|
||||
ref="stockTooltip"
|
||||
class="stock-tooltip cargo-space-stock-tooltip"
|
||||
:class="{ 'is-multi': tooltip.stocks.length > 3 }"
|
||||
:style="tooltipStyle"
|
||||
data-testid="tray-stock-tooltip"
|
||||
@mouseenter="handleTooltipEnter"
|
||||
@mouseleave="handleLocationLeave"
|
||||
>
|
||||
<div class="stock-tooltip__head">
|
||||
<strong>{{ tooltip.cargoSpaceId || tooltip.title || '-' }}</strong>
|
||||
<span class="stock-tooltip__status">{{ tooltip.statusName || '-' }}</span>
|
||||
</div>
|
||||
<div class="stock-tooltip__meta">
|
||||
<div v-if="tooltip.coordText"><label>坐标</label><span>{{ tooltip.coordText }}</span></div>
|
||||
<div v-if="tooltip.trayCode"><label>托盘</label><span>{{ tooltip.trayCode }}</span></div>
|
||||
</div>
|
||||
<div v-if="tooltip.loading" class="stock-tooltip__loading">
|
||||
<i class="el-icon-loading"></i> 正在联查库存...
|
||||
</div>
|
||||
<div v-else-if="tooltip.stocks.length" class="stock-tooltip__body">
|
||||
<div class="stock-tooltip__count">库存 {{ tooltip.stocks.length }} 条</div>
|
||||
<div class="stock-tooltip__list" :class="{ 'is-cols': tooltip.stocks.length > 3 }">
|
||||
<div
|
||||
v-for="(stock, index) in tooltip.stocks"
|
||||
:key="index"
|
||||
class="stock-tooltip__item"
|
||||
>
|
||||
<div class="stock-tooltip__title">
|
||||
<span class="stock-tooltip__name">{{ stock.productName || stock.productId || '-' }}</span>
|
||||
<em>×{{ formatQty(stock.quantity) }}</em>
|
||||
</div>
|
||||
<div class="stock-tooltip__tags">
|
||||
<span>编码 {{ stock.productId || '-' }}</span>
|
||||
<span>批次 {{ stock.lotNo || '-' }}</span>
|
||||
<span>项目 {{ stock.projectNo || '-' }}</span>
|
||||
<span v-if="stock.trayCode">托盘 {{ stock.trayCode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="stock-tooltip__empty">当前货位无库存记录</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getChartMag } from '@/api/kpi/kpi'
|
||||
|
||||
export default {
|
||||
name: 'TrayCargoSpaceMap',
|
||||
props: {
|
||||
locations: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
floors: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
platoon: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
warehouseId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
stockCache: {},
|
||||
hoverTimer: null,
|
||||
hideTimer: null,
|
||||
requestSeq: 0,
|
||||
tooltip: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cursorX: 0,
|
||||
cursorY: 0,
|
||||
maxHeight: 360,
|
||||
maxWidth: 360,
|
||||
title: '',
|
||||
cargoSpaceId: '',
|
||||
cargoSpaceName: '',
|
||||
statusName: '',
|
||||
coordText: '',
|
||||
trayCode: '',
|
||||
stocks: [],
|
||||
cacheKey: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columnLabels() {
|
||||
if (this.columns && this.columns.length) {
|
||||
return this.columns.map(Number).sort((a, b) => a - b)
|
||||
}
|
||||
const set = new Set()
|
||||
this.locations.forEach(item => {
|
||||
if (item.theColumn != null) set.add(Number(item.theColumn))
|
||||
})
|
||||
return Array.from(set).sort((a, b) => a - b)
|
||||
},
|
||||
floorLabels() {
|
||||
if (this.floors && this.floors.length) {
|
||||
return this.floors.map(Number).sort((a, b) => b - a)
|
||||
}
|
||||
const set = new Set()
|
||||
this.locations.forEach(item => {
|
||||
if (item.theFloor != null) set.add(Number(item.theFloor))
|
||||
})
|
||||
return Array.from(set).sort((a, b) => b - a)
|
||||
},
|
||||
floorCount() {
|
||||
return Math.max(this.floorLabels.length, 1)
|
||||
},
|
||||
columnCount() {
|
||||
return Math.max(this.columnLabels.length, 1)
|
||||
},
|
||||
gridStyle() {
|
||||
return {
|
||||
gridTemplateColumns: `36px repeat(${this.columnCount}, minmax(0, 1fr))`,
|
||||
gridTemplateRows: `22px repeat(${this.floorCount}, minmax(18px, 1fr)) 26px`
|
||||
}
|
||||
},
|
||||
bottomAxisTitleStyle() {
|
||||
return {
|
||||
gridColumn: '1',
|
||||
gridRow: String(this.floorCount + 2)
|
||||
}
|
||||
},
|
||||
plotLocations() {
|
||||
return this.locations.filter(item => item.theColumn != null && item.theFloor != null)
|
||||
},
|
||||
scaffoldCells() {
|
||||
const cells = []
|
||||
this.floorLabels.forEach((floor, floorIndex) => {
|
||||
this.columnLabels.forEach((column, columnIndex) => {
|
||||
cells.push({
|
||||
key: `${column}-${floor}`,
|
||||
style: {
|
||||
gridColumn: String(columnIndex + 2),
|
||||
gridRow: String(floorIndex + 2)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
return cells
|
||||
},
|
||||
platoonLabel() {
|
||||
return this.platoon !== '' && this.platoon != null ? `${this.platoon}排` : '本排'
|
||||
},
|
||||
summaryItems() {
|
||||
const countStatus = status => this.locations.filter(item => String(item.status) === status).length
|
||||
return [
|
||||
{ key: 'empty', label: '空货位', count: countStatus('1') },
|
||||
{ key: 'full', label: '满货位', count: countStatus('2') },
|
||||
{ key: 'inAllot', label: '入库分配', count: countStatus('3') },
|
||||
{ key: 'outAllot', label: '出库分配', count: countStatus('4') },
|
||||
{ key: 'check', label: '需盘点', count: countStatus('5') + countStatus('6') }
|
||||
]
|
||||
},
|
||||
legendItems() {
|
||||
return [
|
||||
{ status: '1', label: '空货位' },
|
||||
{ status: '2', label: '满货位' },
|
||||
{ status: '3', label: '入库分配' },
|
||||
{ status: '4', label: '出库分配' },
|
||||
{ status: '5', label: '入库需盘点' },
|
||||
{ status: '6', label: '出库需盘点' }
|
||||
]
|
||||
},
|
||||
tooltipStyle() {
|
||||
return {
|
||||
position: 'fixed',
|
||||
left: this.tooltip.x + 'px',
|
||||
top: this.tooltip.y + 'px',
|
||||
zIndex: 5000,
|
||||
maxHeight: this.tooltip.maxHeight + 'px',
|
||||
maxWidth: this.tooltip.maxWidth + 'px'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
locations() {
|
||||
this.resetSelection()
|
||||
},
|
||||
platoon() {
|
||||
this.resetSelection()
|
||||
},
|
||||
warehouseId() {
|
||||
this.resetSelection()
|
||||
this.stockCache = {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.mountTooltip()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearTimers()
|
||||
this.unmountTooltip()
|
||||
},
|
||||
methods: {
|
||||
pad2(value) {
|
||||
return String(value).padStart(2, '0')
|
||||
},
|
||||
formatQty(value) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
return value
|
||||
},
|
||||
locationKey(location) {
|
||||
return `${location.thePlatoon || this.platoon || ''}-${location.theColumn}-${location.theFloor}`
|
||||
},
|
||||
shortLabel(location) {
|
||||
return `${this.pad2(location.theColumn)}-${this.pad2(location.theFloor)}`
|
||||
},
|
||||
axisFloorStyle(floor) {
|
||||
const index = this.floorLabels.indexOf(Number(floor))
|
||||
return {
|
||||
gridColumn: '1',
|
||||
gridRow: String(index + 2)
|
||||
}
|
||||
},
|
||||
axisColumnStyle(column) {
|
||||
const index = this.columnLabels.indexOf(Number(column))
|
||||
return {
|
||||
gridColumn: String(index + 2),
|
||||
gridRow: String(this.floorCount + 2)
|
||||
}
|
||||
},
|
||||
cellStyle(location) {
|
||||
const columnIndex = this.columnLabels.indexOf(Number(location.theColumn))
|
||||
const floorIndex = this.floorLabels.indexOf(Number(location.theFloor))
|
||||
return {
|
||||
gridColumn: String(columnIndex + 2),
|
||||
gridRow: String(floorIndex + 2)
|
||||
}
|
||||
},
|
||||
locationClass(location) {
|
||||
return [
|
||||
`tray-location--status-${location.status}`,
|
||||
{ 'tray-location--locked': String(location.locked) === '1' }
|
||||
]
|
||||
},
|
||||
statusName(status) {
|
||||
const names = {
|
||||
'1': '空货位',
|
||||
'2': '满货位',
|
||||
'3': '入库分配',
|
||||
'4': '出库分配',
|
||||
'5': '入库需盘点',
|
||||
'6': '出库需盘点'
|
||||
}
|
||||
return names[String(status)] || `状态${status}`
|
||||
},
|
||||
mountTooltip() {
|
||||
const el = this.$refs.stockTooltip
|
||||
if (el && el.parentNode !== document.body) {
|
||||
document.body.appendChild(el)
|
||||
}
|
||||
},
|
||||
unmountTooltip() {
|
||||
const el = this.$refs.stockTooltip
|
||||
if (el && el.parentNode === document.body) {
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
},
|
||||
clearTimers() {
|
||||
if (this.hoverTimer) {
|
||||
clearTimeout(this.hoverTimer)
|
||||
this.hoverTimer = null
|
||||
}
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer)
|
||||
this.hideTimer = null
|
||||
}
|
||||
},
|
||||
resetSelection() {
|
||||
this.tooltip.visible = false
|
||||
this.clearTimers()
|
||||
},
|
||||
updateTooltipPosition(event) {
|
||||
this.tooltip.cursorX = event.clientX
|
||||
this.tooltip.cursorY = event.clientY
|
||||
this.repositionTooltip()
|
||||
},
|
||||
/** 按视口与内容动态计算宽高与位置,避免贴底/裁切 */
|
||||
repositionTooltip() {
|
||||
this.mountTooltip()
|
||||
const gap = 14
|
||||
const margin = 8
|
||||
const tip = this.$refs.stockTooltip
|
||||
if (!tip) return
|
||||
|
||||
const vw = window.innerWidth
|
||||
const vh = window.innerHeight
|
||||
const cursorX = this.tooltip.cursorX || 0
|
||||
const cursorY = this.tooltip.cursorY || 0
|
||||
const stockCount = (this.tooltip.stocks && this.tooltip.stocks.length) || 0
|
||||
const preferWide = stockCount > 3
|
||||
|
||||
const rightSpace = vw - cursorX - gap - margin
|
||||
const leftSpace = cursorX - gap - margin
|
||||
let maxWidth = preferWide ? 560 : 360
|
||||
maxWidth = Math.min(maxWidth, Math.max(rightSpace, leftSpace, 240), vw - margin * 2)
|
||||
this.tooltip.maxWidth = Math.max(240, maxWidth)
|
||||
|
||||
const belowSpace = vh - cursorY - gap - margin
|
||||
const aboveSpace = cursorY - gap - margin
|
||||
const maxHeight = Math.max(belowSpace, aboveSpace, 160)
|
||||
this.tooltip.maxHeight = Math.min(vh - margin * 2, Math.max(160, maxHeight))
|
||||
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs.stockTooltip
|
||||
if (!el) return
|
||||
const tipWidth = el.offsetWidth || 280
|
||||
const tipHeight = el.offsetHeight || 120
|
||||
let x = cursorX + gap
|
||||
let y = cursorY + gap
|
||||
|
||||
if (x + tipWidth > vw - margin) {
|
||||
x = cursorX - tipWidth - gap
|
||||
}
|
||||
if (y + tipHeight > vh - margin) {
|
||||
y = cursorY - tipHeight - gap
|
||||
}
|
||||
if (x < margin) x = margin
|
||||
if (y < margin) y = margin
|
||||
if (tipHeight >= vh - margin * 2) {
|
||||
y = margin
|
||||
}
|
||||
this.tooltip.x = Math.round(x)
|
||||
this.tooltip.y = Math.round(y)
|
||||
})
|
||||
},
|
||||
fillTooltipBase(location) {
|
||||
const platoon = location.thePlatoon != null ? location.thePlatoon : this.platoon
|
||||
this.tooltip.title = location.cargoSpaceName || this.shortLabel(location)
|
||||
this.tooltip.cargoSpaceId = location.cargoSpaceId || this.shortLabel(location)
|
||||
this.tooltip.cargoSpaceName = location.cargoSpaceName || ''
|
||||
this.tooltip.statusName = this.statusName(location.status)
|
||||
this.tooltip.coordText = `${platoon != null && platoon !== '' ? this.pad2(platoon) + '排 ' : ''}${this.pad2(location.theColumn)}列 ${this.pad2(location.theFloor)}层`
|
||||
this.tooltip.trayCode = location.trayCode || ''
|
||||
this.tooltip.cacheKey = this.locationKey(location)
|
||||
},
|
||||
handleLocationEnter(event, location) {
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer)
|
||||
this.hideTimer = null
|
||||
}
|
||||
this.fillTooltipBase(location)
|
||||
this.tooltip.visible = true
|
||||
this.mountTooltip()
|
||||
this.updateTooltipPosition(event)
|
||||
const cached = this.stockCache[this.tooltip.cacheKey]
|
||||
if (cached) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = cached
|
||||
this.$nextTick(() => this.repositionTooltip())
|
||||
return
|
||||
}
|
||||
this.tooltip.loading = true
|
||||
this.tooltip.stocks = []
|
||||
if (this.hoverTimer) clearTimeout(this.hoverTimer)
|
||||
this.hoverTimer = setTimeout(() => {
|
||||
this.fetchStockInfo(location, this.tooltip.cacheKey)
|
||||
}, 120)
|
||||
},
|
||||
handleLocationMove(event) {
|
||||
if (this.tooltip.visible) this.updateTooltipPosition(event)
|
||||
},
|
||||
handleTooltipEnter() {
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer)
|
||||
this.hideTimer = null
|
||||
}
|
||||
},
|
||||
handleLocationLeave() {
|
||||
if (this.hideTimer) clearTimeout(this.hideTimer)
|
||||
this.hideTimer = setTimeout(() => {
|
||||
this.tooltip.visible = false
|
||||
this.clearTimers()
|
||||
}, 120)
|
||||
},
|
||||
selectLocation(location) {
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer)
|
||||
this.hideTimer = null
|
||||
}
|
||||
this.fillTooltipBase(location)
|
||||
this.tooltip.visible = true
|
||||
this.mountTooltip()
|
||||
const key = this.locationKey(location)
|
||||
if (this.stockCache[key]) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = this.stockCache[key]
|
||||
this.$nextTick(() => this.repositionTooltip())
|
||||
return
|
||||
}
|
||||
this.tooltip.loading = true
|
||||
this.tooltip.stocks = []
|
||||
this.fetchStockInfo(location, key)
|
||||
},
|
||||
fetchStockInfo(location, cacheKey) {
|
||||
if (!this.warehouseId) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = []
|
||||
return
|
||||
}
|
||||
const seq = ++this.requestSeq
|
||||
getChartMag({
|
||||
warehouseId: this.warehouseId,
|
||||
thePlatoon: String(location.thePlatoon != null ? location.thePlatoon : this.platoon),
|
||||
theColumn: String(location.theColumn),
|
||||
theFloor: String(location.theFloor)
|
||||
}).then(response => {
|
||||
if (seq !== this.requestSeq) return
|
||||
const data = response.data || {}
|
||||
const stocks = data.stocks || []
|
||||
this.stockCache[cacheKey] = stocks
|
||||
if (this.tooltip.cacheKey === cacheKey) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = stocks
|
||||
if (data.cargoSpaceId) this.tooltip.cargoSpaceId = data.cargoSpaceId
|
||||
if (data.status != null && data.status !== '') {
|
||||
this.tooltip.statusName = this.statusName(data.status)
|
||||
} else if (data.statusName) {
|
||||
this.tooltip.statusName = data.statusName
|
||||
}
|
||||
if (data.trayCode) this.tooltip.trayCode = data.trayCode
|
||||
this.$nextTick(() => this.repositionTooltip())
|
||||
}
|
||||
}).catch(() => {
|
||||
if (this.tooltip.cacheKey === cacheKey) {
|
||||
this.tooltip.loading = false
|
||||
this.tooltip.stocks = []
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tray-map {
|
||||
--tray-ink: #10243a;
|
||||
--tray-muted: #60758a;
|
||||
--tray-line: #cbd7e2;
|
||||
--tray-panel: #f5f8fb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 190px);
|
||||
min-height: 470px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #d7e1ea;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 34px rgba(20, 48, 76, 0.1);
|
||||
font-family: "Bahnschrift", "DIN Alternate", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.tray-map__summary {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
border-bottom: 1px solid #e1e8ef;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
border-right: 1px solid #e1e8ef;
|
||||
color: var(--tray-muted);
|
||||
}
|
||||
|
||||
.summary-card strong {
|
||||
margin-left: auto;
|
||||
color: var(--tray-ink);
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.summary-card__label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.summary-card__mark {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 1px #98aabd;
|
||||
}
|
||||
|
||||
.summary-card__mark--empty { background: #fff; }
|
||||
.summary-card__mark--full { background: #ed9c58; box-shadow: none; }
|
||||
.summary-card__mark--inAllot { background: #ffe38a; box-shadow: none; }
|
||||
.summary-card__mark--outAllot { background: #8ec5ff; box-shadow: none; }
|
||||
.summary-card__mark--check { background: #ffb7b7; box-shadow: none; }
|
||||
.summary-card--total { border-right: 0; background: #edf5f8; }
|
||||
|
||||
.tray-map__legend {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: clamp(7px, 1.1vw, 15px);
|
||||
min-height: 32px;
|
||||
padding: 6px 12px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #e5ebf0;
|
||||
color: #52687b;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.legend-item__swatch {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border: 1px solid #9eafbf;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legend-item__swatch--1 { background: #fff; }
|
||||
.legend-item__swatch--2 { background: #ed9c58; border-color: #d8833d; }
|
||||
.legend-item__swatch--3 { background: #ffe38a; border-color: #e3bd3c; }
|
||||
.legend-item__swatch--4 { background: #8ec5ff; border-color: #599bdc; }
|
||||
.legend-item__swatch--5 { background: #ffb7b7; border-color: #d95858; }
|
||||
.legend-item__swatch--6 { background: #cbb7f4; border-color: #8562c7; }
|
||||
|
||||
.tray-map__viewport {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(rgba(24, 62, 89, 0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(24, 62, 89, 0.025) 1px, transparent 1px),
|
||||
var(--tray-panel);
|
||||
background-size: 18px 18px;
|
||||
}
|
||||
|
||||
.tray-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid #aebdca;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
box-shadow: 0 6px 18px rgba(28, 55, 77, 0.08);
|
||||
}
|
||||
|
||||
.tray-grid__scaffold {
|
||||
z-index: 0;
|
||||
border-right: 1px solid #d7e0e8;
|
||||
border-bottom: 1px solid #d7e0e8;
|
||||
background: #fbfcfd;
|
||||
}
|
||||
|
||||
.axis-title,
|
||||
.axis-label {
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #29465e;
|
||||
background: #e9f0f5;
|
||||
font-size: clamp(8px, 0.72vw, 11px);
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.axis-title--floor {
|
||||
border-right: 1px solid #b9c7d2;
|
||||
border-bottom: 1px solid #b9c7d2;
|
||||
}
|
||||
|
||||
.axis-title--column {
|
||||
border-top: 1px solid #aebdca;
|
||||
border-right: 1px solid #b9c7d2;
|
||||
}
|
||||
|
||||
.axis-label--floor {
|
||||
border-right: 1px solid #b9c7d2;
|
||||
border-bottom: 1px solid #d7e0e8;
|
||||
}
|
||||
|
||||
.axis-label--column {
|
||||
border-top: 1px solid #aebdca;
|
||||
border-right: 1px solid #d7e0e8;
|
||||
}
|
||||
|
||||
.tray-location {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 0 2px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #b7c5d0;
|
||||
color: #213c52;
|
||||
background: #fff;
|
||||
font-family: "Bahnschrift", "DIN Alternate", monospace;
|
||||
font-size: clamp(7px, 0.68vw, 10px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: z-index 0s, transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
|
||||
}
|
||||
|
||||
.tray-location:hover,
|
||||
.tray-location:focus {
|
||||
z-index: 5;
|
||||
outline: none;
|
||||
transform: translateY(-1px) scale(1.035);
|
||||
box-shadow: 0 4px 10px rgba(14, 45, 69, 0.24);
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
|
||||
.tray-location--status-2 { border-color: #d8833d; background: #ed9c58; }
|
||||
.tray-location--status-3 { border-color: #e3bd3c; background: #ffe38a; }
|
||||
.tray-location--status-4 { border-color: #599bdc; background: #8ec5ff; }
|
||||
.tray-location--status-5 { border-color: #d95858; color: #6e1111; background: #ffb7b7; }
|
||||
.tray-location--status-6 { border-color: #8562c7; color: #321866; background: #cbb7f4; }
|
||||
.tray-location--locked { box-shadow: inset 0 0 0 2px #d92d20; }
|
||||
|
||||
.tray-location__lock {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.tray-map__footer {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 34px;
|
||||
padding: 5px 12px;
|
||||
border-top: 1px solid #e2e9ef;
|
||||
color: #577084;
|
||||
background: #f8fafc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.location-readout {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px 14px;
|
||||
}
|
||||
|
||||
.location-readout span:not(:first-child) {
|
||||
padding-left: 14px;
|
||||
border-left: 1px solid #ccd8e1;
|
||||
}
|
||||
|
||||
.location-readout__id {
|
||||
color: #153e59;
|
||||
font-family: "Bahnschrift", monospace;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.location-readout--empty,
|
||||
.tray-map__hint {
|
||||
color: #8293a1;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.tray-map__hint {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 非 scoped:悬浮层挂到 body 后仍可生效 */
|
||||
.cargo-space-stock-tooltip.stock-tooltip {
|
||||
position: fixed !important;
|
||||
z-index: 5000 !important;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: max-content;
|
||||
min-width: 240px;
|
||||
overflow: hidden;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #c9d7e3;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 12px 28px rgba(16, 42, 64, 0.22);
|
||||
color: #29465e;
|
||||
pointer-events: auto;
|
||||
font-family: "Microsoft YaHei", "Bahnschrift", sans-serif;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip.is-multi {
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__head {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e4ebf1;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__head strong {
|
||||
color: #153e59;
|
||||
font-family: "Bahnschrift", monospace;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.3px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__status {
|
||||
flex: 0 0 auto;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
background: #eef4f8;
|
||||
color: #4d667a;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__meta {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
color: #5f7386;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__meta > div {
|
||||
display: grid;
|
||||
grid-template-columns: 36px 1fr;
|
||||
gap: 6px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__meta label {
|
||||
color: #8aa0b2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__meta span {
|
||||
color: #3d5569;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__loading,
|
||||
.cargo-space-stock-tooltip .stock-tooltip__empty {
|
||||
flex: 0 0 auto;
|
||||
color: #6b8296;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__count {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 6px;
|
||||
color: #7d93a6;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__list.is-cols {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__list:not(.is-cols) .stock-tooltip__item + .stock-tooltip__item {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #dce5ec;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__list.is-cols .stock-tooltip__item {
|
||||
padding: 8px;
|
||||
border: 1px solid #e4ebf1;
|
||||
border-radius: 6px;
|
||||
background: #f8fbfd;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: #1f3d56;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__name {
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__title em {
|
||||
flex: 0 0 auto;
|
||||
color: #c65a12;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cargo-space-stock-tooltip .stock-tooltip__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 8px;
|
||||
margin-top: 4px;
|
||||
color: #6b8296;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -1,121 +1,220 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="app-container plan-page">
|
||||
<div class="search-box">
|
||||
|
||||
<el-form :model="queryParams" ref="queryForm" :inline="true">
|
||||
<!-- 仓库 -->
|
||||
<el-form ref="queryForm" :model="queryParams" :inline="true">
|
||||
<el-form-item label="仓库" prop="warehouseId">
|
||||
<el-select v-model="queryParams.warehouseId" placeholder="请选择仓库" clearable @change="handleWarehouseChange">
|
||||
<el-select
|
||||
v-model="queryParams.warehouseId"
|
||||
placeholder="请选择仓库"
|
||||
clearable
|
||||
@change="handleWarehouseChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in warehouseOptions"
|
||||
:key="dict.warehouseId"
|
||||
:label="dict.warehouseName"
|
||||
:value="dict.warehouseId"
|
||||
v-for="item in warehouseOptions"
|
||||
:key="item.warehouseId"
|
||||
:label="item.warehouseName"
|
||||
:value="item.warehouseId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 巷道 -->
|
||||
<el-form-item label="巷道" prop="roadwayId">
|
||||
<el-select v-model="queryParams.roadwayId" placeholder="请选择巷道" clearable @change="handleRoadwayChange">
|
||||
<el-option v-for="item in roadwayOptions" :key="item.value" :label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 排 -->
|
||||
<el-form-item label="排">
|
||||
<el-select v-model="queryParams.thePlatoon" placeholder="请选择排" clearable style="width: 200px" @change="handlePlatoonChange">
|
||||
<el-option v-for="item in platoonOptions" :key="item.value" :label="item.label" :value="item.value"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="!isDenseWarehouse">
|
||||
<el-form-item label="巷道" prop="roadwayId">
|
||||
<el-select
|
||||
v-model="queryParams.roadwayId"
|
||||
placeholder="请选择巷道"
|
||||
clearable
|
||||
@change="handleRoadwayChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in roadwayOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排" prop="thePlatoon">
|
||||
<el-select
|
||||
v-model="queryParams.thePlatoon"
|
||||
placeholder="请选择排"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@change="handlePlatoonChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in platoonOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
icon="el-icon-refresh"
|
||||
size="small"
|
||||
data-testid="tray-refresh"
|
||||
@click="loadChartData"
|
||||
>刷新</el-button>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<el-form-item label="层" prop="theFloor" class="dense-floor-control">
|
||||
<el-radio-group v-model="selectedDenseFloor" size="small" data-testid="floor-switch">
|
||||
<el-radio-button
|
||||
v-for="floor in denseFloors"
|
||||
:key="floor"
|
||||
:label="Number(floor)"
|
||||
>{{ floor }}层</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
icon="el-icon-refresh"
|
||||
size="small"
|
||||
data-testid="dense-refresh"
|
||||
@click="loadDenseChartData"
|
||||
>刷新</el-button>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div id="container" v-show="queryParams.warehouseId !== '04'" style="height: 60vh"></div>
|
||||
<div v-if="isDenseWarehouse" class="chart-container chart-container--dense">
|
||||
<dense-cargo-space-map
|
||||
:value="selectedDenseFloor"
|
||||
:locations="denseLocations"
|
||||
:warehouse-id="queryParams.warehouseId"
|
||||
:loading="denseLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="chart-container chart-container--tray">
|
||||
<tray-cargo-space-map
|
||||
:locations="trayLocations"
|
||||
:columns="trayColumns"
|
||||
:floors="trayFloors"
|
||||
:platoon="queryParams.thePlatoon"
|
||||
:warehouse-id="queryParams.warehouseId"
|
||||
:loading="trayLoading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import {listWarehouse} from "@/api/bas/warehouse";
|
||||
import {listRoadway} from "@/api/bas/roadway";
|
||||
import {getPlatoons} from "@/api/bas/cargospace";
|
||||
import {getChartData, getChartMag} from "@/api/kpi/kpi";
|
||||
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 DenseCargoSpaceMap from './components/DenseCargoSpaceMap.vue'
|
||||
import TrayCargoSpaceMap from './components/TrayCargoSpaceMap.vue'
|
||||
|
||||
export default {
|
||||
name: 'SpacePlan',
|
||||
// 必须与菜单路由名称一致,keep-alive 才能缓存,避免切页/聚焦后整页重载
|
||||
name: '货位平面图',
|
||||
components: {
|
||||
DenseCargoSpaceMap,
|
||||
TrayCargoSpaceMap
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
queryParams: {
|
||||
warehouseId: '01',
|
||||
roadwayId: '1',
|
||||
thePlatoon: '1',
|
||||
theFloor: '',
|
||||
specifications: '',
|
||||
quantity_x: '',
|
||||
quantity_j: '',
|
||||
layer_quantity: '',
|
||||
classgroup_number: '',
|
||||
class_workshop_date_time: '',
|
||||
class_now_date_time: '',
|
||||
print_date: '',
|
||||
lot_no: '',
|
||||
|
||||
theFloor: ''
|
||||
},
|
||||
warehouseOptions: [],
|
||||
roadwayOptions: [],
|
||||
floorOptions: [],
|
||||
platoonOptions: [],
|
||||
normalChart: null,
|
||||
specialChart: null,
|
||||
loadingKeys: {},
|
||||
chartData: {
|
||||
x: [],
|
||||
y: [],
|
||||
empty: [],
|
||||
full: [],
|
||||
inAllot: [],
|
||||
outAllot: [],
|
||||
inCheck: [],
|
||||
outCheck: [],
|
||||
resultsOf: [],
|
||||
outCargo: [],
|
||||
theLocked: []
|
||||
},
|
||||
queryWarehouseParam:{
|
||||
denseLoading: false,
|
||||
denseFloors: [],
|
||||
denseLocations: [],
|
||||
selectedDenseFloor: 1,
|
||||
trayLoading: false,
|
||||
trayLocations: [],
|
||||
trayColumns: [],
|
||||
trayFloors: [],
|
||||
// 是否已完成首次加载,避免 keep-alive 激活时重复拉数
|
||||
pageInitialized: false,
|
||||
queryWarehouseParam: {
|
||||
pageNum: 1,
|
||||
pageSize: 99,
|
||||
typeData:[1]
|
||||
typeData: [1]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isDenseWarehouse() {
|
||||
return String(this.queryParams.warehouseId) === '03'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getWarehouseList()
|
||||
this.initCharts()
|
||||
this.handleWarehouseChange()
|
||||
this.initPage()
|
||||
},
|
||||
activated() {
|
||||
// 从其他页签切回时不重新请求/重置仓库筛选
|
||||
if (!this.pageInitialized) {
|
||||
this.initPage()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWarehouseList() {
|
||||
|
||||
listWarehouse(this.queryWarehouseParam).then(response => {
|
||||
this.warehouseOptions = response.rows;
|
||||
});
|
||||
initPage() {
|
||||
if (this.pageInitialized) return
|
||||
this.getWarehouseList()
|
||||
this.handleWarehouseChange()
|
||||
this.pageInitialized = true
|
||||
},
|
||||
initCharts() {
|
||||
this.normalChart = echarts.init(document.getElementById('container'))
|
||||
getWarehouseList() {
|
||||
listWarehouse(this.queryWarehouseParam).then(response => {
|
||||
this.warehouseOptions = response.rows || []
|
||||
})
|
||||
},
|
||||
resetTrayMap() {
|
||||
this.trayLocations = []
|
||||
this.trayColumns = []
|
||||
this.trayFloors = []
|
||||
},
|
||||
resetDenseMap() {
|
||||
this.denseFloors = []
|
||||
this.denseLocations = []
|
||||
},
|
||||
handleWarehouseChange() {
|
||||
this.queryParams.roadwayId = ''
|
||||
this.queryParams.thePlatoon = ''
|
||||
this.roadwayOptions = []
|
||||
this.platoonOptions = []
|
||||
this.floorOptions = []
|
||||
|
||||
if (!this.queryParams.warehouseId) {
|
||||
this.resetTrayMap()
|
||||
this.resetDenseMap()
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isDenseWarehouse) {
|
||||
this.resetTrayMap()
|
||||
this.loadDenseChartData()
|
||||
return
|
||||
}
|
||||
|
||||
this.resetDenseMap()
|
||||
listRoadway({ warehouseId: this.queryParams.warehouseId }).then(response => {
|
||||
// 异步返回时若已切到密集库,忽略本次结果,避免覆盖当前选择
|
||||
if (this.isDenseWarehouse) return
|
||||
const roadways = response.rows || []
|
||||
if (roadways && roadways.length > 0) {
|
||||
this.roadwayOptions = roadways.map(item => ({ value: item.roadwayId, label: item.roadwayName || item.roadwayId }))
|
||||
if (roadways.length > 0) {
|
||||
this.roadwayOptions = roadways.map(item => ({
|
||||
value: item.roadwayId,
|
||||
label: item.roadwayName || item.roadwayId
|
||||
}))
|
||||
this.queryParams.roadwayId = this.roadwayOptions[0].value
|
||||
this.handleRoadwayChange()
|
||||
} else {
|
||||
this.resetTrayMap()
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取巷道列表失败:', error)
|
||||
@@ -123,17 +222,28 @@ export default {
|
||||
})
|
||||
},
|
||||
handleRoadwayChange() {
|
||||
if (this.isDenseWarehouse) return
|
||||
|
||||
this.queryParams.thePlatoon = ''
|
||||
this.platoonOptions = []
|
||||
getPlatoons({ warehouseId: this.queryParams.warehouseId, roadwayId: this.queryParams.roadwayId }).then(response => {
|
||||
if (!this.queryParams.roadwayId) {
|
||||
this.resetTrayMap()
|
||||
return
|
||||
}
|
||||
|
||||
getPlatoons({
|
||||
warehouseId: this.queryParams.warehouseId,
|
||||
roadwayId: this.queryParams.roadwayId
|
||||
}).then(response => {
|
||||
if (this.isDenseWarehouse) return
|
||||
const platoons = response.data || []
|
||||
if (platoons && platoons.length > 0) {
|
||||
if (platoons.length > 0) {
|
||||
this.platoonOptions = platoons.map(item => ({ value: item, label: item + '排' }))
|
||||
this.queryParams.thePlatoon = this.platoonOptions[0].value
|
||||
this.loadChartData()
|
||||
} else {
|
||||
this.platoonOptions = []
|
||||
this.queryParams.thePlatoon = null
|
||||
this.resetTrayMap()
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取排列表失败:', error)
|
||||
@@ -141,257 +251,73 @@ export default {
|
||||
})
|
||||
},
|
||||
handlePlatoonChange() {
|
||||
console.log( this.queryParams.thePlatoon)
|
||||
this.loadChartData()
|
||||
if (this.queryParams.thePlatoon) {
|
||||
this.loadChartData()
|
||||
} else {
|
||||
this.resetTrayMap()
|
||||
}
|
||||
},
|
||||
loadDenseChartData() {
|
||||
if (!this.isDenseWarehouse) return
|
||||
|
||||
this.denseLoading = true
|
||||
getDenseChartData({ warehouseId: this.queryParams.warehouseId }).then(response => {
|
||||
const data = response.data || {}
|
||||
this.denseFloors = (data.floors || []).map(Number)
|
||||
this.denseLocations = data.locations || []
|
||||
if (!this.denseFloors.includes(Number(this.selectedDenseFloor))) {
|
||||
this.selectedDenseFloor = this.denseFloors.length > 0 ? this.denseFloors[0] : 1
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('获取密集库平面图失败:', error)
|
||||
this.$modal.msgError('获取密集库平面图失败')
|
||||
}).finally(() => {
|
||||
this.denseLoading = false
|
||||
})
|
||||
},
|
||||
loadChartData() {
|
||||
if (this.isDenseWarehouse || !this.queryParams.thePlatoon) return
|
||||
|
||||
this.trayLoading = true
|
||||
getChartData(this.queryParams).then(response => {
|
||||
const data = response.data
|
||||
const adjustCoordinates = (data) => {
|
||||
return data.map(item => {
|
||||
const columnLabel = item[1] + '层';
|
||||
const floorLabel = item[0] + '列';
|
||||
const columnIndex = this.chartData.y.indexOf(columnLabel);
|
||||
const floorIndex = this.chartData.x.indexOf(floorLabel);
|
||||
return [floorIndex > -1 ? floorIndex : item[1] - 1, columnIndex > -1 ? columnIndex : item[0] - 1, item[2]];
|
||||
});
|
||||
};
|
||||
this.chartData.x = eval('(' + data["column"] + ')');
|
||||
this.chartData.y = eval('(' + data["floor"] + ')');
|
||||
this.chartData.empty = adjustCoordinates(eval('(' + data["empty"] + ')'));
|
||||
this.chartData.full = adjustCoordinates(eval('(' + data["full"] + ')'));
|
||||
this.chartData.inAllot = adjustCoordinates(eval('(' + data["inAllot"] + ')'));
|
||||
this.chartData.outAllot = adjustCoordinates(eval('(' + data["outAllot"] + ')'));
|
||||
this.chartData.inCheck = adjustCoordinates(eval('(' + data["inCheck"] + ')'));
|
||||
this.chartData.outCheck = adjustCoordinates(eval('(' + data["outCheck"] + ')'));
|
||||
this.updateNormalChart()
|
||||
if (this.isDenseWarehouse) return
|
||||
const data = response.data || {}
|
||||
this.trayLocations = data.locations || []
|
||||
this.trayColumns = (data.columns || []).map(Number)
|
||||
this.trayFloors = (data.floors || []).map(Number)
|
||||
}).catch(error => {
|
||||
console.error('获取托盘库平面图失败:', error)
|
||||
this.$modal.msgError('获取货位平面图失败')
|
||||
}).finally(() => {
|
||||
this.trayLoading = false
|
||||
})
|
||||
|
||||
|
||||
},
|
||||
|
||||
updateNormalChart() {
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params) => {
|
||||
return this.formatTooltip(params)
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: '库位分布平面图',
|
||||
subtext: (() => {
|
||||
const sel = this.platoonOptions.find(o => String(o.value) === String(this.queryParams.thePlatoon))
|
||||
return sel && sel.label ? sel.label : ''
|
||||
})()
|
||||
},
|
||||
grid: {
|
||||
width: '90%',
|
||||
height: '75%',
|
||||
top: '15%',
|
||||
left: '5%'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: this.chartData.x,
|
||||
splitArea: {show: true}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: this.chartData.y,
|
||||
splitArea: {show: true}
|
||||
},
|
||||
visualMap: {
|
||||
show: false,
|
||||
min: 1,
|
||||
max: 6,
|
||||
calculable: true,
|
||||
orient: 'horizontal',
|
||||
left: 'center',
|
||||
inRange: {
|
||||
color: ['#AEEEEE', '#FF9900', '#FFFF00', '#00CC00', '#FF0000', '#0000FF']
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: [
|
||||
{
|
||||
name: '空位',
|
||||
itemStyle: { color: '#AEEEEE' }
|
||||
},
|
||||
{
|
||||
name: '满位',
|
||||
itemStyle: { color: '#FF9900' }
|
||||
},
|
||||
{
|
||||
name: '入库分配',
|
||||
itemStyle: { color: '#FFFF00' }
|
||||
},
|
||||
{
|
||||
name: '出库分配',
|
||||
itemStyle: { color: '#00cc00' }
|
||||
},
|
||||
{
|
||||
name: '入库需盘点',
|
||||
itemStyle: { color: '#FF0000' }
|
||||
},
|
||||
{
|
||||
name: '出库需盘点',
|
||||
itemStyle: { color: '#0000FF' }
|
||||
}
|
||||
],
|
||||
textStyle: {
|
||||
color: '#333'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '空位',
|
||||
type: 'heatmap',
|
||||
data: this.chartData.empty,
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '满位',
|
||||
type: 'heatmap',
|
||||
data: this.chartData.full,
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '入库分配',
|
||||
type: 'heatmap',
|
||||
data: this.chartData.inAllot,
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '出库分配',
|
||||
type: 'heatmap',
|
||||
data: this.chartData.outAllot,
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '入库需盘点',
|
||||
type: 'heatmap',
|
||||
data: this.chartData.inCheck,
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '出库需盘点',
|
||||
type: 'heatmap',
|
||||
data: this.chartData.outCheck,
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.normalChart.setOption(option)
|
||||
},
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
const context = this;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
},
|
||||
formatTooltip(params) {
|
||||
const rowIndex = params.data[1];
|
||||
const rowLabel = this.chartData.y[rowIndex];
|
||||
const colIndex = params.data[0];
|
||||
const colLabel = this.chartData.x[colIndex];
|
||||
|
||||
if (params.seriesName !== '满位') {
|
||||
return params.seriesName + '<br>' + this.queryParams.thePlatoon + '排' + colLabel + rowLabel;
|
||||
}
|
||||
|
||||
const returnData = {
|
||||
"thePlatoon": this.queryParams.thePlatoon,
|
||||
"theColumn": colLabel.replace(/[^\d]/g, ''),
|
||||
"theFloor": rowLabel.replace(/[^\d]/g, ''),
|
||||
"warehouseId": this.queryParams.warehouseId
|
||||
};
|
||||
|
||||
|
||||
const key = `${params.seriesName}-${params.data[0]}-${params.data[1]}`;
|
||||
|
||||
if (this.loadingKeys && this.loadingKeys[key]) {
|
||||
return params.seriesName + '<br>' + this.queryParams.thePlatoon + '排' + colLabel + rowLabel;
|
||||
}
|
||||
|
||||
const baseStr = `${params.seriesName}<br>${this.queryParams.thePlatoon}排${colLabel}${rowLabel}`;
|
||||
|
||||
if (!this.loadingKeys) this.loadingKeys = {};
|
||||
this.loadingKeys[key] = true;
|
||||
|
||||
this.debounce(() => {
|
||||
getChartMag(returnData)
|
||||
.then(response => {
|
||||
const msg = response.data.msg;
|
||||
const str = `${baseStr}<br>${msg}`;
|
||||
this.normalChart.setOption({
|
||||
tooltip: {
|
||||
formatter: (p) => {
|
||||
if (p.seriesName === params.seriesName &&
|
||||
p.data[0] === params.data[0] &&
|
||||
p.data[1] === params.data[1]) {
|
||||
return str;
|
||||
}
|
||||
return this.formatTooltip(p);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取货位信息失败:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
delete this.loadingKeys[key];
|
||||
});
|
||||
}, 300)();
|
||||
|
||||
return baseStr;
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
<style lang="scss" scoped>
|
||||
.plan-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 18px;
|
||||
background: #f2f5f8;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
background-color: #fff;
|
||||
padding: 20px 0 0 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
padding: 18px 18px 0;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #e2e8ef;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 5px 18px rgba(22, 49, 72, 0.06);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
.dense-floor-control ::v-deep .el-radio-button__inner {
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.chart-container--dense,
|
||||
.chart-container--tray {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<i class="el-icon-tickets"></i> 基本信息
|
||||
</div>
|
||||
<el-form ref="mainForm" :model="mainForm" :rules="mainRules" label-width="100px" size="small">
|
||||
<el-row :gutter="24">
|
||||
<el-row :gutter="24" class="stable-form-grid">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="仓库" prop="warehouseId">
|
||||
<el-select
|
||||
@@ -326,7 +326,7 @@ export default {
|
||||
saving: false,
|
||||
mainForm: {
|
||||
outType: null,
|
||||
stockSource: null,
|
||||
stockSource: '1',
|
||||
erpBill: null,
|
||||
documentN: null,
|
||||
documentDat: null,
|
||||
@@ -365,7 +365,9 @@ export default {
|
||||
warehouseId: null,
|
||||
productId: null,
|
||||
productName: null,
|
||||
lotNo: null
|
||||
lotNo: null,
|
||||
stockStatus: '1',
|
||||
productStatus: '1'
|
||||
},
|
||||
selectedStocks: []
|
||||
}
|
||||
@@ -566,6 +568,34 @@ export default {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stable-form-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stable-form-grid > .el-col {
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.create-stockout ::v-deep .el-form-item {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.create-stockout ::v-deep .el-select,
|
||||
.create-stockout ::v-deep .el-input,
|
||||
.create-stockout ::v-deep .el-date-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-stockout ::v-deep .el-form-item__content {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.create-stockout ::v-deep .el-form-item__error {
|
||||
line-height: 16px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -542,7 +542,8 @@ export default {
|
||||
listStockDetail({
|
||||
productId: detail.productId,
|
||||
warehouseId: this.currentStockout ? this.currentStockout.warehouseId : null,
|
||||
stockStatus: '1'
|
||||
stockStatus: '1',
|
||||
productStatus: '1'
|
||||
}).then(res => {
|
||||
this.stockList = res.rows || []
|
||||
this.stockLoading = false
|
||||
|
||||
@@ -9,6 +9,16 @@
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型" prop="inOrOut">
|
||||
<el-select v-model="queryParams.inOrOut" placeholder="请选择任务类型" clearable>
|
||||
<el-option
|
||||
v-for="dict in dict.type.pd_in_job_type"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择任务状态" clearable>
|
||||
<el-option
|
||||
@@ -108,7 +118,7 @@
|
||||
<el-table-column label="任务号" align="center" prop="taskNo" />
|
||||
<el-table-column label="任务类型" align="center" prop="inOrOut">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.mydo_job_status" :value="scope.row.inOrOut"/>
|
||||
<dict-tag :options="dict.type.pd_in_job_type" :value="scope.row.inOrOut"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行阶段" align="center" prop="executionPhase">
|
||||
@@ -198,7 +208,7 @@ import { listJobdetail } from "@/api/pd/inJobdetail"
|
||||
|
||||
export default {
|
||||
name: "PdJobIn",
|
||||
dicts: ['mydo_job_execution_phase', 'mydo_job_status'],
|
||||
dicts: ['mydo_job_execution_phase', 'mydo_job_status', 'pd_in_job_type'],
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<el-form-item label="任务类型" prop="inOrOut">
|
||||
<el-select v-model="queryParams.inOrOut" placeholder="请选择任务类型" clearable>
|
||||
<el-option
|
||||
v-for="dict in dict.type.mydo_job_status"
|
||||
v-for="dict in dict.type.pd_out_job_type"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
@@ -108,7 +108,7 @@
|
||||
<el-table-column label="任务号" align="center" prop="taskNo" />
|
||||
<el-table-column label="任务类型" align="center" prop="inOrOut">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.mydo_job_status" :value="scope.row.inOrOut"/>
|
||||
<dict-tag :options="dict.type.pd_out_job_type" :value="scope.row.inOrOut"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行阶段" align="center" prop="executionPhase">
|
||||
@@ -200,7 +200,7 @@ import { listJobdetail } from "@/api/pd/outJobdetail"
|
||||
|
||||
export default {
|
||||
name: "PdJobOut",
|
||||
dicts: ['mydo_job_execution_phase', 'mydo_job_status'],
|
||||
dicts: ['mydo_job_execution_phase', 'mydo_job_status', 'pd_out_job_type'],
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
|
||||
Reference in New Issue
Block a user