@@ -0,0 +1,647 @@
< template >
< div class = "wh3d-wrapper" :style = "cssVars" >
< div class = "wh3d-header" >
< h1 > 钢卷库 3 D 可视化 < / 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 : 8 px ;
overflow : hidden ;
border : 1 px solid # ebeef5 ;
}
. wh3d - header {
background : # fff ;
border - bottom : 1 px solid # ebeef5 ;
padding : 10 px 20 px ;
display : flex ;
align - items : center ;
gap : 16 px ;
h1 { color : var ( -- wh3d - primary ) ; font - size : 15 px ; font - weight : 600 ; letter - spacing : 0.5 px ; margin : 0 ; }
. tag { font - size : 11 px ; color : # 909399 ; background : # f4f4f5 ; padding : 3 px 10 px ; border - radius : 3 px ; }
. fps { margin - left : auto ; font - size : 12 px ; color : # 67 C23A ; }
}
. wh3d - main { display : flex ; flex : 1 ; overflow : hidden ; }
. wh3d - view {
position : relative ;
flex : 1 ;
background : # f5f7fa ;
overflow : hidden ;
}
. wh3d - info {
width : 310 px ;
background : # fff ;
border - left : 1 px solid # ebeef5 ;
display : flex ;
flex - direction : column ;
overflow : hidden ;
h2 {
font - size : 13 px ;
color : # 303133 ;
padding : 10 px 14 px 8 px ;
border - bottom : 1 px solid # ebeef5 ;
background : # fafbfc ;
margin : 0 ;
font - weight : 600 ;
}
. ss { padding : 10 px 14 px ; border - bottom : 1 px solid # ebeef5 ; }
. sg { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 6 px ; }
. sc {
background : # fafbfc ;
border : 1 px solid # ebeef5 ;
border - radius : 4 px ;
padding : 7 px 10 px ;
. l { font - size : 11 px ; color : # 909399 ; margin - bottom : 2 px ; }
. v {
font - size : 16 px ; font - weight : 700 ; color : # 303133 ;
& . primary { color : var ( -- wh3d - primary ) ; }
& . success { color : # 67 C23A ; }
& . warning { color : # E6A23C ; }
}
}
. ctb {
flex : 1 ; overflow - y : auto ;
table { width : 100 % ; font - size : 12 px ; border - collapse : collapse ; }
th {
position : sticky ; top : 0 ;
background : # fafbfc ; color : # 606266 ;
padding : 6 px 8 px ; text - align : left ;
font - weight : 600 ; font - size : 11 px ;
border - bottom : 1 px solid # ebeef5 ;
}
td {
padding : 6 px 8 px ;
border - bottom : 1 px solid # f2f3f5 ;
color : # 606266 ;
}
tr : hover { background : var ( -- wh3d - primary - light ) ; cursor : pointer ; }
. ok { color : # 67 C23A ; }
. pn { color : # E6A23C ; }
. ng { color : # F56C6C ; }
}
}
. wh3d - tip {
position : absolute ; bottom : 10 px ; left : 10 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
border : 1 px solid # ebeef5 ; border - radius : 4 px ;
padding : 6 px 12 px ; font - size : 11 px ; color : # 909399 ;
pointer - events : none ; z - index : 5 ;
}
. wh3d - vbt {
position : absolute ; top : 10 px ; right : 10 px ; z - index : 5 ;
}
. wh3d - dlg {
position : absolute ; top : 56 px ; right : 20 px ;
width : 280 px ; background : # fff ;
border : 1 px solid # ebeef5 ; border - radius : 6 px ;
padding : 14 px ; z - index : 20 ;
box - shadow : 0 4 px 16 px rgba ( 0 , 0 , 0 , 0.12 ) ;
h3 {
color : var ( -- wh3d - primary ) ;
font - size : 13 px ; margin : 0 0 10 px ;
padding - bottom : 6 px ;
border - bottom : 1 px solid # ebeef5 ;
font - weight : 600 ;
}
. rw { display : flex ; justify - content : space - between ; margin - bottom : 6 px ; font - size : 12 px ; }
. k { color : # 909399 ; }
. v { color : # 303133 ; font - weight : 600 ; }
. x {
position : absolute ; top : 8 px ; right : 10 px ;
background : none ; border : none ;
color : # 909399 ; cursor : pointer ; font - size : 18 px ;
& : hover { color : # 606266 ; }
}
}
< / style >