From 8482760e3db0581ee34d79424e73fed69e7948d9 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期一, 30 三月 2026 18:10:20 +0800
Subject: [PATCH] feat(stockChat): 库存3D查看器完整实现

---
 Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue |  328 ++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 257 insertions(+), 71 deletions(-)

diff --git a/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
index fdb35a2..a463460 100644
--- a/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
+++ b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
@@ -12,18 +12,22 @@
 
     <!-- 宸ュ叿鏍� -->
     <div class="toolbar">
-      <el-select v-model="filterStockStatus" placeholder="搴撳瓨鐘舵�佺瓫閫�" clearable>
-        <el-option label="鏈夎揣" :value="1" />
-        <el-option label="搴撳瓨绱у紶" :value="2" />
-        <el-option label="宸叉弧" :value="3" />
+      <el-select v-model="filterStockStatus" placeholder="搴撳瓨鐘舵�佺瓫閫�" style="width: 160px" clearable>
+        <el-option label="缁勭洏鏆傚瓨(1)" :value="1" />
+        <el-option label="鍏ュ簱纭(3)" :value="3" />
+        <el-option label="鍏ュ簱瀹屾垚(6)" :value="6" />
+        <el-option label="鍑哄簱閿佸畾(7)" :value="7" />
+        <el-option label="鍑哄簱瀹屾垚(8)" :value="8" />
+        <el-option label="绌烘墭鐩�(22)" :value="22" />
       </el-select>
-      <el-select v-model="filterMaterielCode" placeholder="鐗╂枡绛涢��" clearable>
+      <el-select v-model="filterMaterielCode" placeholder="鐗╂枡绛涢��" style="width: 140px" clearable>
         <el-option v-for="code in materielCodeList" :key="code" :label="code" :value="code" />
       </el-select>
-      <el-select v-model="filterBatchNo" placeholder="鎵规绛涢��" clearable>
+      <el-select v-model="filterBatchNo" placeholder="鎵规绛涢��" style="width: 140px" clearable>
         <el-option v-for="batch in batchNoList" :key="batch" :label="batch" :value="batch" />
       </el-select>
       <el-button @click="resetCamera">閲嶇疆瑙嗚</el-button>
+      <el-button type="primary" @click="refreshData" :loading="refreshing">鍒锋柊鏁版嵁</el-button>
     </div>
 
     <!-- 3D Canvas -->
@@ -31,32 +35,46 @@
 
     <!-- 鐘舵�佸浘渚� -->
     <div class="legend">
+      <div class="legend-title">璐т綅鐘舵��</div>
       <div v-for="item in legendItems" :key="item.status" class="legend-item">
         <span class="color-box" :style="{ background: item.color }" />
         <span>{{ item.label }}</span>
       </div>
     </div>
 
-    <!-- 璇︽儏寮圭獥 -->
-    <el-dialog v-model="detailDialogVisible" title="搴撳瓨璇︽儏" fullscreen>
+    <!-- 璇︽儏渚ц竟闈㈡澘 -->
+    <el-drawer v-model="detailDialogVisible" title="搴撳瓨璇︽儏" direction="rtl" size="500px">
       <div v-if="selectedLocation" class="detail-content">
         <el-descriptions :column="2" border>
           <el-descriptions-item label="璐т綅缂栧彿">{{ selectedLocation.locationCode }}</el-descriptions-item>
           <el-descriptions-item label="璐т綅鐘舵��">{{ getLocationStatusText(selectedLocation.locationStatus) }}</el-descriptions-item>
           <el-descriptions-item label="鎵樼洏缂栧彿">{{ selectedLocation.palletCode || '鏃�' }}</el-descriptions-item>
           <el-descriptions-item label="搴撳瓨鐘舵��">{{ getStockStatusText(selectedLocation.stockStatus) }}</el-descriptions-item>
-          <el-descriptions-item label="褰撳墠搴撳瓨">{{ selectedLocation.stockQuantity }}</el-descriptions-item>
-          <el-descriptions-item label="鐗╂枡缂栧彿">{{ selectedLocation.materielCode || '鏃�' }}</el-descriptions-item>
-          <el-descriptions-item label="鐗╂枡鍚嶇О">{{ selectedLocation.materielName || '鏃�' }}</el-descriptions-item>
-          <el-descriptions-item label="鎵规鍙�">{{ selectedLocation.batchNo || '鏃�' }}</el-descriptions-item>
+          <el-descriptions-item label="鎬诲簱瀛�">{{ selectedLocation.stockQuantity }}{{ selectedLocation.unit || '' }}</el-descriptions-item>
         </el-descriptions>
+
+        <!-- 搴撳瓨鏄庣粏琛ㄦ牸 -->
+        <div v-if="selectedLocation.details && selectedLocation.details.length > 0" class="detail-table">
+          <h4>搴撳瓨鏄庣粏</h4>
+          <el-table :data="selectedLocation.details" border size="small" max-height="400">
+            <el-table-column prop="materielCode" label="鐗╂枡缂栫爜" width="100" />
+            <el-table-column prop="materielName" label="鐗╂枡鍚嶇О" min-width="120" show-overflow-tooltip />
+            <el-table-column prop="batchNo" label="鎵规鍙�" width="100" show-overflow-tooltip />
+            <el-table-column prop="stockQuantity" label="鏁伴噺" width="70" align="right" />
+            <el-table-column prop="unit" label="鍗曚綅" width="50" align="center" />
+            <el-table-column prop="effectiveDate" label="鏈夋晥鏈�" width="100" />
+          </el-table>
+        </div>
+        <div v-else class="no-detail">
+          <el-empty description="鏆傛棤搴撳瓨鏄庣粏" />
+        </div>
       </div>
-    </el-dialog>
+    </el-drawer>
   </div>
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, onUnmounted, watch, getCurrentInstance } from 'vue'
+import { ref, reactive, onMounted, onUnmounted, watch, nextTick, getCurrentInstance } from 'vue'
 import * as THREE from 'three'
 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
 import * as signalR from '@microsoft/signalr'
@@ -66,24 +84,44 @@
 // SignalR 杩炴帴
 let connection = null
 
-// 棰滆壊甯搁噺
+// 棰滆壊甯搁噺 - 鍩轰簬瀹為檯鏋氫妇鍊�
+// 璐т綅鐘舵��(locationStatus): 0=绌洪棽, 1=閿佸畾, 10=鏈夎揣閿佸畾, 20=绌洪棽閿佸畾, 99=澶ф墭鐩橀攣瀹�, 100=鏈夎揣
+// 搴撳瓨鐘舵��(stockStatus): 1=缁勭洏鏆傚瓨, 3=鍏ュ簱纭, 6=鍏ュ簱瀹屾垚, 7=鍑哄簱閿佸畾, 8=鍑哄簱瀹屾垚, 9=绉诲簱閿佸畾绛�
 const COLOR_MAP = {
-  DISABLED: 0x2d2d2d,    // 绂佺敤 - 娣辩伆
-  LOCKED: 0xF56C6C,      // 閿佸畾 - 绾㈣壊
-  EMPTY: 0x4a4a4a,       // 绌鸿揣浣� - 鏆楃伆
-  HAS_STOCK: 0x409EFF,   // 鏈夎揣 - 钃濊壊
-  LOW_STOCK: 0xE6A23C,   // 搴撳瓨绱у紶 - 姗欒壊
-  FULL: 0x67C23A,        // 宸叉弧 - 缁胯壊
+  // 璐т綅鐘舵�侀鑹�
+  LOC_FREE: 0x90EE90,         // 0=绌洪棽 - 娴呯豢鑹�
+  LOC_LOCK: 0xFF6B6B,        // 1=閿佸畾 - 绾㈣壊
+  LOC_INSTOCK_LOCK: 0xFFA500, // 10=鏈夎揣閿佸畾 - 姗欒壊
+  LOC_FREE_LOCK: 0xFFD700,   // 20=绌洪棽閿佸畾 - 閲戣壊
+  LOC_PALLET_LOCK: 0x9370DB,  // 99=澶ф墭鐩橀攣瀹� - 绱壊
+  LOC_INSTOCK: 0x409EFF,     // 100=鏈夎揣 - 钃濊壊
+
+  // 搴撳瓨鐘舵�侀鑹�
+  STOCK_PENDING: 0x00CED1,      // 1=缁勭洏鏆傚瓨 - 娣遍潚鑹�
+  STOCK_CONFIRMED: 0x87CEEB,     // 3=鍏ュ簱纭 - 澶╄摑鑹�
+  STOCK_COMPLETED: 0x32CD32,     // 6=鍏ュ簱瀹屾垚 - 浜豢鑹�
+  STOCK_OUT_LOCK: 0xFF6347,     // 7=鍑哄簱閿佸畾 - 鐣寗绾�
+  STOCK_OUT_COMPLETED: 0x228B22, // 8=鍑哄簱瀹屾垚 - 妫灄缁�
+  STOCK_TRANSFER_LOCK: 0xFF8C00, // 9=绉诲簱閿佸畾 - 娣辨鑹�
+  STOCK_COMPLETED_NO_ORDER: 0x20B2AA, // 10=鍏ュ簱瀹屾垚鏈缓鍑哄簱鍗�
+  STOCK_RETURN: 0xFF4500,       // 11=閫�搴�
+  STOCK_MANUAL_PENDING: 0x48D1CC, // 12=鎵嬪姩缁勭洏鏆傚瓨
+  STOCK_MANUAL_CONFIRMED: 0x7FFFD4, // 13=鎵嬪姩缁勭洏鍏ュ簱纭
+  STOCK_PICK_COMPLETED: 0x6B8E23, // 14=鎷i�夊畬鎴�
+  STOCK_MES_RETURN: 0xDC143C,   // 21=MES閫�搴�
+  STOCK_EMPTY_TRAY: 0xDDA0DD,   // 22=绌烘墭鐩樺簱瀛� - 姊呯孩鑹�
+  STOCK_GROUP_CANCEL: 0xDEB887, // 99=缁勭洏鎾ら攢 - 鏆楅噾鑹�
+  STOCK_IN_CANCEL: 0xA0522D,    // 199=鍏ュ簱鎾ら攢 - 璧壊
 }
 
-// 鍥句緥椤�
+// 鍥句緥椤� - 璐т綅鐘舵��
 const legendItems = [
-  { status: 'disabled', label: '绂佺敤', color: '#2d2d2d' },
-  { status: 'locked', label: '閿佸畾', color: '#F56C6C' },
-  { status: 'empty', label: '绌鸿揣浣�', color: '#4a4a4a' },
-  { status: 'hasStock', label: '鏈夎揣', color: '#409EFF' },
-  { status: 'lowStock', label: '搴撳瓨绱у紶', color: '#E6A23C' },
-  { status: 'full', label: '宸叉弧', color: '#67C23A' },
+  { status: 'loc_free', label: '绌洪棽(0)', color: '#90EE90' },
+  { status: 'loc_lock', label: '閿佸畾(1)', color: '#FF6B6B' },
+  { status: 'loc_instock_lock', label: '鏈夎揣閿佸畾(10)', color: '#FFA500' },
+  { status: 'loc_free_lock', label: '绌洪棽閿佸畾(20)', color: '#FFD700' },
+  { status: 'loc_pallet_lock', label: '澶ф墭鐩橀攣瀹�(99)', color: '#9370DB' },
+  { status: 'loc_instock', label: '鏈夎揣(100)', color: '#409EFF' },
 ]
 
 // Refs
@@ -99,12 +137,15 @@
 const batchNoList = ref([])
 const detailDialogVisible = ref(false)
 const selectedLocation = ref(null)
+const refreshing = ref(false)
 
 // Three.js 鐩稿叧
 let scene, camera, renderer, controls, raycaster, mouse
 let locationMesh = null
 let locationData = []
+let originalLocationData = [] // 淇濆瓨鍘熷瀹屾暣鏁版嵁锛岀敤浜庣瓫閫夋仮澶�
 let animationId = null
+let locationIdToInstanceId = new Map() // locationId -> instanceId 鏄犲皠
 
 // SignalR 鍒濆鍖�
 function initSignalR() {
@@ -117,51 +158,100 @@
     connection.start().catch((err) => console.log('SignalR杩炴帴澶辫触:', err));
 
     connection.on('StockUpdated', (update) => {
+      console.log('鏀跺埌搴撳瓨鏇存柊:', update)
       // 鏇存柊瀵瑰簲璐т綅鐨勬暟鎹�
       const idx = locationData.findIndex(x => x.locationId === update.locationId);
       if (idx !== -1) {
         locationData[idx].stockQuantity = update.stockQuantity;
         locationData[idx].stockStatus = update.stockStatus;
-        // 閲嶆柊娓叉煋鍗曚釜璐т綅棰滆壊
-        updateInstanceColor(idx, update.stockStatus);
+        locationData[idx].palletCode = update.palletCode;
+        locationData[idx].locationStatus = update.locationStatus;
+        // 鏇存柊搴撳瓨鏄庣粏
+        if (update.details && update.details.length > 0) {
+          locationData[idx].details = update.details;
+        }
+        // 閫氳繃鏄犲皠鎵惧埌瀹炰緥ID锛屾洿鏂伴鑹�
+        const instanceId = locationIdToInstanceId.get(update.locationId);
+        if (instanceId !== undefined) {
+          updateInstanceColor(instanceId, update.locationStatus);
+        }
       }
     });
   });
 }
 
 // 鏇存柊鍗曚釜璐т綅棰滆壊
-function updateInstanceColor(instanceId, stockStatus) {
+function updateInstanceColor(instanceId, locationStatus) {
   if (!locationMesh) return;
-  const loc = locationData[instanceId];
-  if (!loc) return;
-  const color = getLocationColor(loc);
+  // 鏍规嵁璐т綅鐘舵�佽幏鍙栭鑹�
+  const color = getLocationColorByStatus(locationStatus);
   locationMesh.setColorAt(instanceId, new THREE.Color(color));
   locationMesh.instanceColor.needsUpdate = true;
 }
 
-// 鑾峰彇璐т綅棰滆壊
-function getLocationColor(location) {
-  if (location.locationStatus === 3) return COLOR_MAP.DISABLED  // 绂佺敤
-  if (location.locationStatus === 2) return COLOR_MAP.LOCKED     // 閿佸畾
-  if (location.locationStatus === 1) {
-    if (location.stockStatus === 0) return COLOR_MAP.EMPTY       // 鏃犺揣
-    if (location.stockStatus === 1) return COLOR_MAP.HAS_STOCK    // 鏈夎揣
-    if (location.stockStatus === 2) return COLOR_MAP.LOW_STOCK   // 搴撳瓨绱у紶
-    if (location.stockStatus === 3) return COLOR_MAP.FULL        // 宸叉弧
+// 鏍规嵁璐т綅鐘舵�佽幏鍙栭鑹�
+function getLocationColorByStatus(locStatus) {
+  switch (locStatus) {
+    case 0: return COLOR_MAP.LOC_FREE           // 绌洪棽
+    case 1: return COLOR_MAP.LOC_LOCK            // 閿佸畾
+    case 10: return COLOR_MAP.LOC_INSTOCK_LOCK   // 鏈夎揣閿佸畾
+    case 20: return COLOR_MAP.LOC_FREE_LOCK      // 绌洪棽閿佸畾
+    case 99: return COLOR_MAP.LOC_PALLET_LOCK    // 澶ф墭鐩橀攣瀹�
+    case 100: return COLOR_MAP.LOC_INSTOCK      // 鏈夎揣
+    default: return COLOR_MAP.LOC_FREE // 榛樿绌洪棽鑹�
   }
-  return COLOR_MAP.EMPTY // 榛樿绌�
+}
+
+// 鑾峰彇璐т綅棰滆壊 - 鍙牴鎹揣浣嶇姸鎬�
+function getLocationColor(location) {
+  const locStatus = location.locationStatus
+
+  // 鏍规嵁璐т綅鐘舵�佸垽鏂鑹�
+  switch (locStatus) {
+    case 0: return COLOR_MAP.LOC_FREE           // 绌洪棽
+    case 1: return COLOR_MAP.LOC_LOCK            // 閿佸畾
+    case 10: return COLOR_MAP.LOC_INSTOCK_LOCK   // 鏈夎揣閿佸畾
+    case 20: return COLOR_MAP.LOC_FREE_LOCK      // 绌洪棽閿佸畾
+    case 99: return COLOR_MAP.LOC_PALLET_LOCK    // 澶ф墭鐩橀攣瀹�
+    case 100: return COLOR_MAP.LOC_INSTOCK      // 鏈夎揣
+    default: return COLOR_MAP.LOC_FREE // 榛樿绌洪棽鑹�
+  }
 }
 
 // 鑾峰彇璐т綅鐘舵�佹枃鏈�
 function getLocationStatusText(status) {
-  const map = { 0: '姝e父', 1: '姝e父', 2: '閿佸畾', 3: '绂佺敤' }
-  return map[status] || '鏈煡'
+  const map = {
+    0: '绌洪棽',
+    1: '閿佸畾',
+    10: '鏈夎揣閿佸畾',
+    20: '绌洪棽閿佸畾',
+    99: '澶ф墭鐩橀攣瀹�',
+    100: '鏈夎揣'
+  }
+  return map[status] || '鏈煡(' + status + ')'
 }
 
 // 鑾峰彇搴撳瓨鐘舵�佹枃鏈�
 function getStockStatusText(status) {
-  const map = { 0: '鏃犺揣', 1: '鏈夎揣', 2: '搴撳瓨绱у紶', 3: '宸叉弧' }
-  return map[status] || '鏈煡'
+  const map = {
+    0: '鏃犲簱瀛�',
+    1: '缁勭洏鏆傚瓨',
+    3: '鍏ュ簱纭',
+    6: '鍏ュ簱瀹屾垚',
+    7: '鍑哄簱閿佸畾',
+    8: '鍑哄簱瀹屾垚',
+    9: '绉诲簱閿佸畾',
+    10: '鍏ュ簱瀹屾垚鏈缓鍑哄簱鍗�',
+    11: '閫�搴�',
+    12: '鎵嬪姩缁勭洏鏆傚瓨',
+    13: '鎵嬪姩缁勭洏鍏ュ簱纭',
+    14: '鎷i�夊畬鎴�',
+    21: 'MES閫�搴�',
+    22: '绌烘墭鐩樺簱瀛�',
+    99: '缁勭洏鎾ら攢',
+    199: '鍏ュ簱鎾ら攢'
+  }
+  return map[status] || '鏈煡(' + status + ')'
 }
 
 // 鍔犺浇浠撳簱鍒楄〃
@@ -184,15 +274,13 @@
 async function loadWarehouseData(warehouseId) {
   try {
     const res = await proxy.http.get(`/api/StockInfo/Get3DLayout?warehouseId=${warehouseId}`)
-    console.log('Get3DLayout response:', res)
     if (res.status && res.data) {
       const data = res.data
-      console.log('data.locations:', data.locations)
-      locationData = data.locations || []
+      originalLocationData = data.locations || [] // 淇濆瓨鍘熷瀹屾暣鏁版嵁
+      locationData = [...originalLocationData] // 褰撳墠鏄剧ず鏁版嵁
       // 浣跨敤鍚庣杩斿洖鐨勭瓫閫夊垪琛�
       materielCodeList.value = data.materielCodeList || []
       batchNoList.value = data.batchNoList || []
-      console.log('locationData set:', locationData.length, 'items')
       // 娓叉煋璐т綅
       renderLocations()
     }
@@ -215,6 +303,7 @@
   // 鍒涘缓鐩告満
   camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
   camera.position.set(20, 20, 20)
+  camera.lookAt(0, 0, 0)
 
   // 鍒涘缓娓叉煋鍣�
   renderer = new THREE.WebGLRenderer({ antialias: true })
@@ -228,13 +317,18 @@
   controls.dampingFactor = 0.05
 
   // 鍒涘缓鐜鍏�
-  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
+  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
   scene.add(ambientLight)
 
-  // 鍒涘缓瀹氬悜鍏�
-  const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
-  directionalLight.position.set(10, 20, 10)
+  // 鍒涘缓涓诲畾鍚戝厜
+  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0)
+  directionalLight.position.set(50, 100, 50)
   scene.add(directionalLight)
+
+  // 鍒涘缓琛ュ厜
+  const fillLight = new THREE.DirectionalLight(0xffffff, 0.3)
+  fillLight.position.set(-50, 50, -50)
+  scene.add(fillLight)
 
   // 鍒涘缓鍦伴潰
   const groundGeometry = new THREE.PlaneGeometry(100, 100)
@@ -253,7 +347,7 @@
   mouse = new THREE.Vector2()
 
   // 娣诲姞鐐瑰嚮浜嬩欢
-  canvasContainer.value.addEventListener('click', onCanvasClick)
+  canvasContainer.value.addEventListener('mousedown', onCanvasClick)
 
   // 寮�濮嬫覆鏌撳惊鐜�
   animate()
@@ -261,19 +355,32 @@
 
 // 娓叉煋璐т綅
 function renderLocations() {
-  console.log('renderLocations called', { scene: !!scene, locationDataLength: locationData.length })
   if (!scene) return
+  console.log('娓叉煋璐т綅锛屽師濮嬫暟鎹�绘暟:', originalLocationData.length)
+  // 濡傛灉鍘熷鏁版嵁涓虹┖锛屽皾璇曢噸鏂板姞杞�
+  if (originalLocationData.length === 0) {
+    console.warn('鍘熷鏁版嵁涓虹┖锛岄噸鏂板姞杞�...')
+    if (activeWarehouse.value) {
+      loadWarehouseData(activeWarehouse.value)
+    }
+    return
+  }
 
   // 绉婚櫎鏃х殑璐т綅缃戞牸
+  console.log("馃殌 ~ renderLocations ~ locationMesh:", locationMesh)
   if (locationMesh) {
     scene.remove(locationMesh)
     locationMesh.geometry.dispose()
-    locationMesh.material.forEach(m => m.dispose())
+    if (Array.isArray(locationMesh.material)) {
+      locationMesh.material.forEach(m => m.dispose())
+    } else {
+      locationMesh.material.dispose()
+    }
     locationMesh = null
   }
 
-  // 杩囨护鏁版嵁
-  let filteredData = [...locationData]
+  // 杩囨护鏁版嵁 - 濮嬬粓浠庡師濮嬪畬鏁存暟鎹繃婊�
+  let filteredData = [...originalLocationData]
   if (filterStockStatus.value !== null) {
     filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
   }
@@ -284,14 +391,27 @@
     filteredData = filteredData.filter(loc => loc.batchNo === filterBatchNo.value)
   }
 
-  console.log('filteredData length:', filteredData.length)
-  if (filteredData.length === 0) return
+  console.log('杩囨护鍚庢暟鎹暟閲�:', filteredData.length, '绛涢�夋潯浠�:', filterStockStatus.value, filterMaterielCode.value, filterBatchNo.value)
 
   // 鍒涘缓 InstancedMesh
   const geometry = new THREE.BoxGeometry(1.5, 1, 1.5)
-  const material = new THREE.MeshStandardMaterial({ color: 0xffffff })
+  const material = new THREE.MeshStandardMaterial({
+    color: 0xffffff,
+    roughness: 0.5,
+    metalness: 0.1
+  })
 
-  locationMesh = new THREE.InstancedMesh(geometry, [material], filteredData.length)
+  // 濡傛灉杩囨护鍚庢棤鏁版嵁锛屽垱寤虹┖鐨� InstancedMesh
+  if (filteredData.length === 0) {
+    locationMesh = new THREE.InstancedMesh(geometry, material, 0)
+    scene.add(locationMesh)
+    return
+  }
+
+  locationMesh = new THREE.InstancedMesh(geometry, material, filteredData.length)
+
+  // 娓呯┖骞堕噸寤烘槧灏�
+  locationIdToInstanceId.clear()
 
   const dummy = new THREE.Object3D()
   const color = new THREE.Color()
@@ -309,6 +429,9 @@
     color.setHex(getLocationColor(location))
     locationMesh.setColorAt(i, color)
 
+    // 寤虹珛鏄犲皠: locationId -> instanceId (i)
+    locationIdToInstanceId.set(location.locationId, i)
+
     if (i === 0) {
       console.log('First location:', location, { x, y, z })
     }
@@ -318,7 +441,6 @@
   if (locationMesh.instanceColor) locationMesh.instanceColor.needsUpdate = true
 
   scene.add(locationMesh)
-  console.log('locationMesh added to scene')
 }
 
 // 鍔ㄧ敾寰幆
@@ -342,7 +464,7 @@
   if (intersects.length > 0) {
     const instanceId = intersects[0].instanceId
     // 鑾峰彇鍘熷鏁版嵁绱㈠紩锛堣�冭檻杩囨护鍚庣殑鏁版嵁锛�
-    let filteredData = locationData
+    let filteredData = [...originalLocationData]
     if (filterStockStatus.value !== null) {
       filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
     }
@@ -404,15 +526,47 @@
   controls.target.set(0, 0, 0)
 }
 
+// 鍒锋柊鏁版嵁
+async function refreshData() {
+  refreshing.value = true
+  try {
+    // 閲嶇疆绛涢�夋潯浠�
+    filterStockStatus.value = null
+    filterMaterielCode.value = null
+    filterBatchNo.value = null
+    // 閲嶆柊鍔犺浇褰撳墠浠撳簱鏁版嵁
+    if (activeWarehouse.value) {
+      await loadWarehouseData(activeWarehouse.value)
+    }
+  } finally {
+    refreshing.value = false
+  }
+}
+
 // 浠撳簱鍒囨崲
 async function onWarehouseChange(warehouseId) {
   await loadWarehouseData(warehouseId)
 }
 
 // 鐩戝惉绛涢�夊彉鍖�
-watch(filterStockStatus, () => renderLocations())
-watch(filterMaterielCode, () => renderLocations())
-watch(filterBatchNo, () => renderLocations())
+watch(filterStockStatus, async () => {
+  await nextTick()
+  if (originalLocationData.length > 0) {
+    renderLocations()
+  }
+})
+watch(filterMaterielCode, async () => {
+  await nextTick()
+  if (originalLocationData.length > 0) {
+    renderLocations()
+  }
+})
+watch(filterBatchNo, async () => {
+  await nextTick()
+  if (originalLocationData.length > 0) {
+    renderLocations()
+  }
+})
 
 // 绐楀彛澶у皬鍙樺寲
 function onWindowResize() {
@@ -438,7 +592,7 @@
     cancelAnimationFrame(animationId)
   }
   if (canvasContainer.value) {
-    canvasContainer.value.removeEventListener('click', onCanvasClick)
+    canvasContainer.value.removeEventListener('mousedown', onCanvasClick)
   }
   window.removeEventListener('resize', onWindowResize)
   if (renderer) {
@@ -455,6 +609,7 @@
   width: 100%;
   height: calc(100vh - 120px);
   position: relative;
+  overflow: visible;
 }
 .toolbar {
   position: absolute;
@@ -466,10 +621,12 @@
   background: rgba(255, 255, 255, 0.9);
   padding: 10px;
   border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
 }
 .canvas-container {
   width: 100%;
   height: 100%;
+  overflow: hidden;
 }
 .legend {
   position: absolute;
@@ -480,19 +637,48 @@
   border-radius: 4px;
   color: white;
   z-index: 10;
+  max-height: 400px;
+  overflow-y: auto;
+}
+.legend-title {
+  font-weight: bold;
+  font-size: 13px;
+  margin-bottom: 6px;
+  color: #fff;
+}
+.legend-divider {
+  height: 1px;
+  background: rgba(255, 255, 255, 0.3);
+  margin: 8px 0;
 }
 .legend-item {
   display: flex;
   align-items: center;
   gap: 8px;
   margin-bottom: 4px;
+  font-size: 12px;
 }
 .color-box {
   width: 16px;
   height: 16px;
   border-radius: 2px;
+  flex-shrink: 0;
 }
 .detail-content {
   padding: 20px;
 }
+.detail-table {
+  margin-top: 20px;
+}
+.detail-table h4 {
+  margin-bottom: 10px;
+  color: #303133;
+}
+.no-detail {
+  margin-top: 20px;
+}
+
+:deep(.el-drawer__body) {
+  padding: 20px;
+}
 </style>

--
Gitblit v1.9.3