提交3d钢卷
This commit is contained in:
@@ -1,25 +1,37 @@
|
||||
<template>
|
||||
<div class="wh3d-wrapper" :style="cssVars">
|
||||
<div class="wh3d-wrapper" :class="'wh3d-' + themeMode" :style="cssVars">
|
||||
<div class="wh3d-header">
|
||||
<h1>钢卷库 3D 可视化</h1>
|
||||
<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>
|
||||
<button v-for="v in viewBtns" :key="v.k" :class="{ act: viewMode === v.k }" @click="setView(v.k)">{{ v.l }}</button>
|
||||
<button class="theme-toggle" @click="toggleTheme" :title="'切换为' + (themeMode === 'dark' ? '浅色' : '深色') + '主题'">
|
||||
{{ themeMode === 'dark' ? '浅色' : '深色' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="wh3d-tip">左键旋转 · 滚轮缩放 · 右键平移 · 点击钢卷查看 · [R] 复位 · [Esc] 关闭</div>
|
||||
|
||||
<div class="wh3d-help">
|
||||
<div class="hh">操作说明</div>
|
||||
<div class="hr"><kbd>左键</kbd> 旋转视角</div>
|
||||
<div class="hr"><kbd>右键</kbd> 平移视角</div>
|
||||
<div class="hr"><kbd>滚轮</kbd> 缩放</div>
|
||||
<div class="hr"><kbd>点击</kbd> 查看钢卷详情</div>
|
||||
<div class="hr"><kbd>R</kbd> 复位视角 · <kbd>Esc</kbd> 关闭</div>
|
||||
</div>
|
||||
|
||||
<div class="wh3d-axis">
|
||||
<span>X 轴 · 列 (C)</span>
|
||||
<span class="vt">Z 轴 · 排 (R)</span>
|
||||
</div>
|
||||
|
||||
<div class="wh3d-dlg" v-show="detail">
|
||||
<button class="x" @click="detail = null">×</button>
|
||||
<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.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>
|
||||
@@ -27,16 +39,17 @@
|
||||
<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.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 class="sc"><div class="l">双层叠放</div><div class="v">{{ stats.doubled }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>钢卷明细</h2>
|
||||
@@ -44,7 +57,9 @@
|
||||
<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)">
|
||||
<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>
|
||||
@@ -80,15 +95,6 @@ function hexToInt(hex) {
|
||||
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: {
|
||||
@@ -103,29 +109,54 @@ export default {
|
||||
coilList: [],
|
||||
cols: 14,
|
||||
rows: 6,
|
||||
themeMode: localStorage.getItem('wh3d-theme') || 'dark',
|
||||
viewBtns: [
|
||||
{ k: 'iso', l: '等轴' },
|
||||
{ k: 'top', l: '俯视' },
|
||||
{ k: 'front', l: '正视' },
|
||||
{ k: 'side', l: '侧视' },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
themeColor() {
|
||||
return (this.$store && this.$store.state.settings && this.$store.state.settings.theme) || '#409EFF';
|
||||
},
|
||||
cssVars() {
|
||||
palette() {
|
||||
// 整套色板根据 themeMode 切换;3D 场景与 DOM 都用同一份
|
||||
if (this.themeMode === 'light') {
|
||||
return {
|
||||
sceneBg: 0xf5f7fa,
|
||||
ground: 0xe4eaf2,
|
||||
gridMajor: 0x9aa5b1,
|
||||
gridMinor: 0xdcdfe6,
|
||||
slot: 0xeef2f7,
|
||||
slotOpacity: 0.55,
|
||||
coilBody: 0x303133,
|
||||
coilEnd: 0x9aa5b1,
|
||||
coilHole: 0x1a1a1a,
|
||||
ambient: 0.95, dirMain: 1.0, dirFill: 0.4, hemiSky: 0xffffff, hemiGround: 0xcccccc,
|
||||
highlightEmissive: 0xf39c12,
|
||||
highlightColor: hexToInt(this.themeColor),
|
||||
};
|
||||
}
|
||||
return {
|
||||
'--wh3d-primary': this.themeColor,
|
||||
'--wh3d-primary-light': '#ecf5ff',
|
||||
sceneBg: 0x080c14,
|
||||
ground: 0x111a26,
|
||||
gridMajor: 0x1e3a5f,
|
||||
gridMinor: 0x0f1923,
|
||||
slot: 0x1a3a1a,
|
||||
slotOpacity: 0.55,
|
||||
coilBody: 0x1a1a1a,
|
||||
coilEnd: 0x9aa5b1,
|
||||
coilHole: 0x000000,
|
||||
ambient: 0.6, dirMain: 1.1, dirFill: 0.35, hemiSky: 0x445566, hemiGround: 0x111820,
|
||||
highlightEmissive: 0xf39c12,
|
||||
highlightColor: hexToInt(this.themeColor),
|
||||
};
|
||||
},
|
||||
gradeColorMap() {
|
||||
const base = this.themeColor;
|
||||
return {
|
||||
SPHC: base,
|
||||
SPHD: '#67C23A',
|
||||
Q195: '#9b59b6',
|
||||
Q235: '#F56C6C',
|
||||
DC01: '#E6A23C',
|
||||
DC03: '#e67e22',
|
||||
SUS304: '#85c1e9',
|
||||
};
|
||||
cssVars() {
|
||||
return { '--wh3d-primary': this.themeColor };
|
||||
},
|
||||
tableRows() {
|
||||
return this.coilList.slice().sort((a, b) => {
|
||||
@@ -137,6 +168,11 @@ export default {
|
||||
watch: {
|
||||
warehouseList() { if (this._ready) this.rebuild(); },
|
||||
themeColor() { if (this._ready) this.rebuild(); },
|
||||
themeMode() {
|
||||
if (!this._ready) return;
|
||||
this.scene.background = new THREE.Color(this.palette.sceneBg);
|
||||
this.rebuild();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
@@ -157,7 +193,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
scanText(s) { return s === 'OK' ? '✓ 正常' : (s === 'PENDING' ? '⏳ 待扫码' : '✕ 异常'); },
|
||||
scanText(s) { return s === 'OK' ? '正常' : (s === 'PENDING' ? '待扫码' : '异常'); },
|
||||
scanShort(s) { return s === 'OK' ? '正常' : (s === 'PENDING' ? '待扫' : '异常'); },
|
||||
scanClass(s) { return s === 'OK' ? 'ok' : (s === 'PENDING' ? 'pn' : 'ng'); },
|
||||
|
||||
@@ -177,7 +213,7 @@ export default {
|
||||
initScene() {
|
||||
this.deriveGrid();
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0xf5f7fa);
|
||||
this.scene.background = new THREE.Color(this.palette.sceneBg);
|
||||
|
||||
const el = this.$refs.viewEl;
|
||||
const w = Math.max(el.clientWidth - 310, 100);
|
||||
@@ -195,15 +231,17 @@ export default {
|
||||
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);
|
||||
const P = this.palette;
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, P.ambient));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, P.dirMain);
|
||||
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);
|
||||
const fill = new THREE.DirectionalLight(0xa0c0ff, P.dirFill);
|
||||
fill.position.set(-15, 20, -10);
|
||||
this.scene.add(fill);
|
||||
this.scene.add(new THREE.HemisphereLight(P.hemiSky, P.hemiGround, 0.5));
|
||||
|
||||
this.clock = new THREE.Clock();
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
@@ -223,7 +261,7 @@ export default {
|
||||
this._onResize = () => this.resizeRenderer();
|
||||
this._onKey = (e) => {
|
||||
if (e.key === 'r' || e.key === 'R') this.setView('iso');
|
||||
if (e.key === 'Escape') this.detail = null;
|
||||
if (e.key === 'Escape') this.closeDetail();
|
||||
};
|
||||
window.addEventListener('resize', this._onResize);
|
||||
document.addEventListener('keydown', this._onKey);
|
||||
@@ -250,12 +288,32 @@ export default {
|
||||
|
||||
rebuild() {
|
||||
if (!this.scene) return;
|
||||
this.detail = null;
|
||||
this.clearScene();
|
||||
// 重新加灯(clearScene 保留灯,但主题变化时需要刷新强度,所以也清掉并重建)
|
||||
const lights = this.scene.children.filter((o) => o.isLight);
|
||||
lights.forEach((l) => this.scene.remove(l));
|
||||
const P = this.palette;
|
||||
this.scene.background = new THREE.Color(P.sceneBg);
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, P.ambient));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, P.dirMain);
|
||||
dl.position.set(20, 30, 18);
|
||||
dl.castShadow = true;
|
||||
this.scene.add(dl);
|
||||
const fill = new THREE.DirectionalLight(0xa0c0ff, P.dirFill);
|
||||
fill.position.set(-15, 20, -10);
|
||||
this.scene.add(fill);
|
||||
this.scene.add(new THREE.HemisphereLight(P.hemiSky, P.hemiGround, 0.5));
|
||||
this.buildGround();
|
||||
this.buildAllCoils();
|
||||
this.updateStats();
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
this.themeMode = this.themeMode === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem('wh3d-theme', this.themeMode);
|
||||
},
|
||||
|
||||
resizeRenderer() {
|
||||
if (!this.renderer) return;
|
||||
const el = this.$refs.viewEl;
|
||||
@@ -274,13 +332,33 @@ export default {
|
||||
c.lookAt(this.camTarget.x, this.camTarget.y, this.camTarget.z);
|
||||
},
|
||||
|
||||
createTextSprite(text, size, color) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const px = Math.floor(size * 256);
|
||||
canvas.width = px * 3;
|
||||
canvas.height = px;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = color || '#5dade2';
|
||||
ctx.font = 'bold ' + Math.floor(size * 64) + 'px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
tex.minFilter = THREE.LinearFilter;
|
||||
const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
|
||||
const sp = new THREE.Sprite(mat);
|
||||
sp.renderOrder = 10;
|
||||
return sp;
|
||||
},
|
||||
|
||||
buildGround() {
|
||||
const tw = this.cols * CELL_W + GAP * (this.cols - 1);
|
||||
const td = this.rows * CELL_D + GAP * (this.rows - 1);
|
||||
|
||||
const P = this.palette;
|
||||
const gnd = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(tw + 8, td + 8),
|
||||
new THREE.MeshStandardMaterial({ color: 0xeef2f7, roughness: 0.95 })
|
||||
new THREE.MeshStandardMaterial({ color: P.ground, roughness: 0.95 })
|
||||
);
|
||||
gnd.rotation.x = -Math.PI / 2;
|
||||
gnd.receiveShadow = true;
|
||||
@@ -289,15 +367,12 @@ export default {
|
||||
const grid = new THREE.GridHelper(
|
||||
Math.max(tw, td) + 6,
|
||||
Math.max(this.cols, this.rows) * 2,
|
||||
hexToInt(this.themeColor),
|
||||
0xdcdfe6
|
||||
P.gridMajor,
|
||||
P.gridMinor
|
||||
);
|
||||
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;
|
||||
@@ -305,7 +380,7 @@ export default {
|
||||
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 })
|
||||
new THREE.MeshStandardMaterial({ color: P.slot, roughness: 0.8, transparent: true, opacity: 0.4 })
|
||||
);
|
||||
b.rotation.x = -Math.PI / 2;
|
||||
b.position.set(cx, 0.02, cz);
|
||||
@@ -313,6 +388,27 @@ export default {
|
||||
this.scene.add(b);
|
||||
}
|
||||
}
|
||||
|
||||
const labelColor = this.themeMode === 'light' ? '#3A71A8' : '#5dade2';
|
||||
const subColor = this.themeMode === 'light' ? '#909399' : '#7f8c8d';
|
||||
for (let c = 0; c < this.cols; c++) {
|
||||
const cx = -tw / 2 + c * (CELL_W + GAP) + CELL_W / 2;
|
||||
const sp = this.createTextSprite('C' + pad2(c + 1), 0.5, labelColor);
|
||||
sp.position.set(cx, 0.1, -td / 2 - 2.2);
|
||||
sp.scale.set(2.6, 0.9, 1);
|
||||
this.scene.add(sp);
|
||||
}
|
||||
for (let r = 0; r < this.rows; r++) {
|
||||
const cz = -td / 2 + r * (CELL_D + GAP) + CELL_D / 2;
|
||||
const sp = this.createTextSprite('R' + (r + 1), 0.5, labelColor);
|
||||
sp.position.set(-tw / 2 - 2.5, 0.1, cz);
|
||||
sp.scale.set(2.2, 0.9, 1);
|
||||
this.scene.add(sp);
|
||||
}
|
||||
const origin = this.createTextSprite('原点 (0,0)', 0.42, subColor);
|
||||
origin.position.set(-tw / 2 - 2.5, 0.1, -td / 2 - 2.2);
|
||||
origin.scale.set(3, 0.8, 1);
|
||||
this.scene.add(origin);
|
||||
},
|
||||
|
||||
buildAllCoils() {
|
||||
@@ -350,6 +446,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -360,13 +457,19 @@ export default {
|
||||
[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);
|
||||
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 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);
|
||||
@@ -377,42 +480,42 @@ export default {
|
||||
warehouseId: w.actualWarehouseId, warehouseCode: w.actualWarehouseCode,
|
||||
});
|
||||
});
|
||||
this.updateSlotColor(key, 'OK');
|
||||
this.updateSlotColor(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateSlotColor(key, scan) {
|
||||
updateSlotColor(key) {
|
||||
const P = this.palette;
|
||||
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;
|
||||
obj.material.color.setHex(P.slot);
|
||||
obj.material.opacity = P.slotOpacity;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createCoilGroup(grade) {
|
||||
createCoilGroup() {
|
||||
const P = this.palette;
|
||||
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.CylinderGeometry(COIL_OD / 2, COIL_OD / 2, COIL_W, 28),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: col, roughness: 0.45, metalness: 0.65,
|
||||
emissive: col.clone().multiplyScalar(0.04),
|
||||
color: P.coilBody, roughness: 0.55, metalness: 0.85,
|
||||
emissive: 0x000000, emissiveIntensity: 1,
|
||||
})
|
||||
);
|
||||
cyl.rotation.x = Math.PI / 2;
|
||||
cyl.castShadow = true;
|
||||
cyl.userData.isCoilBody = 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 rMat = new THREE.MeshStandardMaterial({ color: P.coilEnd, roughness: 0.3, metalness: 0.95 });
|
||||
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 })
|
||||
new THREE.MeshStandardMaterial({ color: P.coilHole })
|
||||
);
|
||||
hole.rotation.x = Math.PI / 2;
|
||||
g.add(hole);
|
||||
@@ -420,6 +523,7 @@ export default {
|
||||
},
|
||||
|
||||
onCanvasClick(e) {
|
||||
if (this._didDrag) { this._didDrag = false; return; }
|
||||
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;
|
||||
@@ -435,27 +539,62 @@ export default {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.detail = null;
|
||||
this.closeDetail();
|
||||
},
|
||||
|
||||
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();
|
||||
this.applyHighlight();
|
||||
},
|
||||
|
||||
closeDetail() {
|
||||
this.detail = null;
|
||||
this.applyHighlight();
|
||||
},
|
||||
|
||||
applyHighlight() {
|
||||
const P = this.palette;
|
||||
this.coilList.forEach((c) => {
|
||||
if (!c.mesh) return;
|
||||
c.mesh.traverse((ch) => {
|
||||
if (ch.userData && ch.userData.isCoilBody) {
|
||||
ch.material.color.setHex(P.coilBody);
|
||||
ch.material.emissive.setHex(0x000000);
|
||||
ch.material.emissiveIntensity = 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (this.detail) {
|
||||
const c = this.coilList.find((x) => x.id === this.detail.id);
|
||||
if (c && c.mesh) {
|
||||
c.mesh.traverse((ch) => {
|
||||
if (ch.userData && ch.userData.isCoilBody) {
|
||||
ch.material.color.setHex(P.highlightColor);
|
||||
ch.material.emissive.setHex(P.highlightEmissive);
|
||||
ch.material.emissiveIntensity = 0.9;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onMouseDown(e) {
|
||||
e.preventDefault();
|
||||
this._didDrag = false;
|
||||
this._dragStartX = e.clientX; this._dragStartY = e.clientY;
|
||||
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 || this.isPanning) {
|
||||
if (Math.abs(e.clientX - this._dragStartX) + Math.abs(e.clientY - this._dragStartY) > 4) {
|
||||
this._didDrag = true;
|
||||
}
|
||||
}
|
||||
if (this.isRotating) {
|
||||
const dx = e.clientX - this.prevMX, dy = e.clientY - this.prevMY;
|
||||
this.camTheta -= dx * 0.005;
|
||||
@@ -505,143 +644,242 @@ export default {
|
||||
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);
|
||||
});
|
||||
|
||||
// 仅给选中钢卷一个柔和呼吸高亮,其它一律保持静态
|
||||
if (this.detail) {
|
||||
const c = this.coilList.find((x) => x.id === this.detail.id);
|
||||
if (c && c.mesh) {
|
||||
const pulse = 0.6 + 0.4 * Math.abs(Math.sin(t * 2.5));
|
||||
c.mesh.traverse((ch) => {
|
||||
if (ch.userData && ch.userData.isCoilBody) ch.material.emissiveIntensity = pulse;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
background: #0a0e17;
|
||||
color: #c8d6e5;
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ebeef5;
|
||||
border: 1px solid #1e3a5f;
|
||||
}
|
||||
.wh3d-header {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
background: linear-gradient(90deg, #0d1320, #111827, #0d1320);
|
||||
border-bottom: 1px solid #1e3a5f;
|
||||
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; }
|
||||
h1 { color: #5dade2; font-size: 15px; font-weight: 600; letter-spacing: 1px; margin: 0; }
|
||||
.tag { font-size: 10px; color: #7f8c8d; background: #1a2233; padding: 3px 8px; border-radius: 3px; }
|
||||
.fps { margin-left: auto; font-size: 12px; color: #2ecc71; }
|
||||
}
|
||||
.wh3d-main { display: flex; flex: 1; overflow: hidden; }
|
||||
.wh3d-view {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
background: #f5f7fa;
|
||||
background: #080c14;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wh3d-info {
|
||||
width: 310px;
|
||||
background: #fff;
|
||||
border-left: 1px solid #ebeef5;
|
||||
background: #0f1923;
|
||||
border-left: 1px solid #1e3a5f;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
font-size: 13px; color: #5dade2;
|
||||
padding: 10px 14px 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
background: #fafbfc;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #1a2a3a;
|
||||
background: #0d1320; margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ss { padding: 10px 14px; border-bottom: 1px solid #ebeef5; }
|
||||
.ss { padding: 10px 14px; border-bottom: 1px solid #1a2a3a; }
|
||||
.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; }
|
||||
background: #151f2e; border: 1px solid #1e3a5f; border-radius: 4px; padding: 7px 10px;
|
||||
.l { font-size: 10px; color: #576574; margin-bottom: 2px; }
|
||||
.v {
|
||||
font-size: 16px; font-weight: 700; color: #303133;
|
||||
font-size: 15px; font-weight: 700; color: #2ecc71;
|
||||
&.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; }
|
||||
table { width: 100%; font-size: 11px; 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;
|
||||
background: #0d1320; color: #5dade2;
|
||||
padding: 5px 6px; text-align: left;
|
||||
font-weight: 600; font-size: 10px;
|
||||
border-bottom: 1px solid #1e3a5f;
|
||||
}
|
||||
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; }
|
||||
td { padding: 5px 6px; border-bottom: 1px solid #0f1923; color: #8395a7; }
|
||||
tr { cursor: pointer; }
|
||||
tr:hover { background: #151f2e; }
|
||||
tr.sel { background: #1a3a5f; td { color: #5dade2; } }
|
||||
.ok { color: #2ecc71; }
|
||||
.pn { color: #e67e22; }
|
||||
.ng { color: #e74c3c; }
|
||||
}
|
||||
}
|
||||
.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;
|
||||
display: flex; gap: 4px;
|
||||
button {
|
||||
background: #1a2a3a; border: 1px solid #2a4a6f; color: #8395a7;
|
||||
padding: 5px 12px; font-size: 11px; border-radius: 3px; cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
&:hover { background: #1a3a5f; color: #5dade2; border-color: #5dade2; }
|
||||
&.act { background: #1a3a5f; color: #5dade2; border-color: #5dade2; font-weight: 600; }
|
||||
&.theme-toggle {
|
||||
margin-left: 8px;
|
||||
border-color: var(--wh3d-primary);
|
||||
color: var(--wh3d-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ====== 浅色主题覆盖 ====== */
|
||||
.wh3d-light {
|
||||
background: #fff !important;
|
||||
color: #303133 !important;
|
||||
border-color: #ebeef5 !important;
|
||||
.wh3d-header {
|
||||
background: #fafbfc;
|
||||
border-bottom-color: #ebeef5;
|
||||
h1 { color: var(--wh3d-primary); }
|
||||
.tag { color: #909399; background: #f4f4f5; }
|
||||
.fps { color: #67C23A; }
|
||||
}
|
||||
.wh3d-view { background: #f5f7fa; }
|
||||
.wh3d-info {
|
||||
background: #fff;
|
||||
border-left-color: #ebeef5;
|
||||
h2 { color: #303133; background: #fafbfc; border-bottom-color: #ebeef5; }
|
||||
.ss { border-bottom-color: #ebeef5; }
|
||||
.sc {
|
||||
background: #fafbfc; border-color: #ebeef5;
|
||||
.l { color: #909399; }
|
||||
.v { color: #67C23A; &.primary { color: var(--wh3d-primary); } }
|
||||
}
|
||||
.ctb {
|
||||
th { background: #fafbfc; color: #606266; border-bottom-color: #ebeef5; }
|
||||
td { color: #606266; border-bottom-color: #f2f3f5; }
|
||||
tr:hover { background: #ecf5ff; }
|
||||
tr.sel { background: var(--wh3d-primary); td { color: #fff; } }
|
||||
}
|
||||
}
|
||||
.wh3d-vbt button {
|
||||
background: #fff; border-color: #dcdfe6; color: #606266;
|
||||
&:hover { background: #ecf5ff; color: var(--wh3d-primary); border-color: var(--wh3d-primary); }
|
||||
&.act { background: var(--wh3d-primary); color: #fff; border-color: var(--wh3d-primary); }
|
||||
&.theme-toggle { border-color: var(--wh3d-primary); color: var(--wh3d-primary); background: #fff; }
|
||||
}
|
||||
.wh3d-help {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-color: #ebeef5;
|
||||
color: #606266;
|
||||
.hh { color: var(--wh3d-primary); border-bottom-color: #ebeef5; }
|
||||
.hr kbd {
|
||||
background: #f4f4f5; border-color: #dcdfe6;
|
||||
color: var(--wh3d-primary);
|
||||
}
|
||||
}
|
||||
.wh3d-axis {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-color: #ebeef5;
|
||||
color: var(--wh3d-primary);
|
||||
.vt { color: #E6A23C; }
|
||||
}
|
||||
.wh3d-dlg {
|
||||
background: #fff; border-color: var(--wh3d-primary);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
h3 { color: var(--wh3d-primary); border-bottom-color: #ebeef5; }
|
||||
.k { color: #909399; }
|
||||
.v { color: #303133; }
|
||||
.x { color: #909399; &:hover { color: #303133; } }
|
||||
}
|
||||
}
|
||||
|
||||
.wh3d-help {
|
||||
position: absolute; top: 10px; left: 10px;
|
||||
background: rgba(15, 25, 35, 0.92);
|
||||
border: 1px solid #1e3a5f; border-radius: 6px;
|
||||
padding: 10px 14px; z-index: 5;
|
||||
font-size: 11px; color: #8395a7;
|
||||
min-width: 180px;
|
||||
.hh {
|
||||
color: #5dade2; font-weight: 600; font-size: 12px;
|
||||
margin-bottom: 8px; padding-bottom: 6px;
|
||||
border-bottom: 1px solid #1e3a5f;
|
||||
}
|
||||
.hr {
|
||||
line-height: 1.9;
|
||||
kbd {
|
||||
display: inline-block;
|
||||
background: #1a2a3a;
|
||||
border: 1px solid #2a4a6f;
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 3px;
|
||||
padding: 1px 6px;
|
||||
margin-right: 6px;
|
||||
color: #5dade2;
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
min-width: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wh3d-axis {
|
||||
position: absolute; bottom: 10px; left: 10px;
|
||||
background: rgba(15, 25, 35, 0.85);
|
||||
border: 1px solid #1e3a5f; border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
z-index: 5;
|
||||
font-size: 11px; color: #5dade2;
|
||||
display: flex; gap: 14px;
|
||||
.vt { color: #f39c12; }
|
||||
}
|
||||
|
||||
.wh3d-dlg {
|
||||
position: absolute; top: 56px; right: 20px;
|
||||
width: 280px; background: #fff;
|
||||
border: 1px solid #ebeef5; border-radius: 6px;
|
||||
width: 280px; background: #0f1923;
|
||||
border: 1px solid var(--wh3d-primary); border-radius: 6px;
|
||||
padding: 14px; z-index: 20;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.7);
|
||||
h3 {
|
||||
color: var(--wh3d-primary);
|
||||
font-size: 13px; margin: 0 0 10px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #1e3a5f;
|
||||
}
|
||||
.rw { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 12px; }
|
||||
.k { color: #909399; }
|
||||
.v { color: #303133; font-weight: 600; }
|
||||
.k { color: #576574; }
|
||||
.v { color: #c8d6e5; font-weight: 600; }
|
||||
.x {
|
||||
position: absolute; top: 8px; right: 10px;
|
||||
background: none; border: none;
|
||||
color: #909399; cursor: pointer; font-size: 18px;
|
||||
&:hover { color: #606266; }
|
||||
color: #576574; cursor: pointer; font-size: 18px;
|
||||
&:hover { color: #c8d6e5; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user