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_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs                                 |   26 +
 Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md                     |  314 +++++++++++++++++++
 Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs                                          |    1 
 Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue                                         |  328 ++++++++++++++++----
 Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs                              |   58 ++-
 Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs |  199 ++++++++++++
 Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs                                    |   21 +
 7 files changed, 854 insertions(+), 93 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>
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
index ff752be..a2aaed2 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
@@ -77,12 +77,12 @@
         public int Layer { get; set; }
 
         /// <summary>
-        /// 璐т綅鐘舵�� (0=绌�, 1=鍗犵敤, 2=閿佸畾, 3=绂佺敤)
+        /// 璐т綅鐘舵��
         /// </summary>
         public int LocationStatus { get; set; }
 
         /// <summary>
-        /// 搴撳瓨鐘舵�� (0=鏃犺揣, 1=鏈夎揣, 2=搴撳瓨绱у紶, 3=宸叉弧)
+        /// 搴撳瓨鐘舵��
         /// </summary>
         public int StockStatus { get; set; }
 
@@ -115,5 +115,27 @@
         /// 鎵规鍙�
         /// </summary>
         public string? BatchNo { get; set; }
+
+        /// <summary>
+        /// 搴撳瓨鏄庣粏鍒楄〃
+        /// </summary>
+        public List<StockDetailItemDTO> Details { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 搴撳瓨鏄庣粏椤笵TO
+    /// </summary>
+    public class StockDetailItemDTO
+    {
+        public int Id { get; set; }
+        public string? MaterielCode { get; set; }
+        public string? MaterielName { get; set; }
+        public string? BatchNo { get; set; }
+        public float StockQuantity { get; set; }
+        public string? Unit { get; set; }
+        public string? ProductionDate { get; set; }
+        public string? EffectiveDate { get; set; }
+        public string? OrderNo { get; set; }
+        public int Status { get; set; }
     }
 }
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
index d992bdf..81f3390 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
@@ -107,7 +107,7 @@
             var locations = await _locationInfoService.Repository.QueryDataAsync(x => x.WarehouseId == warehouseId);
 
             // 3. 鏌ヨ璇ヤ粨搴撴墍鏈夊簱瀛樹俊鎭紙鍖呭惈Details瀵艰埅灞炴�э級
-            var stockInfos = await Repository.QueryDataNavAsync(x => x.WarehouseId == warehouseId);
+            var stockInfos = await Repository.QueryDataNavAsync(x => x.WarehouseId == warehouseId && x.LocationId != 0);
 
             // 4. 鎻愬彇鐗╂枡缂栧彿鍜屾壒娆″彿鍒楄〃锛堝幓閲嶏級
             var materielCodeList = stockInfos
@@ -145,34 +145,52 @@
                 };
 
                 // 灏濊瘯浠庡簱瀛樺瓧鍏镐腑鑾峰彇搴撳瓨淇℃伅
-                if (stockDict.TryGetValue(loc.Id, out var stockInfo) && stockInfo.Details != null)
+                if (stockDict.TryGetValue(loc.Id, out var stockInfo))
                 {
+                    // 绌烘墭鐩樹篃鏈夊簱瀛樿褰曪紝鍙槸涓嶅寘鍚槑缁�
                     item.PalletCode = stockInfo.PalletCode;
-                    item.StockQuantity = stockInfo.Details.Sum(d => d.StockQuantity);
+                    item.StockStatus = stockInfo.StockStatus; // 鐩存帴浣跨敤鍚庣搴撳瓨鐘舵��
 
-                    // 鑾峰彇绗竴涓槑缁嗙殑鐗╂枡淇℃伅锛堝鏋滃瓨鍦級
-                    var firstDetail = stockInfo.Details.FirstOrDefault();
-                    if (firstDetail != null)
+                    // 鍙湁褰揇etails涓嶄负null涓旀湁鏁版嵁鏃舵墠澶勭悊搴撳瓨鏄庣粏
+                    if (stockInfo.Details != null && stockInfo.Details.Any())
                     {
-                        item.MaterielCode = firstDetail.MaterielCode;
-                        item.MaterielName = firstDetail.MaterielName;
-                        item.BatchNo = firstDetail.BatchNo;
-                    }
+                        item.StockQuantity = stockInfo.Details.Sum(d => d.StockQuantity);
 
-                    // 璁$畻搴撳瓨鐘舵��
-                    var ratio = item.MaxCapacity > 0 ? item.StockQuantity / item.MaxCapacity : 0;
-                    if (ratio >= 0.9f)
-                        item.StockStatus = 3; // 宸叉弧 (FULL)
-                    else if (ratio >= 0.1f)
-                        item.StockStatus = 1; // 鏈夎揣 (HAS_STOCK)
-                    else if (ratio > 0)
-                        item.StockStatus = 2; // 搴撳瓨绱у紶 (LOW_STOCK)
+                        // 鑾峰彇绗竴涓槑缁嗙殑鐗╂枡淇℃伅锛堝鏋滃瓨鍦級
+                        var firstDetail = stockInfo.Details.FirstOrDefault();
+                        if (firstDetail != null)
+                        {
+                            item.MaterielCode = firstDetail.MaterielCode;
+                            item.MaterielName = firstDetail.MaterielName;
+                            item.BatchNo = firstDetail.BatchNo;
+                        }
+
+                        // 濉厖搴撳瓨鏄庣粏鍒楄〃
+                        item.Details = stockInfo.Details.Select(d => new StockDetailItemDTO
+                        {
+                            Id = d.Id,
+                            MaterielCode = d.MaterielCode,
+                            MaterielName = d.MaterielName,
+                            BatchNo = d.BatchNo,
+                            StockQuantity = d.StockQuantity,
+                            Unit = d.Unit,
+                            ProductionDate = d.ProductionDate,
+                            EffectiveDate = d.EffectiveDate,
+                            OrderNo = d.OrderNo,
+                            Status = d.Status
+                        }).ToList();
+                    }
                     else
-                        item.StockStatus = 0; // 鏃犺揣 (EMPTY)
+                    {
+                        // 绌烘墭鐩橈紙鏃犳槑缁嗭級
+                        item.StockQuantity = 0;
+                        item.Details = new List<StockDetailItemDTO>(); // 纭繚鏄┖鍒楄〃鑰岄潪null
+                    }
                 }
                 else
                 {
-                    item.StockStatus = 0; // 鏃犺揣 (EMPTY)
+                    // 鏃犲簱瀛樿褰曪紝璐т綅涓虹┖
+                    item.StockStatus = 0; // 绌洪棽
                     item.StockQuantity = 0;
                 }
 
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs
new file mode 100644
index 0000000..074c48e
--- /dev/null
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs
@@ -0,0 +1,199 @@
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using WIDESEA_Core.BaseRepository;
+using WIDESEA_IStockService;
+using WIDESEA_Model.Models;
+using WIDESEA_WMSServer.Hubs;
+
+namespace WIDESEA_WMSServer.BackgroundServices
+{
+    /// <summary>
+    /// 搴撳瓨鐩戞帶鍚庡彴鏈嶅姟
+    /// 瀹氭湡妫�鏌ュ簱瀛樺拰璐т綅鏁版嵁鍙樺寲骞堕�氳繃SignalR鎺ㄩ�佸埌鍓嶇
+    /// </summary>
+    public class StockMonitorBackgroundService : BackgroundService
+    {
+        private readonly ILogger<StockMonitorBackgroundService> _logger;
+        private readonly IHubContext<StockHub> _hubContext;
+        private readonly IServiceProvider _serviceProvider;
+
+        // 璐т綅鐘舵�佸揩鐓э細key = LocationId
+        private ConcurrentDictionary<int, LocationSnapshot> _lastLocationSnapshots = new();
+
+        // 鐩戞帶闂撮殧锛堟绉掞級
+        private const int MonitorIntervalMs = 3000;
+
+        public StockMonitorBackgroundService(
+            ILogger<StockMonitorBackgroundService> logger,
+            IHubContext<StockHub> hubContext,
+            IServiceProvider serviceProvider)
+        {
+            _logger = logger;
+            _hubContext = hubContext;
+            _serviceProvider = serviceProvider;
+        }
+
+        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+        {
+            _logger.LogInformation("搴撳瓨鐩戞帶鍚庡彴鏈嶅姟宸插惎鍔�");
+
+            // 绛夊緟搴旂敤瀹屽叏鍚姩
+            await Task.Delay(5000, stoppingToken);
+
+            while (!stoppingToken.IsCancellationRequested)
+            {
+                try
+                {
+                    await CheckChangesAsync();
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "妫�鏌ユ暟鎹彉鍖栨椂鍙戠敓閿欒");
+                }
+
+                await Task.Delay(MonitorIntervalMs, stoppingToken);
+            }
+
+            _logger.LogInformation("搴撳瓨鐩戞帶鍚庡彴鏈嶅姟宸插仠姝�");
+        }
+
+        /// <summary>
+        /// 妫�鏌ヨ揣浣嶅拰搴撳瓨鍙樺寲
+        /// </summary>
+        private async Task CheckChangesAsync()
+        {
+            using var scope = _serviceProvider.CreateScope();
+            var stockService = scope.ServiceProvider.GetRequiredService<IStockInfoService>();
+            var locationRepo = scope.ServiceProvider.GetRequiredService<IRepository<Dt_LocationInfo>>();
+
+            // 1. 鑾峰彇鎵�鏈夎揣浣嶆暟鎹�
+            var allLocations = await locationRepo.QueryDataAsync(x => x.LocationStatus != 99); // 鎺掗櫎绂佺敤鐨勮揣浣�
+
+            // 2. 鑾峰彇鎵�鏈夊簱瀛樻暟鎹紙鍖呭惈鏄庣粏锛�
+            var allStockData = await stockService.Repository.Db.Queryable<Dt_StockInfo>()
+                .Includes(x => x.Details)
+                .ToListAsync();
+
+            // 鏋勫缓搴撳瓨瀛楀吀锛歀ocationId -> StockInfo
+            var stockDict = allStockData
+                .Where(s => s.LocationId > 0)
+                .ToDictionary(s => s.LocationId, s => s);
+
+            // 鏋勫缓褰撳墠璐т綅蹇収瀛楀吀
+            var currentSnapshots = new ConcurrentDictionary<int, LocationSnapshot>();
+
+            foreach (var location in allLocations)
+            {
+                // 鑾峰彇璇ヨ揣浣嶇殑搴撳瓨淇℃伅
+                stockDict.TryGetValue(location.Id, out var stock);
+
+                // 璁$畻搴撳瓨鏁伴噺
+                float totalQuantity = 0;
+                string detailsHash = string.Empty;
+                if (stock?.Details != null && stock.Details.Any())
+                {
+                    totalQuantity = stock.Details.Sum(d => d.StockQuantity);
+                    detailsHash = GenerateDetailsHash(stock.Details.ToList());
+                }
+
+                var snapshot = new LocationSnapshot
+                {
+                    LocationId = location.Id,
+                    WarehouseId = location.WarehouseId,
+                    LocationCode = location.LocationCode,
+                    LocationStatus = location.LocationStatus,
+                    PalletCode = stock?.PalletCode,
+                    StockStatus = stock?.StockStatus ?? 0,
+                    StockQuantity = totalQuantity,
+                    DetailsHash = detailsHash
+                };
+
+                currentSnapshots.TryAdd(location.Id, snapshot);
+
+                // 妫�鏌ユ槸鍚︽湁鍙樺寲
+                if (_lastLocationSnapshots.TryGetValue(location.Id, out var lastSnapshot))
+                {
+                    // 妫�娴嬪彉鍖栵細璐т綅鐘舵�併�佸簱瀛樼姸鎬併�佹暟閲忋�佹槑缁嗗彉鍖�
+                    if (lastSnapshot.LocationStatus != snapshot.LocationStatus ||
+                        lastSnapshot.StockStatus != snapshot.StockStatus ||
+                        lastSnapshot.PalletCode != snapshot.PalletCode ||
+                        Math.Abs(lastSnapshot.StockQuantity - snapshot.StockQuantity) > 0.001f ||
+                        lastSnapshot.DetailsHash != snapshot.DetailsHash)
+                    {
+                        // 鏋勫缓鏇存柊DTO
+                        var update = new StockUpdateDTO
+                        {
+                            LocationId = snapshot.LocationId,
+                            WarehouseId = snapshot.WarehouseId,
+                            PalletCode = snapshot.PalletCode,
+                            StockQuantity = snapshot.StockQuantity,
+                            StockStatus = snapshot.StockStatus,
+                            LocationStatus = snapshot.LocationStatus,
+                            Details = BuildDetailDtos(stock?.Details?.ToList())
+                        };
+
+                        await _hubContext.Clients.All.SendAsync("StockUpdated", update);
+                        _logger.LogDebug("鏁版嵁鍙樺寲鎺ㄩ��: LocationId={LocationId}, LocStatus={LocStatus}, StockStatus={StockStatus}, Quantity={Quantity}",
+                            snapshot.LocationId, snapshot.LocationStatus, snapshot.StockStatus, snapshot.StockQuantity);
+                    }
+                }
+            }
+
+            // 鏇存柊蹇収鏁版嵁
+            _lastLocationSnapshots = currentSnapshots;
+        }
+
+        /// <summary>
+        /// 鐢熸垚鏄庣粏鏁版嵁鍝堝笇
+        /// </summary>
+        private string GenerateDetailsHash(List<Dt_StockInfoDetail> details)
+        {
+            if (details == null || !details.Any()) return string.Empty;
+
+            var hashString = string.Join("|", details
+                .OrderBy(d => d.Id)
+                .Select(d => $"{d.Id}:{d.MaterielCode}:{d.BatchNo}:{d.StockQuantity}"));
+            return hashString.GetHashCode().ToString();
+        }
+
+        /// <summary>
+        /// 鏋勫缓鏄庣粏DTO鍒楄〃
+        /// </summary>
+        private List<StockDetailUpdateDTO> BuildDetailDtos(List<Dt_StockInfoDetail> details)
+        {
+            if (details == null || !details.Any()) return new List<StockDetailUpdateDTO>();
+
+            return details.Select(d => new StockDetailUpdateDTO
+            {
+                Id = d.Id,
+                MaterielCode = d.MaterielCode,
+                MaterielName = d.MaterielName,
+                BatchNo = d.BatchNo,
+                StockQuantity = d.StockQuantity,
+                Unit = d.Unit,
+                Status = d.Status
+            }).ToList();
+        }
+
+        /// <summary>
+        /// 璐т綅蹇収
+        /// </summary>
+        private class LocationSnapshot
+        {
+            public int LocationId { get; set; }
+            public int WarehouseId { get; set; }
+            public string LocationCode { get; set; }
+            public int LocationStatus { get; set; }
+            public string PalletCode { get; set; }
+            public int StockStatus { get; set; }
+            public float StockQuantity { get; set; }
+            public string DetailsHash { get; set; }
+        }
+    }
+}
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs
index d5208b1..af48512 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs
@@ -1,4 +1,5 @@
 using Microsoft.AspNetCore.SignalR;
+using System.Collections.Generic;
 using System.Threading.Tasks;
 
 namespace WIDESEA_WMSServer.Hubs
@@ -14,11 +15,31 @@
         }
     }
 
+    /// <summary>
+    /// 搴撳瓨鏇存柊DTO锛圫ignalR鎺ㄩ�佺敤锛�
+    /// </summary>
     public class StockUpdateDTO
     {
         public int LocationId { get; set; }
         public int WarehouseId { get; set; }
+        public string PalletCode { get; set; }
         public float StockQuantity { get; set; }
         public int StockStatus { get; set; }
+        public int LocationStatus { get; set; }
+        public List<StockDetailUpdateDTO> Details { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 搴撳瓨鏄庣粏鏇存柊DTO
+    /// </summary>
+    public class StockDetailUpdateDTO
+    {
+        public int Id { get; set; }
+        public string MaterielCode { get; set; }
+        public string MaterielName { get; set; }
+        public string BatchNo { get; set; }
+        public float StockQuantity { get; set; }
+        public string Unit { get; set; }
+        public int Status { get; set; }
     }
 }
\ No newline at end of file
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
index 59cbd0a..a3e0f16 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
@@ -81,6 +81,7 @@
 builder.Services.AddDbSetup(); // Db 鏁版嵁搴撻厤缃�
 builder.Services.AddInitializationHostServiceSetup(); // 搴旂敤绋嬪簭鍒濆鍖栨湇鍔℃敞鍐�
 builder.Services.AddHostedService<AutoOutboundTaskBackgroundService>();  // 鍚姩鑷姩鍑哄簱浠诲姟鍚庡彴鏈嶅姟
+builder.Services.AddHostedService<StockMonitorBackgroundService>();  // 鍚姩搴撳瓨鐩戞帶鍚庡彴鏈嶅姟
 // builder.Services.AddHostedService<PermissionDataHostService>(); // 鏉冮檺鏁版嵁鏈嶅姟
 builder.Services.AddAutoMapperSetup();
 
diff --git a/Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md b/Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md
new file mode 100644
index 0000000..7cddf9c
--- /dev/null
+++ b/Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md
@@ -0,0 +1,314 @@
+# 搴撳瓨3D鏌ョ湅鍣� Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** 瀹炵幇搴撳瓨3D鏌ョ湅鍣紝鐢ㄦ埛鍙湪 Three.js 3D 鍦烘櫙涓贰瑙嗕粨搴撱�佺偣鍑昏揣浣嶆煡鐪嬪簱瀛樿鎯�
+
+**Architecture:** 鍓嶇 Vue 3 + Element Plus + Three.js锛屽悗绔� ASP.NET Core 6 Web API + SignalR 瀹炴椂鎺ㄩ��
+
+**Tech Stack:** Three.js, @microsoft/signalr, Element Plus, Vue 3 Composition API
+
+---
+
+## 鏂囦欢缁撴瀯
+
+```
+鍚庣 (WIDESEA_WMSServer)
+鈹溾攢鈹� WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs      # 3D甯冨眬鍝嶅簲DTO [鏂板缓]
+鈹溾攢鈹� WIDESEA_IStockService/IStockInfoService.cs  # 娣诲姞Get3DLayoutAsync鏂规硶绛惧悕 [淇敼]
+鈹溾攢鈹� WIDESEA_StockService/StockInfoService.cs    # 瀹炵幇Get3DLayoutAsync [淇敼]
+鈹溾攢鈹� WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs  # 娣诲姞Get3DLayout绔偣 [淇敼]
+鈹斺攢鈹� WIDESEA_WMSServer/Hubs/StockHub.cs         # SignalR Hub [鏂板缓]
+
+鍓嶇 (WIDESEA_WMSClient)
+鈹溾攢鈹� package.json                                # 娣诲姞three渚濊禆 [淇敼]
+鈹溾攢鈹� src/router/viewGird.js                      # 娉ㄥ唽璺敱 [淇敼]
+鈹溾攢鈹� src/views/stock/stockChat.vue               # 涓婚〉闈㈢粍浠� [鏂板缓]
+鈹斺攢鈹� src/extension/stock/stockChat.js            # 鎵╁睍閰嶇疆 [鏂板缓]
+```
+
+---
+
+## 瀹炵幇浠诲姟
+
+### Task 1: 鍚庣 - 鍒涘缓 Stock3DLayoutDTO
+
+**Files:**
+- Create: `WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+鍒涘缓涓や釜 DTO 绫伙細
+1. `Stock3DLayoutDTO` - 鍖呭惈浠撳簱鍩烘湰淇℃伅銆佸昂瀵搞�佺瓫閫夊垪琛ㄣ�佽揣浣嶆暟缁�
+2. `Location3DItemDTO` - 鍖呭惈鍗曚釜璐т綅鐨勬墍鏈�3D娓叉煋鎵�闇�鏁版嵁
+
+**楠屾敹鏍囧噯锛�**
+- DTO 鍖呭惈鎵�鏈� spec 涓畾涔夌殑瀛楁
+- 鍛藉悕绌洪棿姝g‘
+- 鍙互琚� Service 灞傛纭紩鐢�
+
+```csharp
+namespace WIDESEA_DTO.Stock
+{
+    /// <summary>
+    /// 浠撳簱3D甯冨眬鍝嶅簲DTO
+    /// </summary>
+    public class Stock3DLayoutDTO
+    {
+        public int WarehouseId { get; set; }
+        public string WarehouseName { get; set; }
+        public int MaxRow { get; set; }
+        public int MaxColumn { get; set; }
+        public int MaxLayer { get; set; }
+        public List<string> MaterielCodeList { get; set; } = new();
+        public List<string> BatchNoList { get; set; } = new();
+        public List<Location3DItemDTO> Locations { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 璐т綅3D鏁版嵁椤�
+    /// </summary>
+    public class Location3DItemDTO
+    {
+        public int LocationId { get; set; }
+        public string LocationCode { get; set; }
+        public int Row { get; set; }
+        public int Column { get; set; }
+        public int Layer { get; set; }
+        public int LocationStatus { get; set; } // 0=绌�, 1=鍗犵敤, 2=閿佸畾, 3=绂佺敤
+        public int StockStatus { get; set; } // 0=鏃犺揣, 1=鏈夎揣, 2=搴撳瓨绱у紶, 3=宸叉弧
+        public float StockQuantity { get; set; }
+        public float MaxCapacity { get; set; }
+        public string? PalletCode { get; set; }
+        public string? MaterielCode { get; set; }
+        public string? MaterielName { get; set; }
+        public string? BatchNo { get; set; }
+    }
+}
+```
+
+---
+
+### Task 2: 鍚庣 - 鏇存柊 IStockInfoService 鎺ュ彛
+
+**Files:**
+- Modify: `WIDESEA_WMSServer/WIDESEA_IStockService/IStockInfoService.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+鍦ㄦ帴鍙d腑娣诲姞鏂规硶绛惧悕锛�
+```csharp
+/// <summary>
+/// 鑾峰彇浠撳簱3D甯冨眬鏁版嵁
+/// </summary>
+/// <param name="warehouseId">浠撳簱ID</param>
+/// <returns>3D甯冨眬DTO</returns>
+Task<Stock3DLayoutDTO> Get3DLayoutAsync(int warehouseId);
+```
+
+**楠屾敹鏍囧噯锛�**
+- 鏂规硶绛惧悕姝g‘
+- 娣诲姞浜嗘枃妗f敞閲�
+- 寮曠敤浜� Stock3DLayoutDTO
+
+---
+
+### Task 3: 鍚庣 - 瀹炵幇 Get3DLayoutAsync
+
+**Files:**
+- Modify: `WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+瀹炵幇 Get3DLayoutAsync 鏂规硶锛�
+1. 鏌ヨ浠撳簱淇℃伅
+2. 鏌ヨ璇ヤ粨搴撴墍鏈夎揣浣�
+3. 鏌ヨ搴撳瓨淇℃伅锛堝寘鍚槑缁嗭級
+4. 鎻愬彇鐗╂枡缂栧彿鍜屾壒娆″彿鍒楄〃
+5. 鏄犲皠鍒� Location3DItemDTO
+6. 璁$畻浠撳簱灏哄
+
+**楠屾敹鏍囧噯锛�**
+- 鏂规硶鑳芥纭繑鍥� Stock3DLayoutDTO
+- 鎵�鏈� locationStatus 鍜� stockStatus 鍊兼纭槧灏�
+- 鎬ц兘閫傚悎涓瀷浠撳簱锛�1000-5000璐т綅锛�
+
+---
+
+### Task 4: 鍚庣 - 娣诲姞 API 绔偣
+
+**Files:**
+- Modify: `WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+娣诲姞绔偣锛�
+```csharp
+/// <summary>
+/// 鑾峰彇浠撳簱3D甯冨眬
+/// </summary>
+/// <param name="warehouseId">浠撳簱ID</param>
+/// <returns>3D甯冨眬鏁版嵁</returns>
+[HttpGet("Get3DLayout")]
+public async Task<WebResponseContent> Get3DLayout(int warehouseId)
+{
+    var result = await Service.Get3DLayoutAsync(warehouseId);
+    return WebResponseContent.Instance.OK(result);
+}
+```
+
+**楠屾敹鏍囧噯锛�**
+- 璺敱姝g‘锛欸ET /api/StockInfo/Get3DLayout?warehouseId={id}
+- 杩斿洖鏍煎紡绗﹀悎 WebResponseContent 瑙勮寖
+
+---
+
+### Task 5: 鍚庣 - 鍒涘缓 SignalR Hub
+
+**Files:**
+- Create: `WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs`
+- Modify: `WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+1. 鍒涘缓 StockHub 绫伙紝缁ф壙 Microsoft.AspNetCore.SignalR.Hub
+2. 娣诲姞 SendStockUpdate 鏂规硶渚涘閮ㄨ皟鐢�
+3. 鍦� Program.cs 涓敞鍐� SignalR 鏈嶅姟骞舵槧灏� Hub
+
+**楠屾敹鏍囧噯锛�**
+- Hub 鍙鍓嶇杩炴帴
+- SendStockUpdate 鏂规硶瀛樺湪涓斿彲琚皟鐢�
+
+---
+
+### Task 6: 鍓嶇 - 瀹夎 Three.js 渚濊禆
+
+**Files:**
+- Modify: `WIDESEA_WMSClient/package.json`
+
+**璇︾粏瑙勮寖锛�**
+
+娣诲姞 three 渚濊禆鍒� package.json锛�
+```json
+"three": "^0.160.0"
+```
+
+**楠屾敹鏍囧噯锛�**
+- package.json 鍖呭惈 three 渚濊禆
+- 鐗堟湰鍙峰悎鐞嗭紙^0.160.0 鎴栨洿鏂扮ǔ瀹氱増锛�
+
+---
+
+### Task 7: 鍓嶇 - 娉ㄥ唽璺敱
+
+**Files:**
+- Modify: `WIDESEA_WMSClient/src/router/viewGird.js`
+
+**璇︾粏瑙勮寖锛�**
+
+鍦� stockView 璺敱鍚庢坊鍔狅細
+```javascript
+{
+  path: '/stockChat',
+  name: 'stockChat',
+  component: () => import('@/views/stock/stockChat.vue')
+}
+```
+
+**楠屾敹鏍囧噯锛�**
+- 璺敱娉ㄥ唽姝g‘
+- 涓庡叾浠栬矾鐢辨牸寮忎竴鑷�
+
+---
+
+### Task 8: 鍓嶇 - 鍒涘缓 stockChat.vue 涓荤粍浠�
+
+**Files:**
+- Create: `WIDESEA_WMSClient/src/views/stock/stockChat.vue`
+
+**璇︾粏瑙勮寖锛�**
+
+缁勪欢蹇呴』鍖呭惈锛�
+1. 浠撳簱 Tabs锛坋l-tabs锛�
+2. 宸ュ叿鏍忥紙绛涢�� + 閲嶇疆瑙嗚鎸夐挳锛�
+3. 3D Canvas 瀹瑰櫒
+4. 鐘舵�佸浘渚�
+5. 璇︽儏寮圭獥锛坋l-dialog fullscreen锛�
+
+Three.js 鍦烘櫙锛�
+1. 鍦烘櫙鍒濆鍖栵紙鑳屾櫙鑹� 0x1a1a2e锛�
+2. 閫忚鐩告満
+3. WebGLRenderer
+4. OrbitControls锛堥樆灏煎惎鐢ㄧ殑杞ㄩ亾鎺у埗鍣級
+5. 鐜鍏� + 瀹氬悜鍏�
+6. 鍦伴潰锛圥laneGeometry锛岀綉鏍硷級
+7. InstancedMesh 鎵归噺娓叉煋璐т綅
+8. Raycaster 鐐瑰嚮鎷惧彇
+9. 鐩告満 lerp 鑱氱劍鍔ㄧ敾
+
+棰滆壊缂栫爜锛堝墠绔疄鐜帮級锛�
+- DISABLED(3): 0x2d2d2d
+- LOCKED(2): 0xF56C6C
+- EMPTY(0/鏃犺揣): 0x4a4a4a
+- HAS_STOCK(1): 0x409EFF
+- LOW_STOCK(2): 0xE6A23C
+- FULL(3): 0x67C23A
+
+**楠屾敹鏍囧噯锛�**
+- 椤甸潰鍙互姝e父鍔犺浇
+- Three.js 鍦烘櫙姝g‘鍒濆鍖�
+- 鐐瑰嚮璐т綅鑳芥樉绀鸿鎯呭脊绐�
+- 棰滆壊缂栫爜姝g‘
+
+---
+
+### Task 9: 鍓嶇 - 鍒涘缓鎵╁睍閰嶇疆鏂囦欢
+
+**Files:**
+- Create: `WIDESEA_WMSClient/src/extension/stock/stockChat.js`
+
+**璇︾粏瑙勮寖锛�**
+
+鍒涘缓鏍囧噯鎵╁睍鏂囦欢鏍煎紡锛�
+```javascript
+let extension = {
+  components: {
+    gridHeader: '',
+    gridBody: '',
+    gridFooter: '',
+    modelHeader: '',
+    modelBody: '',
+    modelFooter: ''
+  },
+  tableAction: '',
+  buttons: { view: [], box: [], detail: [] },
+  methods: {
+    onInit() {},
+    onInited() {}
+  }
+};
+export default extension;
+```
+
+**楠屾敹鏍囧噯锛�**
+- 绗﹀悎椤圭洰鐜版湁鎵╁睍鏂囦欢妯″紡
+
+---
+
+### Task 10: 鍓嶇 - 闆嗘垚 SignalR 瀹炴椂鏇存柊
+
+**Files:**
+- Modify: `WIDESEA_WMSClient/src/views/stock/stockChat.vue`
+
+**璇︾粏瑙勮寖锛�**
+
+1. 鍦� onMounted 涓垵濮嬪寲 SignalR 杩炴帴
+2. 杩炴帴 /stockHub
+3. 鐩戝惉 StockUpdated 浜嬩欢
+4. 鏇存柊瀵瑰簲璐т綅鐨� stockQuantity 鍜� stockStatus
+5. 鍔ㄦ�佹洿鏂拌揣浣嶉鑹�
+6. 鍦� onUnmounted 涓柇寮�杩炴帴
+
+**楠屾敹鏍囧噯锛�**
+- SignalR 杩炴帴姝e父寤虹珛
+- 鏀跺埌鏇存柊鏃惰揣浣嶉鑹茶兘鍔ㄦ�佸彉鍖�

--
Gitblit v1.9.3