提交3d钢卷初版

This commit is contained in:
2026-06-01 17:11:36 +08:00
parent 0cfffcc60d
commit b134539e53
3 changed files with 691 additions and 12 deletions

View File

@@ -73,6 +73,7 @@
"quill": "1.3.7",
"screenfull": "5.0.2",
"sortablejs": "1.10.2",
"three": "^0.160.1",
"vditor": "^3.11.1",
"video.js": "^8.23.3",
"vue": "2.6.12",

View File

@@ -0,0 +1,647 @@
<template>
<div class="wh3d-wrapper" :style="cssVars">
<div class="wh3d-header">
<h1>钢卷库 3D 可视化</h1>
<span class="tag">地面存放 · 最大叠放 2 · 无货架</span>
<span class="fps">FPS: <b>{{ fps }}</b></span>
</div>
<div class="wh3d-main">
<div class="wh3d-view" ref="viewEl">
<div class="wh3d-vbt">
<el-button-group>
<el-button size="mini" :type="viewMode === 'iso' ? 'primary' : 'default'" @click="setView('iso')">等轴</el-button>
<el-button size="mini" :type="viewMode === 'top' ? 'primary' : 'default'" @click="setView('top')">俯视</el-button>
<el-button size="mini" :type="viewMode === 'front' ? 'primary' : 'default'" @click="setView('front')">正视</el-button>
<el-button size="mini" :type="viewMode === 'side' ? 'primary' : 'default'" @click="setView('side')">侧视</el-button>
</el-button-group>
</div>
<div class="wh3d-tip">左键旋转 · 滚轮缩放 · 右键平移 · 点击钢卷查看 · [R] 复位 · [Esc] 关闭</div>
<div class="wh3d-dlg" v-show="detail">
<button class="x" @click="detail = null">×</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>
</div>
</div>
<div class="wh3d-info">
<h2>库区统计</h2>
<div class="ss">
<div class="sg">
<div class="sc"><div class="l">总库位</div><div class="v">{{ stats.total }}</div></div>
<div class="sc"><div class="l">已占用</div><div class="v success">{{ stats.used }}</div></div>
<div class="sc"><div class="l">空闲位</div><div class="v">{{ stats.free }}</div></div>
<div class="sc"><div class="l">占用率</div><div class="v primary">{{ stats.pct }}</div></div>
<div class="sc"><div class="l">钢卷总数</div><div class="v">{{ stats.coils }}</div></div>
<div class="sc"><div class="l">双层叠放</div><div class="v warning">{{ stats.doubled }}</div></div>
</div>
</div>
<h2>钢卷明细</h2>
<div class="ctb">
<table>
<thead><tr><th>库位</th><th>钢卷号</th><th>规格</th><th>状态</th></tr></thead>
<tbody>
<tr v-for="c in tableRows" :key="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>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import * as THREE from 'three';
const GRADE_LIST = ['SPHC', 'SPHD', 'Q195', 'Q235', 'DC01', 'DC03', 'SUS304'];
const CELL_W = 3.2, CELL_D = 3.2, GAP = 0.5;
const COIL_OD = 1.6, COIL_W = 1.1;
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
function parseCode(code) {
if (!code) return null;
const reg = /^([A-Za-z0-9]{3})([^-]+)-X?(\d{2})-(\d+)$/;
const m = code.match(reg);
if (!m) return null;
return { column: Number(m[2]), row: Number(m[3]), layer: Number(m[4]) };
}
function hexToInt(hex) {
if (!hex) return 0x409eff;
return parseInt(hex.replace('#', ''), 16);
}
function lighten(hex, amt) {
const n = hexToInt(hex);
let r = (n >> 16) & 0xff, g = (n >> 8) & 0xff, b = n & 0xff;
r = Math.min(255, Math.round(r + (255 - r) * amt));
g = Math.min(255, Math.round(g + (255 - g) * amt));
b = Math.min(255, Math.round(b + (255 - b) * amt));
return (r << 16) | (g << 8) | b;
}
export default {
name: 'Warehouse3D',
props: {
warehouseList: { type: Array, default: () => [] },
},
data() {
return {
fps: '--',
viewMode: 'iso',
detail: null,
stats: { total: 0, used: 0, free: 0, pct: '0%', coils: 0, doubled: 0 },
coilList: [],
cols: 14,
rows: 6,
};
},
computed: {
themeColor() {
return (this.$store && this.$store.state.settings && this.$store.state.settings.theme) || '#409EFF';
},
cssVars() {
return {
'--wh3d-primary': this.themeColor,
'--wh3d-primary-light': '#ecf5ff',
};
},
gradeColorMap() {
const base = this.themeColor;
return {
SPHC: base,
SPHD: '#67C23A',
Q195: '#9b59b6',
Q235: '#F56C6C',
DC01: '#E6A23C',
DC03: '#e67e22',
SUS304: '#85c1e9',
};
},
tableRows() {
return this.coilList.slice().sort((a, b) => {
if (a.posKey !== b.posKey) return a.posKey.localeCompare(b.posKey);
return a.layer - b.layer;
});
},
},
watch: {
warehouseList() { if (this._ready) this.rebuild(); },
themeColor() { if (this._ready) this.rebuild(); },
},
mounted() {
this.$nextTick(() => {
this.initScene();
this._ready = true;
});
},
beforeDestroy() {
this._destroyed = true;
if (this._raf) cancelAnimationFrame(this._raf);
window.removeEventListener('resize', this._onResize);
document.removeEventListener('keydown', this._onKey);
if (this.renderer) {
this.renderer.dispose && this.renderer.dispose();
if (this.renderer.domElement && this.renderer.domElement.parentNode) {
this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
}
}
},
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'); },
deriveGrid() {
let maxC = 0, maxR = 0;
(this.warehouseList || []).forEach((w) => {
const info = parseCode(w.actualWarehouseCode);
if (info) {
if (info.column > maxC) maxC = info.column;
if (info.row > maxR) maxR = info.row;
}
});
this.cols = Math.max(maxC, 8);
this.rows = Math.max(maxR, 4);
},
initScene() {
this.deriveGrid();
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xf5f7fa);
const el = this.$refs.viewEl;
const w = Math.max(el.clientWidth - 310, 100);
const h = Math.max(el.clientHeight, 100);
this.camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 200);
this.camTarget = { x: 0, y: 1.5, z: 0 };
this.camR = 36; this.camTheta = 0.6; this.camPhi = 0.55;
this.updateCamera();
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.setSize(w, h);
el.appendChild(this.renderer.domElement);
this.scene.add(new THREE.AmbientLight(0xffffff, 0.9));
const dl = new THREE.DirectionalLight(0xffffff, 0.95);
dl.position.set(20, 30, 18);
dl.castShadow = true;
dl.shadow.mapSize.set(1024, 1024);
this.scene.add(dl);
const dl2 = new THREE.DirectionalLight(0xffffff, 0.4);
dl2.position.set(-15, 20, -10);
this.scene.add(dl2);
this.clock = new THREE.Clock();
this.raycaster = new THREE.Raycaster();
this.mouseVec = new THREE.Vector2();
this.buildGround();
this.buildAllCoils();
this.updateStats();
const dom = this.renderer.domElement;
dom.addEventListener('click', this.onCanvasClick);
dom.addEventListener('mousedown', this.onMouseDown);
dom.addEventListener('mousemove', this.onMouseMove);
dom.addEventListener('mouseup', this.onMouseUp);
dom.addEventListener('wheel', this.onMouseWheel, { passive: false });
dom.addEventListener('contextmenu', (e) => e.preventDefault());
this._onResize = () => this.resizeRenderer();
this._onKey = (e) => {
if (e.key === 'r' || e.key === 'R') this.setView('iso');
if (e.key === 'Escape') this.detail = null;
};
window.addEventListener('resize', this._onResize);
document.addEventListener('keydown', this._onKey);
this.fpsCount = 0; this.fpsTime = 0;
this.animate();
},
clearScene() {
const dispose = (obj) => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (Array.isArray(obj.material)) obj.material.forEach((m) => m.dispose());
else obj.material.dispose();
}
};
const children = this.scene.children.slice();
children.forEach((o) => {
if (o.isLight) return;
o.traverse && o.traverse(dispose);
this.scene.remove(o);
});
},
rebuild() {
if (!this.scene) return;
this.clearScene();
this.buildGround();
this.buildAllCoils();
this.updateStats();
},
resizeRenderer() {
if (!this.renderer) return;
const el = this.$refs.viewEl;
const w = Math.max(el.clientWidth - 310, 100);
const h = Math.max(el.clientHeight, 100);
this.renderer.setSize(w, h);
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
},
updateCamera() {
const c = this.camera;
c.position.x = this.camTarget.x + this.camR * Math.sin(this.camPhi) * Math.cos(this.camTheta);
c.position.y = this.camTarget.y + this.camR * Math.cos(this.camPhi);
c.position.z = this.camTarget.z + this.camR * Math.sin(this.camPhi) * Math.sin(this.camTheta);
c.lookAt(this.camTarget.x, this.camTarget.y, this.camTarget.z);
},
buildGround() {
const tw = this.cols * CELL_W + GAP * (this.cols - 1);
const td = this.rows * CELL_D + GAP * (this.rows - 1);
const gnd = new THREE.Mesh(
new THREE.PlaneGeometry(tw + 8, td + 8),
new THREE.MeshStandardMaterial({ color: 0xeef2f7, roughness: 0.95 })
);
gnd.rotation.x = -Math.PI / 2;
gnd.receiveShadow = true;
this.scene.add(gnd);
const grid = new THREE.GridHelper(
Math.max(tw, td) + 6,
Math.max(this.cols, this.rows) * 2,
hexToInt(this.themeColor),
0xdcdfe6
);
grid.material.opacity = 0.35;
grid.material.transparent = true;
grid.position.y = 0.01;
this.scene.add(grid);
const primaryLight = lighten(this.themeColor, 0.85);
for (let c = 0; c < this.cols; c++) {
for (let r = 0; r < this.rows; r++) {
const cx = -tw / 2 + c * (CELL_W + GAP) + CELL_W / 2;
const cz = -td / 2 + r * (CELL_D + GAP) + CELL_D / 2;
const key = pad2(c + 1) + pad2(r + 1);
const b = new THREE.Mesh(
new THREE.PlaneGeometry(CELL_W - 0.1, CELL_D - 0.1),
new THREE.MeshStandardMaterial({ color: primaryLight, roughness: 0.8, transparent: true, opacity: 0.45 })
);
b.rotation.x = -Math.PI / 2;
b.position.set(cx, 0.02, cz);
b.userData = { isSlot: true, key };
this.scene.add(b);
}
}
},
buildAllCoils() {
this.coilList = [];
const tw = this.cols * CELL_W + GAP * (this.cols - 1);
const td = this.rows * CELL_D + GAP * (this.rows - 1);
const occupiedMap = {};
(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;
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;
});
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] = {};
}
}
for (let c = 0; c < this.cols; c++) {
for (let r = 0; r < this.rows; r++) {
const key = pad2(c + 1) + pad2(r + 1);
if (!occupiedMap[key]) continue;
const cx = -tw / 2 + c * (CELL_W + GAP) + CELL_W / 2;
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];
const grade = GRADE_LIST[Math.floor(Math.random() * GRADE_LIST.length)];
const width = 800 + Math.floor(Math.random() * 550);
const od = 1100 + Math.floor(Math.random() * 900);
const weight = (4 + Math.random() * 22).toFixed(1);
const scans = ['OK', 'OK', 'OK', 'OK', 'PENDING', 'NG'];
const scan = scans[Math.floor(Math.random() * scans.length)];
const g = this.createCoilGroup(grade);
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 };
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,
});
});
this.updateSlotColor(key, 'OK');
}
}
},
updateSlotColor(key, scan) {
this.scene.traverse((obj) => {
if (obj.userData && obj.userData.isSlot && obj.userData.key === key) {
const c = scan === 'NG' ? 0xfde2e2 : (scan === 'PENDING' ? 0xfaecd8 : 0xe1f3d8);
obj.material.color.setHex(c);
obj.material.opacity = 0.7;
}
});
},
createCoilGroup(grade) {
const g = new THREE.Group();
const hex = this.gradeColorMap[grade] || this.themeColor;
const col = new THREE.Color(hex);
const cyl = new THREE.Mesh(
new THREE.CylinderGeometry(COIL_OD / 2, COIL_OD / 2, COIL_W, 24),
new THREE.MeshStandardMaterial({
color: col, roughness: 0.45, metalness: 0.65,
emissive: col.clone().multiplyScalar(0.04),
})
);
cyl.rotation.x = Math.PI / 2;
cyl.castShadow = true;
g.add(cyl);
const rGeo = new THREE.TorusGeometry(COIL_OD / 2, 0.06, 8, 24);
const rMat = new THREE.MeshStandardMaterial({ color: 0xe4e7ed, roughness: 0.3, metalness: 0.85 });
const rn1 = new THREE.Mesh(rGeo, rMat); rn1.position.z = COIL_W / 2 - 0.03; g.add(rn1);
const rn2 = new THREE.Mesh(rGeo, rMat); rn2.position.z = -(COIL_W / 2 - 0.03); g.add(rn2);
const hole = new THREE.Mesh(
new THREE.CylinderGeometry(0.22, 0.22, COIL_W + 0.1, 12),
new THREE.MeshStandardMaterial({ color: 0x909399 })
);
hole.rotation.x = Math.PI / 2;
g.add(hole);
return g;
},
onCanvasClick(e) {
const rect = this.renderer.domElement.getBoundingClientRect();
this.mouseVec.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
this.mouseVec.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
this.raycaster.setFromCamera(this.mouseVec, this.camera);
const targets = [];
this.scene.traverse((o) => { if (o.userData && o.userData.coilId) targets.push(o); });
const hits = this.raycaster.intersectObjects(targets, true);
if (hits.length > 0) {
let obj = hits[0].object;
while (obj.parent && !(obj.userData && obj.userData.coilId)) obj = obj.parent;
if (obj.userData && obj.userData.coilId) {
this.showCoilDetail(obj.userData.coilId);
return;
}
}
this.detail = null;
},
showCoilDetail(cid) {
const coil = this.coilList.find((c) => c.id === cid);
if (!coil) return;
this.detail = coil;
this.camTarget.x = coil.mesh.position.x;
this.camTarget.y = coil.mesh.position.y;
this.camTarget.z = coil.mesh.position.z;
this.updateCamera();
},
onMouseDown(e) {
e.preventDefault();
if (e.button === 0) this.isRotating = true;
if (e.button === 2) this.isPanning = true;
this.prevMX = e.clientX; this.prevMY = e.clientY;
},
onMouseUp() { this.isRotating = false; this.isPanning = false; },
onMouseMove(e) {
if (this.isRotating) {
const dx = e.clientX - this.prevMX, dy = e.clientY - this.prevMY;
this.camTheta -= dx * 0.005;
this.camPhi = Math.max(0.15, Math.min(1.45, this.camPhi + dy * 0.005));
this.updateCamera();
}
if (this.isPanning) {
const dx = e.clientX - this.prevMX, dy = e.clientY - this.prevMY;
this.camTarget.x += (-dx * Math.cos(this.camTheta) + dy * Math.sin(this.camTheta)) * 0.04;
this.camTarget.z += (-dx * Math.sin(this.camTheta) - dy * Math.cos(this.camTheta)) * 0.04;
this.updateCamera();
}
this.prevMX = e.clientX; this.prevMY = e.clientY;
},
onMouseWheel(e) {
this.camR = Math.max(8, Math.min(80, this.camR + e.deltaY * 0.03));
this.updateCamera();
e.preventDefault();
},
setView(type) {
this.viewMode = type;
if (type === 'iso') { this.camR = 36; this.camTheta = 0.6; this.camPhi = 0.55; this.camTarget = { x: 0, y: 1.5, z: 0 }; }
if (type === 'top') { this.camR = 34; this.camTheta = 0; this.camPhi = 0.01; this.camTarget = { x: 0, y: 0, z: 0 }; }
if (type === 'front') { this.camR = 32; this.camTheta = 0; this.camPhi = 1.5; this.camTarget = { x: 0, y: 2, z: 0 }; }
if (type === 'side') { this.camR = 32; this.camTheta = Math.PI / 2; this.camPhi = 1.5; this.camTarget = { x: 0, y: 2, z: 0 }; }
this.updateCamera();
},
updateStats() {
const total = this.cols * this.rows;
const usedKeys = {};
let doubled = 0;
this.coilList.forEach((c) => {
usedKeys[c.posKey] = true;
if (c.layer === 2) doubled++;
});
const used = Object.keys(usedKeys).length;
this.stats = {
total, used, free: total - used,
pct: total > 0 ? Math.round(used / total * 100) + '%' : '0%',
coils: this.coilList.length, doubled,
};
},
animate() {
if (this._destroyed) return;
this._raf = requestAnimationFrame(this.animate);
const t = Date.now() * 0.001;
this.coilList.forEach((coil, i) => {
if (!coil.mesh) return;
if (coil.scan === 'PENDING') this.setEmissive(coil.mesh, 0xE6A23C, 0.05 + 0.25 * Math.abs(Math.sin(t * 4 + i)));
else if (coil.scan === 'NG') this.setEmissive(coil.mesh, 0xF56C6C, 0.05 + 0.2 * Math.abs(Math.sin(t * 2)));
else if (this.detail && coil.id === this.detail.id) this.setEmissive(coil.mesh, hexToInt(this.themeColor), 0.4);
});
this.fpsCount++;
this.fpsTime += this.clock.getDelta();
if (this.fpsTime >= 1) { this.fps = this.fpsCount; this.fpsCount = 0; this.fpsTime = 0; }
this.renderer.render(this.scene, this.camera);
},
setEmissive(group, hex, intensity) {
group.traverse((ch) => {
if (ch.material && ch.material.emissive) {
ch.material.emissive.setHex(hex);
ch.material.emissiveIntensity = intensity;
}
});
},
},
};
</script>
<style scoped lang="scss">
.wh3d-wrapper {
background: #fff;
color: #303133;
font-family: 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
height: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
border: 1px solid #ebeef5;
}
.wh3d-header {
background: #fff;
border-bottom: 1px solid #ebeef5;
padding: 10px 20px;
display: flex;
align-items: center;
gap: 16px;
h1 { color: var(--wh3d-primary); font-size: 15px; font-weight: 600; letter-spacing: 0.5px; margin: 0; }
.tag { font-size: 11px; color: #909399; background: #f4f4f5; padding: 3px 10px; border-radius: 3px; }
.fps { margin-left: auto; font-size: 12px; color: #67C23A; }
}
.wh3d-main { display: flex; flex: 1; overflow: hidden; }
.wh3d-view {
position: relative;
flex: 1;
background: #f5f7fa;
overflow: hidden;
}
.wh3d-info {
width: 310px;
background: #fff;
border-left: 1px solid #ebeef5;
display: flex;
flex-direction: column;
overflow: hidden;
h2 {
font-size: 13px;
color: #303133;
padding: 10px 14px 8px;
border-bottom: 1px solid #ebeef5;
background: #fafbfc;
margin: 0;
font-weight: 600;
}
.ss { padding: 10px 14px; border-bottom: 1px solid #ebeef5; }
.sg { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.sc {
background: #fafbfc;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 7px 10px;
.l { font-size: 11px; color: #909399; margin-bottom: 2px; }
.v {
font-size: 16px; font-weight: 700; color: #303133;
&.primary { color: var(--wh3d-primary); }
&.success { color: #67C23A; }
&.warning { color: #E6A23C; }
}
}
.ctb {
flex: 1; overflow-y: auto;
table { width: 100%; font-size: 12px; border-collapse: collapse; }
th {
position: sticky; top: 0;
background: #fafbfc; color: #606266;
padding: 6px 8px; text-align: left;
font-weight: 600; font-size: 11px;
border-bottom: 1px solid #ebeef5;
}
td {
padding: 6px 8px;
border-bottom: 1px solid #f2f3f5;
color: #606266;
}
tr:hover { background: var(--wh3d-primary-light); cursor: pointer; }
.ok { color: #67C23A; }
.pn { color: #E6A23C; }
.ng { color: #F56C6C; }
}
}
.wh3d-tip {
position: absolute; bottom: 10px; left: 10px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid #ebeef5; border-radius: 4px;
padding: 6px 12px; font-size: 11px; color: #909399;
pointer-events: none; z-index: 5;
}
.wh3d-vbt {
position: absolute; top: 10px; right: 10px; z-index: 5;
}
.wh3d-dlg {
position: absolute; top: 56px; right: 20px;
width: 280px; background: #fff;
border: 1px solid #ebeef5; border-radius: 6px;
padding: 14px; z-index: 20;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
h3 {
color: var(--wh3d-primary);
font-size: 13px; margin: 0 0 10px;
padding-bottom: 6px;
border-bottom: 1px solid #ebeef5;
font-weight: 600;
}
.rw { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 12px; }
.k { color: #909399; }
.v { color: #303133; font-weight: 600; }
.x {
position: absolute; top: 8px; right: 10px;
background: none; border: none;
color: #909399; cursor: pointer; font-size: 18px;
&:hover { color: #606266; }
}
}
</style>

View File

@@ -10,18 +10,23 @@
</el-tree>
</div>
<!-- 右侧仓库信息区域 - 替换为 Bird 组件 -->
<!-- 右侧仓库信息区域 - 2D / 3D 双视图 tab -->
<div class="warehouse-container" v-if="selectedNodeId" v-loading="rightLoading" element-loading-text="加载中..."
element-loading-spinner="el-icon-loading">
<!-- 导出所有二维码 -->
<!-- <button buttonLoading type="primary" @click="exportAllQrcodes">导出二维码</button> -->
<WarehouseBird
:id="selectedNodeId"
:warehouse-list="warehouseList"
@open-init-dialog="openInitDialog"
@split-warehouse="handleSplitWarehouse"
@merge-warehouse="handleMergeWarehouse"
/>
<el-tabs v-model="activeTab" class="overview-tabs">
<el-tab-pane label="平面视图" name="bird">
<WarehouseBird
:id="selectedNodeId"
:warehouse-list="warehouseList"
@open-init-dialog="openInitDialog"
@split-warehouse="handleSplitWarehouse"
@merge-warehouse="handleMergeWarehouse"
/>
</el-tab-pane>
<el-tab-pane label="数字孪生 3D" name="three">
<Warehouse3D v-if="activeTab === 'three'" :warehouse-list="warehouseList" />
</el-tab-pane>
</el-tabs>
</div>
<!-- 未选中节点提示 -->
@@ -66,12 +71,13 @@
<script>
import { listActualWarehouse, treeActualWarehouseTwoLevel, getActualWarehouse, generateLocations, splitActualWarehouse, mergeActualWarehouse } from "@/api/wms/actualWarehouse";
import WarehouseBird from './components/WarehouseBird.vue';
import Warehouse3D from './components/Warehouse3D.vue';
import jsPDF from 'jspdf';
import QRCode from 'qrcode';
export default {
name: "Overview",
components: { WarehouseBird },
components: { WarehouseBird, Warehouse3D },
data() {
// 自定义验证规则:正整数
const positiveIntegerValidator = (rule, value, callback) => {
@@ -99,6 +105,7 @@ export default {
selectedNodeId: "",
selectedNode: null,
warehouseList: [], // 透传给 Bird 组件的原始数据
activeTab: 'bird',
// 初始化弹窗相关
initDialogVisible: false,
initForm: {
@@ -457,7 +464,31 @@ export default {
.warehouse-container {
flex: 1;
height: 100%;
overflow-y: auto;
overflow: hidden;
display: flex;
flex-direction: column;
::v-deep .overview-tabs {
height: 100%;
display: flex;
flex-direction: column;
.el-tabs__header { margin: 0 0 8px; }
.el-tabs__content {
flex: 1;
overflow: hidden;
}
.el-tab-pane {
height: 100%;
overflow: auto;
}
.el-tab-pane:has(> .wh3d-wrapper) {
overflow: hidden;
}
}
}
.empty-select-tip {