feat: 完善WCS调度、库存追溯和出库分配

This commit is contained in:
zgw
2026-07-15 16:24:07 +08:00
parent 9d3aa7a3bd
commit 66ac268524
101 changed files with 12596 additions and 669 deletions

View File

@@ -59,6 +59,17 @@ export function getChartData(query) {
})
}
/**
* 密集库货位平面图数据
*/
export function getDenseChartData(query) {
return request({
url: '/kpi/denseCargoSpacePlan',
method: 'get',
params: query
})
}
/**
* 货位平面图-货位详情
*/

View File

@@ -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>

View File

@@ -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,

View File

@@ -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;

View File

@@ -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" />

View File

@@ -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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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

View File

@@ -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 {
// 遮罩层

View File

@@ -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 {
// 遮罩层