提交3d钢卷,追溯和意库内容
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user