提交3d钢卷,追溯和意库内容

This commit is contained in:
2026-06-02 10:08:47 +08:00
parent 12545f7c8b
commit 29328d70e9
2 changed files with 228 additions and 62 deletions

View File

@@ -28,15 +28,48 @@
<span class="vt">Z · (R)</span>
</div>
<div class="wh3d-dlg" v-show="detail">
<div class="wh3d-dlg" v-show="detail" v-loading="detailLoading">
<button class="x" @click="closeDetail">×</button>
<h3>钢卷 {{ detail && detail.id }}</h3>
<div class="rw"><span class="k">库位</span><span class="v">{{ detail && detail.posKey }} · {{ detail && detail.layer }} </span></div>
<div class="rw"><span class="k">钢卷号</span><span class="v">{{ detail && detail.id }}</span></div>
<div class="rw"><span class="k">钢种</span><span class="v">{{ detail && detail.grade }}</span></div>
<div class="rw"><span class="k">宽度×外径</span><span class="v">{{ detail && (detail.width + ' × Ø' + detail.od) }}</span></div>
<div class="rw"><span class="k">重量(t)</span><span class="v">{{ detail && detail.weight }}</span></div>
<div class="rw"><span class="k">扫码状态</span><span class="v">{{ detail && scanText(detail.scan) }}</span></div>
<h3>钢卷 {{ detail && detail.coilNo }}</h3>
<div class="rw"><span class="k">库位</span><span class="v">{{ detail && (detail.warehouseName || detail.posKey) }} · {{ detail && detail.layer }} </span></div>
<div class="rw"><span class="k">钢卷号</span><span class="v">{{ detail && detail.coilNo }}</span></div>
<div class="rw"><span class="k">入库钢卷号</span><span class="v">{{ detail && (detail.enterCoilNo || '-') }}</span></div>
<div class="rw"><span class="k">材质</span><span class="v">{{ detail && detail.material }}</span></div>
<div class="rw"><span class="k">规格</span><span class="v">{{ detail && detail.specification }}</span></div>
<div class="rw"><span class="k">毛重(kg)</span><span class="v">{{ detail && fmtNum(detail.grossWeight) }}</span></div>
<div class="rw"><span class="k">净重(kg)</span><span class="v">{{ detail && fmtNum(detail.netWeight) }}</span></div>
<div class="rw"><span class="k">质量状态</span><span class="v">{{ detail && detail.qualityStatus }}</span></div>
<div class="sec-title">移库信息</div>
<table class="sec-tbl" v-if="detail && detail.moves && detail.moves.length">
<thead><tr><th>时间</th><th>库位</th><th>操作</th><th>出入库</th></tr></thead>
<tbody>
<tr v-for="(m, i) in detail.moves" :key="i">
<td>{{ m.time }}</td>
<td>{{ m.warehouse }}</td>
<td>{{ m.operationType }}</td>
<td>{{ m.inOutType }}</td>
</tr>
</tbody>
</table>
<div v-else-if="detail && !detailLoading" class="sec-empty">暂无移库记录</div>
<div class="sec-title">生产追溯</div>
<div v-if="detail && detail.trace && detail.trace.steps && detail.trace.steps.length" class="trace">
<div v-for="(step, i) in traceSteps" :key="i" class="trace-step">
<div class="ts-head">
<span class="ts-action">{{ step.action }}</span>
<span class="ts-time">{{ step.time }}</span>
</div>
<div class="ts-op" v-if="step.operation">操作员{{ step.operation }}</div>
<div class="ts-coils" v-if="step.newCoilInfoList && step.newCoilInfoList.length">
<span v-for="(c, idx) in step.newCoilInfoList" :key="idx" class="ts-tag" v-if="c && c.currentCoilNo && c.currentCoilNo !== '-'">
{{ c.currentCoilNo }}
</span>
</div>
</div>
</div>
<div v-else-if="detail && !detailLoading" class="sec-empty">暂无追溯数据</div>
</div>
</div>
@@ -55,15 +88,15 @@
<h2>钢卷明细</h2>
<div class="ctb">
<table>
<thead><tr><th>库位</th><th>钢卷号</th><th>规格</th><th>状态</th></tr></thead>
<thead><tr><th>库位</th><th>钢卷号</th><th>规格</th><th>重量</th></tr></thead>
<tbody>
<tr v-for="c in tableRows" :key="c.id"
:class="{ sel: detail && detail.id === c.id }"
@click="showCoilDetail(c.id)">
<td>{{ c.posKey }} L{{ c.layer }}</td>
<td>{{ c.id }}</td>
<td>{{ c.width }}ר{{ c.od }}</td>
<td :class="scanClass(c.scan)">{{ scanShort(c.scan) }}</td>
<td>{{ c.coilNo }}</td>
<td>{{ c.specification }}</td>
<td>{{ fmtNum(c.grossWeight) }}</td>
</tr>
</tbody>
</table>
@@ -75,6 +108,19 @@
<script>
import * as THREE from 'three';
import { listMaterialCoil, getMaterialCoilTrace } from '@/api/wms/coil';
import { getCoilWarehouseOperationLogByCoilId } from '@/api/wms/coilWarehouseOperationLog';
const OP_TYPE_MAP = { 1: '收货', 2: '加工', 3: '调拨', 4: '发货' };
const IN_OUT_MAP = { 1: '入库', 2: '出库' };
function formatDate(d) {
if (!d) return '-';
const dt = typeof d === 'string' ? new Date(d.replace(' ', 'T')) : new Date(d);
if (isNaN(dt.getTime())) return String(d);
const p = (n) => (n < 10 ? '0' + n : '' + n);
return `${dt.getFullYear()}-${p(dt.getMonth() + 1)}-${p(dt.getDate())} ${p(dt.getHours())}:${p(dt.getMinutes())}`;
}
const GRADE_LIST = ['SPHC', 'SPHD', 'Q195', 'Q235', 'DC01', 'DC03', 'SUS304'];
const CELL_W = 3.2, CELL_D = 3.2, GAP = 0.5;
@@ -99,6 +145,7 @@ export default {
name: 'Warehouse3D',
props: {
warehouseList: { type: Array, default: () => [] },
parentId: { type: [String, Number], default: '' },
},
data() {
return {
@@ -110,6 +157,8 @@ export default {
cols: 14,
rows: 6,
themeMode: localStorage.getItem('wh3d-theme') || 'dark',
materialCoils: [],
detailLoading: false,
viewBtns: [
{ k: 'iso', l: '等轴' },
{ k: 'top', l: '俯视' },
@@ -164,9 +213,14 @@ export default {
return a.layer - b.layer;
});
},
traceSteps() {
if (!this.detail || !this.detail.trace || !this.detail.trace.steps) return [];
return this.detail.trace.steps.slice().reverse();
},
},
watch: {
warehouseList() { if (this._ready) this.rebuild(); },
warehouseList() { if (this._ready) this.fetchAndRebuild(); },
parentId() { if (this._ready) this.fetchAndRebuild(); },
themeColor() { if (this._ready) this.rebuild(); },
themeMode() {
if (!this._ready) return;
@@ -178,6 +232,7 @@ export default {
this.$nextTick(() => {
this.initScene();
this._ready = true;
this.fetchAndRebuild();
});
},
beforeDestroy() {
@@ -193,9 +248,7 @@ export default {
}
},
methods: {
scanText(s) { return s === 'OK' ? '正常' : (s === 'PENDING' ? '待扫码' : '异常'); },
scanShort(s) { return s === 'OK' ? '正常' : (s === 'PENDING' ? '待扫' : '异常'); },
scanClass(s) { return s === 'OK' ? 'ok' : (s === 'PENDING' ? 'pn' : 'ng'); },
fmtNum(n) { return n == null ? '-' : Number(n).toLocaleString(); },
deriveGrid() {
let maxC = 0, maxR = 0;
@@ -416,37 +469,30 @@ export default {
const tw = this.cols * CELL_W + GAP * (this.cols - 1);
const td = this.rows * CELL_D + GAP * (this.rows - 1);
const occupiedMap = {};
// 1) actualWarehouseId → 库位坐标和层号(来自 warehouseList 中的库位编码)
const slotIndex = {};
(this.warehouseList || []).forEach((w) => {
const info = parseCode(w.actualWarehouseCode);
if (!info) return;
if (w.isEnabled !== 0) return;
const c = info.column - 1, r = info.row - 1, l = info.layer;
const c = info.column - 1, r = info.row - 1;
if (c < 0 || c >= this.cols || r < 0 || r >= this.rows) return;
const key = pad2(c + 1) + pad2(r + 1);
if (!occupiedMap[key]) occupiedMap[key] = {};
occupiedMap[key][l] = w;
slotIndex[w.actualWarehouseId] = {
c, r,
key: pad2(c + 1) + pad2(r + 1),
layer: info.layer || 1,
code: w.actualWarehouseCode,
};
});
if (Object.keys(occupiedMap).length === 0) {
const total = this.cols * this.rows;
const nUsed = Math.floor(total * 0.65);
const indices = [];
for (let i = 0; i < total; i++) indices.push(i);
for (let i = total - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[indices[i], indices[j]] = [indices[j], indices[i]];
}
for (let i = 0; i < nUsed; i++) {
const idx = indices[i];
const c = Math.floor(idx / this.rows), r = idx % this.rows;
const key = pad2(c + 1) + pad2(r + 1);
occupiedMap[key] = { 1: {} };
if (Math.random() < 0.3) occupiedMap[key][2] = {};
}
}
// 2) 按库位 + 层分组真实钢卷
const occupiedMap = {};
(this.materialCoils || []).forEach((mc) => {
const slot = slotIndex[mc.actualWarehouseId];
if (!slot) return;
if (!occupiedMap[slot.key]) occupiedMap[slot.key] = {};
if (!occupiedMap[slot.key][slot.layer]) occupiedMap[slot.key][slot.layer] = mc;
});
let posSeed = 0;
for (let c = 0; c < this.cols; c++) {
for (let r = 0; r < this.rows; r++) {
const key = pad2(c + 1) + pad2(r + 1);
@@ -455,29 +501,29 @@ export default {
const cz = -td / 2 + r * (CELL_D + GAP) + CELL_D / 2;
[1, 2].forEach((layer) => {
if (!occupiedMap[key][layer]) return;
const w = occupiedMap[key][layer];
posSeed++;
// 仅极少数标记为非正常状态,避免整体闪烁
let scan = 'OK';
const rnd = (posSeed * 9301 + 49297) % 233280 / 233280;
if (rnd < 0.04) scan = 'NG';
else if (rnd < 0.12) scan = 'PENDING';
const grade = GRADE_LIST[(posSeed) % GRADE_LIST.length];
const width = 800 + ((posSeed * 37) % 550);
const od = 1100 + ((posSeed * 53) % 900);
const weight = (4 + ((posSeed * 7) % 220) / 10).toFixed(1);
const mc = occupiedMap[key][layer];
if (!mc) return;
const g = this.createCoilGroup();
const y = layer === 1 ? COIL_OD / 2 : (COIL_OD / 2 + COIL_OD + 0.08);
g.position.set(cx, y, cz);
const cid = 'C' + key + pad2(layer);
g.userData = { coilId: cid };
g.userData = { coilId: mc.coilId };
this.scene.add(g);
this.coilList.push({
id: cid, posKey: key, grade, width, od, weight, scan, mesh: g, layer,
warehouseId: w.actualWarehouseId, warehouseCode: w.actualWarehouseCode,
id: mc.coilId,
coilNo: mc.currentCoilNo || mc.enterCoilNo || ('#' + mc.coilId),
enterCoilNo: mc.enterCoilNo,
posKey: key,
layer,
specification: mc.specification || '-',
material: mc.material || '-',
grossWeight: mc.grossWeight,
netWeight: mc.netWeight,
qualityStatus: mc.qualityStatus || '-',
warehouseId: mc.actualWarehouseId,
warehouseName: mc.actualWarehouseName,
mesh: g,
moves: null,
trace: null,
});
});
this.updateSlotColor(key);
@@ -485,6 +531,26 @@ export default {
}
},
async fetchAndRebuild() {
await this.fetchMaterialCoils();
this.rebuild();
},
async fetchMaterialCoils() {
if (!this.parentId) { this.materialCoils = []; return; }
try {
const res = await listMaterialCoil({
actualWarehouseId: this.parentId,
pageNum: 1,
pageSize: 9999,
});
this.materialCoils = res.rows || res.data || [];
} catch (err) {
console.error('[Warehouse3D] 查询钢卷失败', err);
this.materialCoils = [];
}
},
updateSlotColor(key) {
const P = this.palette;
this.scene.traverse((obj) => {
@@ -542,11 +608,34 @@ export default {
this.closeDetail();
},
showCoilDetail(cid) {
async showCoilDetail(cid) {
const coil = this.coilList.find((c) => c.id === cid);
if (!coil) return;
this.detail = coil;
this.applyHighlight();
if (coil.moves && coil.trace) return;
this.detailLoading = true;
try {
const [mvRes, trRes] = await Promise.all([
getCoilWarehouseOperationLogByCoilId({ coilId: coil.id }),
getMaterialCoilTrace({ coilId: coil.id, currentCoilNo: coil.coilNo }),
]);
coil.moves = (mvRes.data || mvRes.rows || []).map((log) => ({
time: formatDate(log.createTime),
warehouse: (log.warehouse && (log.warehouse.actualWarehouseName || log.warehouse.actualWarehouseCode)) || '-',
operationType: OP_TYPE_MAP[log.operationType] || '-',
inOutType: IN_OUT_MAP[log.inOutType] || '-',
remark: log.remark || '',
}));
coil.trace = trRes.data || null;
} catch (err) {
console.error('[Warehouse3D] 查询追溯失败', err);
coil.moves = coil.moves || [];
coil.trace = coil.trace || null;
} finally {
this.detailLoading = false;
if (this.detail && this.detail.id === cid) this.detail = { ...coil };
}
},
closeDetail() {
@@ -862,7 +951,9 @@ export default {
.wh3d-dlg {
position: absolute; top: 56px; right: 20px;
width: 280px; background: #0f1923;
width: 340px; max-height: calc(100% - 80px);
overflow-y: auto;
background: #0f1923;
border: 1px solid var(--wh3d-primary); border-radius: 6px;
padding: 14px; z-index: 20;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.7);
@@ -874,12 +965,87 @@ export default {
}
.rw { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 12px; }
.k { color: #576574; }
.v { color: #c8d6e5; font-weight: 600; }
.v {
color: #c8d6e5; font-weight: 600;
&.ok { color: #2ecc71; }
&.pn { color: #e67e22; }
&.ng { color: #e74c3c; }
}
.x {
position: absolute; top: 8px; right: 10px;
background: none; border: none;
color: #576574; cursor: pointer; font-size: 18px;
&:hover { color: #c8d6e5; }
}
.sec-title {
margin: 14px 0 8px;
padding: 4px 8px;
font-size: 12px; font-weight: 600;
color: var(--wh3d-primary);
background: #0a1422;
border-left: 3px solid var(--wh3d-primary);
border-radius: 0 3px 3px 0;
}
.sec-tbl {
width: 100%; font-size: 11px;
border-collapse: collapse;
th {
background: #0d1320; color: #5dade2;
padding: 4px 6px; text-align: left; font-weight: 600;
border-bottom: 1px solid #1e3a5f;
}
td {
padding: 4px 6px;
color: #8395a7;
border-bottom: 1px solid #1a2a3a;
}
}
.trace { display: flex; flex-direction: column; gap: 8px; }
.trace-step {
border-left: 2px solid var(--wh3d-primary);
padding: 4px 8px;
background: rgba(30, 58, 95, 0.25);
border-radius: 0 3px 3px 0;
font-size: 11.5px;
.ts-head { display: flex; justify-content: space-between; margin-bottom: 3px; }
.ts-action { color: var(--wh3d-primary); font-weight: 600; }
.ts-time { color: #576574; }
.ts-op { color: #8395a7; margin-bottom: 3px; }
.ts-coils { display: flex; flex-wrap: wrap; gap: 3px; }
.ts-tag {
display: inline-block;
padding: 1px 6px;
background: #1a2a3a;
border: 1px solid #1e3a5f;
border-radius: 2px;
color: #c8d6e5;
font-size: 10.5px;
}
}
.sec-empty {
padding: 10px;
color: #576574;
font-size: 11.5px;
text-align: center;
font-style: italic;
}
}
.wh3d-light .wh3d-dlg {
.sec-title {
background: #ecf5ff;
color: var(--wh3d-primary);
}
.sec-tbl {
th { background: #fafbfc; color: #606266; border-bottom-color: #ebeef5; }
td { color: #606266; border-bottom-color: #f2f3f5; }
}
.trace-step {
background: #ecf5ff;
.ts-time { color: #909399; }
.ts-op { color: #606266; }
.ts-tag { background: #fff; border-color: #dcdfe6; color: #606266; }
}
.sec-empty { color: #909399; }
}
</style>

View File

@@ -24,7 +24,7 @@
/>
</el-tab-pane>
<el-tab-pane label="数字孪生 3D" name="three">
<Warehouse3D v-if="activeTab === 'three'" :warehouse-list="warehouseList" />
<Warehouse3D v-if="activeTab === 'three'" :warehouse-list="warehouseList" :parent-id="selectedNodeId" />
</el-tab-pane>
</el-tabs>
</div>