From 9b77acb859f0866f3a854d2a2842072b2fe9cca8 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期二, 31 三月 2026 16:43:27 +0800
Subject: [PATCH] feat(wms): 完善库存三维看板与库存/货位变更追踪

---
 Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs                                 |    9 
 Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json                                     |    2 
 Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue                                          | 1827 +++++++++++++++++++++++++++------------
 Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/LocationInfoService.cs                            |  147 +++
 Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/SemanticSymbols.db |    0 
 Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs  |  348 +++++-
 Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/StockQuantityChangeRecordService.cs              |   80 +
 Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs                                     |    4 
 Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/RecordService.cs                                 |   49 +
 Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs                                  |    9 
 Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IStockQuantityChangeRecordService.cs            |   14 
 Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/ILocationStatusChangeRecordService.cs           |   15 
 Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/LocationStatusChangeRecordService.cs             |   38 
 Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs                               |  105 ++
 Code/WMS/WIDESEA_WMSServer/WIDESEA_Model/Models/Record/Dt_StockQuantityChangeRecord.cs            |   36 
 Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IRecordService.cs                               |   28 
 Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/CodeChunks.db      |    0 
 17 files changed, 2,035 insertions(+), 676 deletions(-)

diff --git a/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
index a463460..6337909 100644
--- a/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
+++ b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
@@ -1,133 +1,116 @@
 <template>
-  <div class="stock-chat-container">
-    <!-- 浠撳簱 Tabs -->
-    <el-tabs v-model="activeWarehouse" @tab-change="onWarehouseChange">
-      <el-tab-pane
-        v-for="wh in warehouseList"
-        :key="wh.warehouseId || wh.id"
-        :label="wh.warehouseName"
-        :name="wh.warehouseId || wh.id"
-      />
-    </el-tabs>
+    <div class="stock-chat-container">
+        <el-tabs v-model="activeWarehouse" @tab-change="onWarehouseChange">
+            <el-tab-pane
+                v-for="wh in warehouseList"
+                :key="wh.warehouseId || wh.id"
+                :label="wh.warehouseName"
+                :name="wh.warehouseId || wh.id"
+            />
+        </el-tabs>
 
-    <!-- 宸ュ叿鏍� -->
-    <div class="toolbar">
-      <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="鐗╂枡绛涢��" 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="鎵规绛涢��" 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 -->
-    <div ref="canvasContainer" class="canvas-container" />
-
-    <!-- 鐘舵�佸浘渚� -->
-    <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-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 }}{{ 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 class="toolbar">
+            <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="鐗╂枡缂栫爜绛涢��" 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="鎵规绛涢��" 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>
-        <div v-else class="no-detail">
-          <el-empty description="鏆傛棤搴撳瓨鏄庣粏" />
+
+        <div ref="canvasContainer" class="canvas-container" />
+
+        <div class="legend">
+            <div class="legend-title">璐т綅鐘舵��</div>
+            <div class="legend-subtitle">褰㈡�佽鏄�</div>
+            <div class="legend-item legend-item--shape">
+                <span class="shape-pill shape-pill--empty" />
+                <span>绌鸿揣浣�</span>
+            </div>
+            <div class="legend-item legend-item--shape">
+                <span class="shape-pill shape-pill--pallet" />
+                <span>绌烘墭鐩樹綅</span>
+            </div>
+            <div class="legend-item legend-item--shape">
+                <span class="shape-pill shape-pill--cargo" />
+                <span>鏈夎揣浣�</span>
+            </div>
+            <div class="legend-divider" />
+            <div class="legend-subtitle">鐘舵�侀鑹�</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>
-      </div>
-    </el-drawer>
-  </div>
+
+        <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 }}{{ selectedLocation.unit || '' }}</el-descriptions-item>
+                    <el-descriptions-item label="鍑哄簱鏃ユ湡">{{ selectedLocation.outboundDate }}{{ 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="materielName" label="鐗╂枡鍚嶇О" min-width="140" show-overflow-tooltip />
+                        <el-table-column prop="serialNumber" label="鐢佃姱鏉$爜" min-width="160" show-overflow-tooltip />
+                        <el-table-column prop="inboundOrderRowNo" label="閫氶亾鍙�" min-width="100" show-overflow-tooltip />
+                    </el-table>
+                </div>
+                <div v-else class="no-detail">
+                    <el-empty description="鏆傛棤搴撳瓨鏄庣粏" />
+                </div>
+            </div>
+        </el-drawer>
+    </div>
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, onUnmounted, watch, nextTick, getCurrentInstance } from 'vue'
+import { ref, 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'
 
 const { proxy } = getCurrentInstance()
 
-// SignalR 杩炴帴
 let connection = null
 
-// 棰滆壊甯搁噺 - 鍩轰簬瀹為檯鏋氫妇鍊�
-// 璐т綅鐘舵��(locationStatus): 0=绌洪棽, 1=閿佸畾, 10=鏈夎揣閿佸畾, 20=绌洪棽閿佸畾, 99=澶ф墭鐩橀攣瀹�, 100=鏈夎揣
-// 搴撳瓨鐘舵��(stockStatus): 1=缁勭洏鏆傚瓨, 3=鍏ュ簱纭, 6=鍏ュ簱瀹屾垚, 7=鍑哄簱閿佸畾, 8=鍑哄簱瀹屾垚, 9=绉诲簱閿佸畾绛�
 const COLOR_MAP = {
-  // 璐т綅鐘舵�侀鑹�
-  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=鍏ュ簱鎾ら攢 - 璧壊
+    LOC_FREE: 0x90EE90,
+    LOC_LOCK: 0xFF6B6B,
+    LOC_INSTOCK_LOCK: 0xFFA500,
+    LOC_FREE_LOCK: 0xFFD700,
+    LOC_PALLET_LOCK: 0x9370DB,
+    LOC_INSTOCK: 0x409EFF,
+    STOCK_EMPTY_TRAY: 0xDDA0DD
 }
 
-// 鍥句緥椤� - 璐т綅鐘舵��
 const legendItems = [
-  { 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' },
+    { 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' },
+    { status: 'stock_empty_tray', label: '绌烘墭鐩�(22)', color: '#DDA0DD' }
 ]
 
-// Refs
 const canvasContainer = ref(null)
-
-// 鐘舵��
 const activeWarehouse = ref(null)
 const warehouseList = ref([])
 const filterStockStatus = ref(null)
@@ -139,546 +122,1250 @@
 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 scene = null
+let camera = null
+let renderer = null
+let controls = null
+let raycaster = null
+let mouse = null
 let animationId = null
-let locationIdToInstanceId = new Map() // locationId -> instanceId 鏄犲皠
-
-// SignalR 鍒濆鍖�
-function initSignalR() {
-  proxy.http.post('api/User/GetCurrentUserInfo').then((result) => {
-    connection = new signalR.HubConnectionBuilder()
-      .withAutomaticReconnect()
-      .withUrl(`${proxy.http.ipAddress}stockHub?userName=${result.data.userName}`)
-      .build();
-
-    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;
-        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);
-        }
-      }
-    });
-  });
+let locationMeshes = null
+let pickableMeshes = []
+let selectionOutline = null
+let pointerDownHit = null
+let pointerDownPosition = null
+let aisleModels = []
+let isShiftPressed = false
+const preventCanvasContextMenu = (event) => event.preventDefault()
+const preventRightMouseDefault = (event) => {
+    if (event.button === 2) {
+        event.preventDefault()
+    }
 }
 
-// 鏇存柊鍗曚釜璐т綅棰滆壊
-function updateInstanceColor(instanceId, locationStatus) {
-  if (!locationMesh) return;
-  // 鏍规嵁璐т綅鐘舵�佽幏鍙栭鑹�
-  const color = getLocationColorByStatus(locationStatus);
-  locationMesh.setColorAt(instanceId, new THREE.Color(color));
-  locationMesh.instanceColor.needsUpdate = true;
+function updateControlMouseBinding(forceShiftPressed = isShiftPressed) {
+    if (!controls) {
+        return
+    }
+
+    controls.mouseButtons = {
+        LEFT: forceShiftPressed ? THREE.MOUSE.ROTATE : THREE.MOUSE.PAN,
+        MIDDLE: THREE.MOUSE.DOLLY,
+        RIGHT: null
+    }
 }
 
-// 鏍规嵁璐т綅鐘舵�佽幏鍙栭鑹�
+function onKeyDown(event) {
+    if (event.key !== 'Shift' || isShiftPressed) {
+        return
+    }
+
+    isShiftPressed = true
+    updateControlMouseBinding()
+}
+
+function onKeyUp(event) {
+    if (event.key !== 'Shift') {
+        return
+    }
+
+    isShiftPressed = false
+    updateControlMouseBinding()
+}
+
+function onWindowBlur() {
+    isShiftPressed = false
+    updateControlMouseBinding()
+}
+
+let locationData = []
+let originalLocationData = []
+let renderedLocations = []
+const locationIdToInstanceId = new Map()
+const CELL_SPACING_X = 2
+const CELL_SPACING_Z = 2.2
+const AISLE_GAP_Z = 4.8
+const BASE_COLOR = 0x748294
+const PALLET_COLOR = 0xb89c74
+const POST_COLOR = 0x75859d
+
 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 // 榛樿绌洪棽鑹�
-  }
+    switch (locStatus) {
+        case 1:
+            return COLOR_MAP.LOC_LOCK
+        case 10:
+            return COLOR_MAP.LOC_INSTOCK_LOCK
+        case 20:
+            return COLOR_MAP.LOC_FREE_LOCK
+        case 22:
+            return COLOR_MAP.STOCK_EMPTY_TRAY
+        case 99:
+            return COLOR_MAP.LOC_PALLET_LOCK
+        case 100:
+            return COLOR_MAP.LOC_INSTOCK
+        case 0:
+        default:
+            return COLOR_MAP.LOC_FREE
+    }
 }
 
-// 鑾峰彇璐т綅棰滆壊 - 鍙牴鎹揣浣嶇姸鎬�
 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: '绌洪棽',
-    1: '閿佸畾',
-    10: '鏈夎揣閿佸畾',
-    20: '绌洪棽閿佸畾',
-    99: '澶ф墭鐩橀攣瀹�',
-    100: '鏈夎揣'
-  }
-  return map[status] || '鏈煡(' + status + ')'
-}
-
-// 鑾峰彇搴撳瓨鐘舵�佹枃鏈�
-function getStockStatusText(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 + ')'
-}
-
-// 鍔犺浇浠撳簱鍒楄〃
-async function loadWarehouseList() {
-  try {
-    const res = await proxy.http.get('/api/Warehouse/GetAll')
-    if (res.status && res.data) {
-      warehouseList.value = res.data
-      if (res.data.length > 0) {
-        activeWarehouse.value = res.data[0].warehouseId || res.data[0].id
-        await loadWarehouseData(activeWarehouse.value)
-      }
+    if (location.stockStatus === 22) {
+        return COLOR_MAP.STOCK_EMPTY_TRAY
     }
-  } catch (e) {
-    console.error('鍔犺浇浠撳簱鍒楄〃澶辫触', e)
-  }
+    return getLocationColorByStatus(location.locationStatus)
 }
 
-// 鍔犺浇浠撳簱璐т綅鏁版嵁
-async function loadWarehouseData(warehouseId) {
-  try {
-    const res = await proxy.http.get(`/api/StockInfo/Get3DLayout?warehouseId=${warehouseId}`)
-    if (res.status && res.data) {
-      const data = res.data
-      originalLocationData = data.locations || [] // 淇濆瓨鍘熷瀹屾暣鏁版嵁
-      locationData = [...originalLocationData] // 褰撳墠鏄剧ず鏁版嵁
-      // 浣跨敤鍚庣杩斿洖鐨勭瓫閫夊垪琛�
-      materielCodeList.value = data.materielCodeList || []
-      batchNoList.value = data.batchNoList || []
-      // 娓叉煋璐т綅
-      renderLocations()
+function hasCargo(location) {
+    return Number(location.stockQuantity || 0) > 0 || ((location.details && location.details.length > 0) || false)
+}
+
+function hasPallet(location) {
+    if (location.stockStatus === 22 || location.locationStatus === 99) {
+        return true
     }
-  } catch (e) {
-    console.error('鍔犺浇璐т綅鏁版嵁澶辫触', e)
-  }
-}
-
-// 鍒濆鍖� Three.js 鍦烘櫙
-function initThreeJS() {
-  if (!canvasContainer.value) return
-
-  const width = canvasContainer.value.clientWidth
-  const height = canvasContainer.value.clientHeight
-
-  // 鍒涘缓鍦烘櫙
-  scene = new THREE.Scene()
-  scene.background = new THREE.Color(0x1a1a2e)
-
-  // 鍒涘缓鐩告満
-  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 })
-  renderer.setPixelRatio(window.devicePixelRatio)
-  renderer.setSize(width, height)
-  canvasContainer.value.appendChild(renderer.domElement)
-
-  // 鍒涘缓杞ㄩ亾鎺у埗鍣�
-  controls = new OrbitControls(camera, renderer.domElement)
-  controls.enableDamping = true
-  controls.dampingFactor = 0.05
-
-  // 鍒涘缓鐜鍏�
-  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
-  scene.add(ambientLight)
-
-  // 鍒涘缓涓诲畾鍚戝厜
-  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)
-  const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x2d2d2d })
-  const ground = new THREE.Mesh(groundGeometry, groundMaterial)
-  ground.rotation.x = -Math.PI / 2
-  ground.position.y = -0.5
-  scene.add(ground)
-
-  // 鍒涘缓缃戞牸
-  const gridHelper = new THREE.GridHelper(100, 50)
-  scene.add(gridHelper)
-
-  // 鍒涘缓灏勭嚎妫�娴嬪櫒
-  raycaster = new THREE.Raycaster()
-  mouse = new THREE.Vector2()
-
-  // 娣诲姞鐐瑰嚮浜嬩欢
-  canvasContainer.value.addEventListener('mousedown', onCanvasClick)
-
-  // 寮�濮嬫覆鏌撳惊鐜�
-  animate()
-}
-
-// 娓叉煋璐т綅
-function renderLocations() {
-  if (!scene) return
-  console.log('娓叉煋璐т綅锛屽師濮嬫暟鎹�绘暟:', originalLocationData.length)
-  // 濡傛灉鍘熷鏁版嵁涓虹┖锛屽皾璇曢噸鏂板姞杞�
-  if (originalLocationData.length === 0) {
-    console.warn('鍘熷鏁版嵁涓虹┖锛岄噸鏂板姞杞�...')
-    if (activeWarehouse.value) {
-      loadWarehouseData(activeWarehouse.value)
+    if (hasCargo(location)) {
+        return true
     }
-    return
-  }
-
-  // 绉婚櫎鏃х殑璐т綅缃戞牸
-  console.log("馃殌 ~ renderLocations ~ locationMesh:", locationMesh)
-  if (locationMesh) {
-    scene.remove(locationMesh)
-    locationMesh.geometry.dispose()
-    if (Array.isArray(locationMesh.material)) {
-      locationMesh.material.forEach(m => m.dispose())
-    } else {
-      locationMesh.material.dispose()
-    }
-    locationMesh = null
-  }
-
-  // 杩囨护鏁版嵁 - 濮嬬粓浠庡師濮嬪畬鏁存暟鎹繃婊�
-  let filteredData = [...originalLocationData]
-  if (filterStockStatus.value !== null) {
-    filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
-  }
-  if (filterMaterielCode.value) {
-    filteredData = filteredData.filter(loc => loc.materielCode === filterMaterielCode.value)
-  }
-  if (filterBatchNo.value) {
-    filteredData = filteredData.filter(loc => loc.batchNo === filterBatchNo.value)
-  }
-
-  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,
-    roughness: 0.5,
-    metalness: 0.1
-  })
-
-  // 濡傛灉杩囨护鍚庢棤鏁版嵁锛屽垱寤虹┖鐨� 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()
-
-  filteredData.forEach((location, i) => {
-    const x = (location.column - 1) * 2
-    const y = location.layer * 1.5
-    const z = (location.row - 1) * 2
-
-    dummy.position.set(x, y, z)
-    dummy.updateMatrix()
-    locationMesh.setMatrixAt(i, dummy.matrix)
-
-    // 璁剧疆棰滆壊
-    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 })
-    }
-  })
-
-  locationMesh.instanceMatrix.needsUpdate = true
-  if (locationMesh.instanceColor) locationMesh.instanceColor.needsUpdate = true
-
-  scene.add(locationMesh)
+    return Boolean(location.palletCode)
 }
 
-// 鍔ㄧ敾寰幆
-function animate() {
-  animationId = requestAnimationFrame(animate)
-  controls.update()
-  renderer.render(scene, camera)
+function getShelfAccentColor(location) {
+    return BASE_COLOR
 }
 
-// 鐐瑰嚮鐢诲竷
-function onCanvasClick(event) {
-  if (!canvasContainer.value || !locationMesh) return
+function getPalletColor(location) {
+    return PALLET_COLOR
+}
 
-  const rect = canvasContainer.value.getBoundingClientRect()
-  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
-  mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
+function getCargoColor(location) {
+    const cargoColor = new THREE.Color(getLocationColor(location))
+    return cargoColor.offsetHSL(0, 0.04, 0.08).getHex()
+}
 
-  raycaster.setFromCamera(mouse, camera)
-  const intersects = raycaster.intersectObject(locationMesh)
+function getCargoLidColor(location) {
+    const lidColor = new THREE.Color(getCargoColor(location))
+    return lidColor.offsetHSL(0, -0.02, 0.06).getHex()
+}
 
-  if (intersects.length > 0) {
-    const instanceId = intersects[0].instanceId
-    // 鑾峰彇鍘熷鏁版嵁绱㈠紩锛堣�冭檻杩囨护鍚庣殑鏁版嵁锛�
+function getFilteredLocations() {
     let filteredData = [...originalLocationData]
+
     if (filterStockStatus.value !== null) {
-      filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
+        filteredData = filteredData.filter((loc) => loc.stockStatus === filterStockStatus.value)
     }
     if (filterMaterielCode.value) {
-      filteredData = filteredData.filter(loc => loc.materielCode === filterMaterielCode.value)
+        filteredData = filteredData.filter((loc) => loc.materielCode === filterMaterielCode.value)
     }
     if (filterBatchNo.value) {
-      filteredData = filteredData.filter(loc => loc.batchNo === filterBatchNo.value)
+        filteredData = filteredData.filter((loc) => loc.batchNo === filterBatchNo.value)
     }
 
-    if (instanceId < filteredData.length) {
-      selectedLocation.value = filteredData[instanceId]
-      detailDialogVisible.value = true
-      // 鑱氱劍鐩告満
-      focusCamera(selectedLocation.value)
-    }
-  }
+    return filteredData
 }
 
-// 鑱氱劍鐩告満鍒拌揣浣�
+function matchesCurrentFilters(location) {
+    if (filterStockStatus.value !== null && location.stockStatus !== filterStockStatus.value) {
+        return false
+    }
+    if (filterMaterielCode.value && location.materielCode !== filterMaterielCode.value) {
+        return false
+    }
+    if (filterBatchNo.value && location.batchNo !== filterBatchNo.value) {
+        return false
+    }
+    return true
+}
+
+function getLocationStatusText(status) {
+    const map = {
+        0: '绌洪棽',
+        1: '閿佸畾',
+        10: '鏈夎揣閿佸畾',
+        20: '绌洪棽閿佸畾',
+        99: '鎵樼洏閿佸畾',
+        100: '鏈夎揣'
+    }
+    return map[status] || `鏈煡(${status})`
+}
+
+function getStockStatusText(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})`
+}
+
+function createFloorTexture() {
+    const canvas = document.createElement('canvas')
+    canvas.width = 512
+    canvas.height = 512
+    const context = canvas.getContext('2d')
+
+    const gradient = context.createLinearGradient(0, 0, 512, 512)
+    gradient.addColorStop(0, '#f7f9fc')
+    gradient.addColorStop(1, '#dde5ef')
+    context.fillStyle = gradient
+    context.fillRect(0, 0, 512, 512)
+
+    context.strokeStyle = 'rgba(122, 142, 168, 0.16)'
+    context.lineWidth = 2
+    for (let index = 0; index <= 8; index++) {
+        const offset = index * 64
+        context.beginPath()
+        context.moveTo(offset, 0)
+        context.lineTo(offset, 512)
+        context.stroke()
+
+        context.beginPath()
+        context.moveTo(0, offset)
+        context.lineTo(512, offset)
+        context.stroke()
+    }
+
+    context.fillStyle = 'rgba(255, 255, 255, 0.3)'
+    context.fillRect(0, 242, 512, 28)
+
+    const texture = new THREE.CanvasTexture(canvas)
+    texture.wrapS = THREE.RepeatWrapping
+    texture.wrapT = THREE.RepeatWrapping
+    texture.repeat.set(6, 6)
+    return texture
+}
+
+function createBackgroundTexture() {
+    const canvas = document.createElement('canvas')
+    canvas.width = 16
+    canvas.height = 512
+    const context = canvas.getContext('2d')
+    const gradient = context.createLinearGradient(0, 0, 0, 512)
+    gradient.addColorStop(0, '#eef4fb')
+    gradient.addColorStop(0.45, '#dbe6f3')
+    gradient.addColorStop(1, '#c5d1df')
+    context.fillStyle = gradient
+    context.fillRect(0, 0, 16, 512)
+    return new THREE.CanvasTexture(canvas)
+}
+
+function getLayoutInfo() {
+    const rowValues = [...new Set(renderedLocations.map((item) => Number(item.row || 0)).filter((value) => value > 0))].sort((a, b) => a - b)
+    const columnValues = [...new Set(renderedLocations.map((item) => Number(item.column || 0)).filter((value) => value > 0))].sort((a, b) => a - b)
+    const layerValues = [...new Set(renderedLocations.map((item) => Number(item.layer || 0)).filter((value) => value >= 0))].sort((a, b) => a - b)
+    const rowCount = rowValues.length
+    const splitAfterRow = rowCount > 2 ? 2 : 1
+
+    return {
+        rowValues,
+        columnValues,
+        layerValues,
+        rowCount,
+        splitAfterRow,
+        minColumn: columnValues.length > 0 ? columnValues[0] : 1,
+        maxColumn: columnValues.length > 0 ? columnValues[columnValues.length - 1] : 1,
+        maxLayer: layerValues.length > 0 ? layerValues[layerValues.length - 1] : 1
+    }
+}
+
+function getLocationWorldPosition(location, layoutInfo = getLayoutInfo()) {
+    const x = (Number(location.column || 1) - 1) * CELL_SPACING_X
+    const row = Number(location.row || 1)
+    const zBase = (row - 1) * CELL_SPACING_Z
+    const z = row > layoutInfo.splitAfterRow ? zBase + AISLE_GAP_Z : zBase
+    const y = Number(location.layer || 0) * 1.5
+
+    return { x, y, z }
+}
+
+function clearAisleModels() {
+    if (!scene || aisleModels.length === 0) {
+        aisleModels = []
+        return
+    }
+
+    aisleModels.forEach((mesh) => {
+        scene.remove(mesh)
+        if (mesh.geometry) {
+            mesh.geometry.dispose()
+        }
+        if (mesh.material) {
+            if (Array.isArray(mesh.material)) {
+                mesh.material.forEach((item) => item.dispose())
+            } else {
+                mesh.material.dispose()
+            }
+        }
+    })
+
+    aisleModels = []
+}
+
+function renderAisleEquipment(layoutInfo) {
+    clearAisleModels()
+
+    if (!scene || layoutInfo.rowCount < 2) {
+        return
+    }
+
+    const leftRowZ = (layoutInfo.splitAfterRow - 1) * CELL_SPACING_Z
+    const rightRowZ = layoutInfo.splitAfterRow * CELL_SPACING_Z + AISLE_GAP_Z
+    const aisleCenterZ = (leftRowZ + rightRowZ) / 2
+    const minX = (layoutInfo.minColumn - 1) * CELL_SPACING_X - 1.6
+    const maxX = (layoutInfo.maxColumn - 1) * CELL_SPACING_X + 1.6
+    const railLength = Math.max(maxX - minX, 4)
+    const craneHeight = Math.max(layoutInfo.maxLayer * 1.5 + 1.9, 4)
+    const craneX = minX + railLength / 2
+    const craneBaseY = -0.5
+    const mastSpacing = 0.92
+    const mastThickness = 0.16
+    const carriageY = Math.min(craneHeight * 0.48, craneHeight - 0.78)
+    const leftDepth = layoutInfo.splitAfterRow
+    const rightDepth = Math.max(layoutInfo.rowCount - layoutInfo.splitAfterRow, 1)
+    const leftForkLength = leftDepth > 1 ? 1.72 : 1.02
+    const rightForkLength = rightDepth > 1 ? 1.72 : 1.02
+    const forwardSign = rightDepth > leftDepth ? 1 : -1
+
+    function addAisleMesh(geometry, material, position, rotation = null, castShadow = true) {
+        const mesh = new THREE.Mesh(geometry, material)
+        mesh.position.copy(position)
+        if (rotation) {
+            mesh.rotation.set(rotation.x, rotation.y, rotation.z)
+        }
+        mesh.castShadow = castShadow
+        mesh.receiveShadow = true
+        scene.add(mesh)
+        aisleModels.push(mesh)
+        return mesh
+    }
+
+    const railGeometry = new THREE.BoxGeometry(railLength, 0.08, 0.14)
+    const railMaterial = new THREE.MeshStandardMaterial({
+        color: 0x58677b,
+        roughness: 0.45,
+        metalness: 0.55
+    })
+
+    const railOffsets = [-0.36, 0.36]
+    railOffsets.forEach((offset) => {
+        const rail = new THREE.Mesh(railGeometry, railMaterial.clone())
+        rail.position.set(craneX, -0.42, aisleCenterZ + offset)
+        rail.castShadow = true
+        rail.receiveShadow = true
+        scene.add(rail)
+        aisleModels.push(rail)
+    })
+
+    const sleeperGeometry = new THREE.BoxGeometry(0.18, 0.05, 0.94)
+    for (let x = minX; x <= maxX; x += 1.2) {
+        const sleeper = new THREE.Mesh(sleeperGeometry, new THREE.MeshStandardMaterial({
+            color: 0x8f6a49,
+            roughness: 0.88,
+            metalness: 0.03
+        }))
+        sleeper.position.set(x, -0.46, aisleCenterZ)
+        sleeper.receiveShadow = true
+        scene.add(sleeper)
+        aisleModels.push(sleeper)
+    }
+
+    const machineYellow = new THREE.MeshStandardMaterial({
+        color: 0xe9a63d,
+        roughness: 0.42,
+        metalness: 0.32
+    })
+    const machineDark = new THREE.MeshStandardMaterial({
+        color: 0x334155,
+        roughness: 0.54,
+        metalness: 0.26
+    })
+    const machineSteel = new THREE.MeshStandardMaterial({
+        color: 0x7b8aa0,
+        roughness: 0.42,
+        metalness: 0.58
+    })
+    const guardRed = new THREE.MeshStandardMaterial({
+        color: 0xd05b45,
+        roughness: 0.5,
+        metalness: 0.18
+    })
+
+    addAisleMesh(
+        new THREE.BoxGeometry(1.34, 0.22, 1.12),
+        machineDark.clone(),
+        new THREE.Vector3(craneX, craneBaseY + 0.11, aisleCenterZ)
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(1.08, 0.18, 0.92),
+        machineYellow.clone(),
+        new THREE.Vector3(craneX, craneBaseY + 0.3, aisleCenterZ)
+    )
+
+    const wheelOffsets = [
+        [-0.42, -0.3],
+        [-0.42, 0.3],
+        [0.42, -0.3],
+        [0.42, 0.3]
+    ]
+    wheelOffsets.forEach((offset) => {
+        addAisleMesh(
+            new THREE.CylinderGeometry(0.11, 0.11, 0.12, 16),
+            machineSteel.clone(),
+            new THREE.Vector3(craneX + offset[0], craneBaseY + 0.09, aisleCenterZ + offset[1]),
+            new THREE.Vector3(Math.PI / 2, 0, 0)
+        )
+    })
+
+    const mastPositions = [-mastSpacing / 2, mastSpacing / 2]
+    mastPositions.forEach((offsetX) => {
+        addAisleMesh(
+            new THREE.BoxGeometry(mastThickness, craneHeight, mastThickness),
+            machineYellow.clone(),
+            new THREE.Vector3(craneX + offsetX, craneBaseY + craneHeight / 2, aisleCenterZ)
+        )
+    })
+
+    addAisleMesh(
+        new THREE.BoxGeometry(mastSpacing + 0.34, 0.18, 0.38),
+        machineDark.clone(),
+        new THREE.Vector3(craneX, craneBaseY + craneHeight - 0.12, aisleCenterZ)
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(mastSpacing + 0.18, 0.1, 0.28),
+        machineSteel.clone(),
+        new THREE.Vector3(craneX, craneBaseY + 0.92, aisleCenterZ)
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(mastSpacing + 0.14, 0.44, 0.72),
+        machineDark.clone(),
+        new THREE.Vector3(craneX, carriageY, aisleCenterZ)
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(mastSpacing + 0.28, 0.08, 0.82),
+        machineYellow.clone(),
+        new THREE.Vector3(craneX, carriageY + 0.22, aisleCenterZ)
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(0.26, 0.07, 0.46),
+        machineSteel.clone(),
+        new THREE.Vector3(craneX, carriageY - 0.04, aisleCenterZ)
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(0.12, 0.06, leftForkLength),
+        machineSteel.clone(),
+        new THREE.Vector3(craneX - 0.12, carriageY - 0.04, aisleCenterZ - (0.23 + leftForkLength / 2))
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(0.12, 0.06, rightForkLength),
+        machineSteel.clone(),
+        new THREE.Vector3(craneX + 0.12, carriageY - 0.04, aisleCenterZ + (0.23 + rightForkLength / 2))
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(0.4, 0.54, 0.26),
+        guardRed.clone(),
+        new THREE.Vector3(craneX, carriageY + 0.04, aisleCenterZ - (0.38 * forwardSign))
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(0.78, 0.05, 0.28),
+        machineDark.clone(),
+        new THREE.Vector3(craneX, craneBaseY + 0.66, aisleCenterZ - (0.22 * forwardSign))
+    )
+
+    addAisleMesh(
+        new THREE.BoxGeometry(0.72, 0.38, 0.04),
+        machineSteel.clone(),
+        new THREE.Vector3(craneX, craneBaseY + 0.86, aisleCenterZ - (0.36 * forwardSign))
+    )
+
+    const noseGeometry = new THREE.BoxGeometry(0.52, 0.28, 0.22)
+    addAisleMesh(
+        noseGeometry,
+        machineYellow.clone(),
+        new THREE.Vector3(craneX, carriageY + 0.02, aisleCenterZ + (0.48 * forwardSign))
+    )
+}
+
+function clearLocationMeshes() {
+    pickableMeshes = []
+    pointerDownHit = null
+
+    if (!locationMeshes) {
+        return
+    }
+
+    Object.values(locationMeshes).forEach((mesh) => {
+        scene.remove(mesh)
+        mesh.geometry.dispose()
+        mesh.material.dispose()
+    })
+
+    locationMeshes = null
+}
+
+function updateInstanceVisual(instanceId, location) {
+    if (!locationMeshes) {
+        return
+    }
+
+    const layoutInfo = getLayoutInfo()
+    const { x, y, z } = getLocationWorldPosition(location, layoutInfo)
+    const postTopY = y + 1.54
+    const postBottomY = -0.5
+    const postHeight = postTopY - postBottomY
+    const postCenterY = postBottomY + (postHeight / 2)
+    const showPallet = hasPallet(location)
+    const showCargo = hasCargo(location)
+    const dummy = new THREE.Object3D()
+    const shelfColor = new THREE.Color(getShelfAccentColor(location))
+    const palletColor = new THREE.Color(getPalletColor(location))
+    const cargoColor = new THREE.Color(getCargoColor(location))
+    const cargoLidColor = new THREE.Color(getCargoLidColor(location))
+    const postOffsets = [
+        [-0.76, -0.76],
+        [-0.76, 0.76],
+        [0.76, -0.76],
+        [0.76, 0.76]
+    ]
+
+    dummy.position.set(x, y + 0.12, z)
+    dummy.rotation.set(0, 0, 0)
+    dummy.scale.set(1, 1, 1)
+    dummy.updateMatrix()
+    locationMeshes.base.setMatrixAt(instanceId, dummy.matrix)
+
+    locationMeshes.base.setColorAt(instanceId, shelfColor)
+
+    dummy.position.set(x, y + 0.34, z)
+    dummy.scale.set(showPallet ? 1 : 0.001, showPallet ? 1 : 0.001, showPallet ? 1 : 0.001)
+    dummy.updateMatrix()
+    locationMeshes.pallet.setMatrixAt(instanceId, dummy.matrix)
+    locationMeshes.pallet.setColorAt(instanceId, palletColor)
+
+    dummy.position.set(x, y + 0.86, z)
+    dummy.scale.set(showCargo ? 1 : 0.001, showCargo ? 1 : 0.001, showCargo ? 1 : 0.001)
+    dummy.updateMatrix()
+    locationMeshes.cargoBody.setMatrixAt(instanceId, dummy.matrix)
+    locationMeshes.cargoBody.setColorAt(instanceId, cargoColor)
+
+    dummy.position.set(x, y + 1.25, z)
+    dummy.scale.set(showCargo ? 1 : 0.001, showCargo ? 1 : 0.001, showCargo ? 1 : 0.001)
+    dummy.updateMatrix()
+    locationMeshes.cargoLid.setMatrixAt(instanceId, dummy.matrix)
+    locationMeshes.cargoLid.setColorAt(instanceId, cargoLidColor)
+
+    postOffsets.forEach((offset, postIndex) => {
+        dummy.position.set(x + offset[0], postCenterY, z + offset[1])
+        dummy.rotation.set(0, 0, 0)
+        dummy.scale.set(1, postHeight, 1)
+        dummy.updateMatrix()
+        locationMeshes.posts.setMatrixAt(instanceId * 4 + postIndex, dummy.matrix)
+    })
+
+    locationMeshes.base.instanceMatrix.needsUpdate = true
+    locationMeshes.pallet.instanceMatrix.needsUpdate = true
+    locationMeshes.cargoBody.instanceMatrix.needsUpdate = true
+    locationMeshes.cargoLid.instanceMatrix.needsUpdate = true
+    locationMeshes.posts.instanceMatrix.needsUpdate = true
+    locationMeshes.base.instanceColor.needsUpdate = true
+    locationMeshes.pallet.instanceColor.needsUpdate = true
+    locationMeshes.cargoBody.instanceColor.needsUpdate = true
+    locationMeshes.cargoLid.instanceColor.needsUpdate = true
+}
+
+function syncLocationUpdate(list, update) {
+    const index = list.findIndex((item) => item.locationId === update.locationId)
+    if (index === -1) {
+        return
+    }
+
+    list[index] = {
+        ...list[index],
+        stockQuantity: update.stockQuantity,
+        stockStatus: update.stockStatus,
+        palletCode: update.palletCode,
+        locationStatus: update.locationStatus,
+        details: update.details && update.details.length > 0 ? update.details : list[index].details
+    }
+}
+
+function initSignalR() {
+    proxy.http.post('api/User/GetCurrentUserInfo').then((result) => {
+        connection = new signalR.HubConnectionBuilder()
+            .withAutomaticReconnect()
+            .withUrl(`${proxy.http.ipAddress}stockHub?userName=${result.data.userName}`)
+            .build()
+
+        connection.start().catch((error) => console.log('SignalR杩炴帴澶辫触:', error))
+
+        connection.on('StockUpdated', (update) => {
+            console.log("馃殌 ~ initSignalR ~ update:", update)
+            syncLocationUpdate(locationData, update)
+            syncLocationUpdate(originalLocationData, update)
+            const nextLocation = originalLocationData.find((item) => item.locationId === update.locationId)
+            const instanceId = locationIdToInstanceId.get(update.locationId)
+            const shouldRender = nextLocation && matchesCurrentFilters(nextLocation)
+
+            if (!nextLocation) {
+                return
+            }
+
+            if (instanceId !== undefined && !shouldRender) {
+                renderLocations()
+                return
+            }
+
+            if (instanceId === undefined && shouldRender) {
+                renderLocations()
+                return
+            }
+
+            if (instanceId !== undefined && shouldRender) {
+                renderedLocations[instanceId] = nextLocation
+                updateInstanceVisual(instanceId, nextLocation)
+            }
+        })
+    })
+}
+
+async function loadWarehouseList() {
+    try {
+        const res = await proxy.http.get('/api/Warehouse/GetAll')
+        if (res.status && res.data) {
+            warehouseList.value = res.data
+            if (res.data.length > 0) {
+                activeWarehouse.value = res.data[0].warehouseId || res.data[0].id
+                await loadWarehouseData(activeWarehouse.value)
+            }
+        }
+    } catch (error) {
+        console.error('鍔犺浇浠撳簱鍒楄〃澶辫触', error)
+    }
+}
+
+async function loadWarehouseData(warehouseId) {
+    try {
+        const res = await proxy.http.get(`/api/StockInfo/Get3DLayout?warehouseId=${warehouseId}`)
+        if (res.status && res.data) {
+            const data = res.data
+            originalLocationData = data.locations || []
+            locationData = [...originalLocationData]
+            materielCodeList.value = data.materielCodeList || []
+            batchNoList.value = data.batchNoList || []
+            renderLocations()
+        }
+    } catch (error) {
+        console.error('鍔犺浇璐т綅鏁版嵁澶辫触', error)
+    }
+}
+
+function initThreeJS() {
+    if (!canvasContainer.value) {
+        return
+    }
+
+    const width = canvasContainer.value.clientWidth
+    const height = canvasContainer.value.clientHeight
+
+    scene = new THREE.Scene()
+    scene.background = createBackgroundTexture()
+    scene.fog = new THREE.Fog(0xdeE7f1, 36, 92)
+
+    camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000)
+    camera.position.set(22, 20, 22)
+    camera.lookAt(0, 0, 0)
+
+    renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
+    renderer.setPixelRatio(window.devicePixelRatio)
+    renderer.setSize(width, height)
+    renderer.shadowMap.enabled = true
+    renderer.shadowMap.type = THREE.PCFSoftShadowMap
+    renderer.outputColorSpace = THREE.SRGBColorSpace
+    canvasContainer.value.appendChild(renderer.domElement)
+    canvasContainer.value.addEventListener('contextmenu', preventCanvasContextMenu)
+    canvasContainer.value.addEventListener('mousedown', preventRightMouseDefault)
+    renderer.domElement.addEventListener('contextmenu', preventCanvasContextMenu)
+    renderer.domElement.addEventListener('mousedown', preventRightMouseDefault)
+
+    controls = new OrbitControls(camera, renderer.domElement)
+    controls.enableDamping = true
+    controls.dampingFactor = 0.06
+    controls.enablePan = true
+    controls.screenSpacePanning = true
+    controls.zoomToCursor = true
+    controls.panSpeed = 1.15
+    controls.rotateSpeed = 0.75
+    controls.minDistance = 2
+    controls.maxDistance = 160
+    controls.maxPolarAngle = Math.PI
+    controls.touches = {
+        ONE: THREE.TOUCH.PAN,
+        TWO: THREE.TOUCH.DOLLY_ROTATE
+    }
+    updateControlMouseBinding()
+
+    const hemisphereLight = new THREE.HemisphereLight(0xf8fbff, 0xc8d0d9, 1.15)
+    scene.add(hemisphereLight)
+
+    const keyLight = new THREE.DirectionalLight(0xffffff, 1.25)
+    keyLight.position.set(24, 36, 18)
+    keyLight.castShadow = true
+    keyLight.shadow.mapSize.width = 2048
+    keyLight.shadow.mapSize.height = 2048
+    keyLight.shadow.camera.near = 1
+    keyLight.shadow.camera.far = 120
+    keyLight.shadow.camera.left = -44
+    keyLight.shadow.camera.right = 44
+    keyLight.shadow.camera.top = 44
+    keyLight.shadow.camera.bottom = -44
+    scene.add(keyLight)
+
+    const fillLight = new THREE.DirectionalLight(0xd9ebff, 0.55)
+    fillLight.position.set(-18, 18, -24)
+    scene.add(fillLight)
+
+    const rimLight = new THREE.DirectionalLight(0xfff3d6, 0.35)
+    rimLight.position.set(0, 16, 30)
+    scene.add(rimLight)
+
+    const groundGeometry = new THREE.PlaneGeometry(120, 120)
+    const groundMaterial = new THREE.MeshStandardMaterial({
+        color: 0xe4eaf1,
+        map: createFloorTexture(),
+        roughness: 0.94,
+        metalness: 0.02
+    })
+    const ground = new THREE.Mesh(groundGeometry, groundMaterial)
+    ground.rotation.x = -Math.PI / 2
+    ground.position.y = -0.5
+    ground.receiveShadow = true
+    scene.add(ground)
+
+    const gridHelper = new THREE.GridHelper(100, 20, 0x8da0b8, 0xc8d2de)
+    gridHelper.position.y = -0.49
+    gridHelper.material.transparent = true
+    gridHelper.material.opacity = 0.18
+    scene.add(gridHelper)
+
+    raycaster = new THREE.Raycaster()
+    mouse = new THREE.Vector2()
+
+    const outlineGeometry = new THREE.BoxGeometry(2.3, 2.1, 2.3)
+    const outlineMaterial = new THREE.MeshBasicMaterial({
+        color: 0xffc857,
+        wireframe: true,
+        transparent: true,
+        opacity: 0.92
+    })
+    selectionOutline = new THREE.Mesh(outlineGeometry, outlineMaterial)
+    selectionOutline.visible = false
+    scene.add(selectionOutline)
+
+    canvasContainer.value.addEventListener('pointerdown', onCanvasPointerDown)
+    canvasContainer.value.addEventListener('pointerup', onCanvasPointerUp)
+
+    animate()
+}
+
+function renderLocations() {
+    if (!scene) {
+        return
+    }
+
+    if (originalLocationData.length === 0) {
+        clearLocationMeshes()
+        clearAisleModels()
+        renderedLocations = []
+        if (selectionOutline) {
+            selectionOutline.visible = false
+        }
+        return
+    }
+
+    clearLocationMeshes()
+    renderedLocations = getFilteredLocations()
+    locationIdToInstanceId.clear()
+
+    if (renderedLocations.length === 0) {
+        clearAisleModels()
+        if (selectionOutline) {
+            selectionOutline.visible = false
+        }
+        return
+    }
+
+    const layoutInfo = getLayoutInfo()
+    renderAisleEquipment(layoutInfo)
+
+    const baseGeometry = new THREE.BoxGeometry(1.76, 0.24, 1.76)
+    const palletGeometry = new THREE.BoxGeometry(1.42, 0.16, 1.42)
+    const cargoBodyGeometry = new THREE.BoxGeometry(1.08, 0.7, 1.08)
+    const cargoLidGeometry = new THREE.BoxGeometry(1.16, 0.08, 1.16)
+    const postGeometry = new THREE.BoxGeometry(0.1, 1, 0.1)
+    locationMeshes = {
+        base: new THREE.InstancedMesh(baseGeometry, new THREE.MeshPhysicalMaterial({
+            color: 0xffffff,
+            roughness: 0.46,
+            metalness: 0.28,
+            clearcoat: 0.18
+        }), renderedLocations.length),
+        pallet: new THREE.InstancedMesh(palletGeometry, new THREE.MeshStandardMaterial({
+            color: 0xffffff,
+            roughness: 0.84,
+            metalness: 0.04
+        }), renderedLocations.length),
+        cargoBody: new THREE.InstancedMesh(cargoBodyGeometry, new THREE.MeshPhysicalMaterial({
+            color: 0xffffff,
+            roughness: 0.5,
+            metalness: 0.05,
+            clearcoat: 0.22,
+            clearcoatRoughness: 0.28
+        }), renderedLocations.length),
+        cargoLid: new THREE.InstancedMesh(cargoLidGeometry, new THREE.MeshStandardMaterial({
+            color: 0xffffff,
+            roughness: 0.44,
+            metalness: 0.08
+        }), renderedLocations.length),
+        posts: new THREE.InstancedMesh(postGeometry, new THREE.MeshStandardMaterial({
+            color: POST_COLOR,
+            roughness: 0.48,
+            metalness: 0.24
+        }), renderedLocations.length * 4)
+    }
+
+    Object.values(locationMeshes).forEach((mesh) => {
+        mesh.castShadow = true
+        mesh.receiveShadow = true
+    })
+
+    pickableMeshes = [locationMeshes.base, locationMeshes.pallet, locationMeshes.cargoBody, locationMeshes.cargoLid]
+
+    renderedLocations.forEach((location, index) => {
+        updateInstanceVisual(index, location)
+        locationIdToInstanceId.set(location.locationId, index)
+    })
+
+    Object.values(locationMeshes).forEach((mesh) => {
+        mesh.instanceMatrix.needsUpdate = true
+        if (mesh.instanceColor) {
+            mesh.instanceColor.needsUpdate = true
+        }
+        scene.add(mesh)
+    })
+}
+
+function animate() {
+    animationId = requestAnimationFrame(animate)
+    if (controls) {
+        controls.update()
+    }
+    if (selectionOutline && selectionOutline.visible) {
+        selectionOutline.material.opacity = 0.74 + Math.sin(Date.now() * 0.006) * 0.15
+    }
+    if (renderer && scene && camera) {
+        renderer.render(scene, camera)
+    }
+}
+
 function focusCamera(location) {
-  if (!camera || !controls) return
-
-  const targetX = (location.column - 1) * 2
-  const targetY = location.layer * 1.5
-  const targetZ = (location.row - 1) * 2
-
-  const offsetX = 5
-  const offsetY = 5
-  const offsetZ = 5
-
-  const targetPosition = new THREE.Vector3(targetX + offsetX, targetY + offsetY, targetZ + offsetZ)
-
-  // 浣跨敤 lerp 骞虫粦绉诲姩
-  const startPosition = camera.position.clone()
-  const duration = 500
-  const startTime = Date.now()
-
-  function lerpMove() {
-    const elapsed = Date.now() - startTime
-    const t = Math.min(elapsed / duration, 1)
-    const easeT = 1 - Math.pow(1 - t, 3) // easeOutCubic
-
-    camera.position.lerpVectors(startPosition, targetPosition, easeT)
-    controls.target.set(targetX, targetY, targetZ)
-
-    if (t < 1) {
-      requestAnimationFrame(lerpMove)
+    if (!camera || !controls) {
+        return
     }
-  }
 
-  lerpMove()
+    const { x: targetX, y, z: targetZ } = getLocationWorldPosition(location)
+    const targetY = y + 0.9
+    const currentDirection = camera.position.clone().sub(controls.target).normalize()
+    const fallbackDirection = new THREE.Vector3(1, 0.85, 1).normalize()
+    const focusDirection = currentDirection.lengthSq() > 0 ? currentDirection : fallbackDirection
+    const targetPosition = new THREE.Vector3(targetX, targetY, targetZ).add(focusDirection.multiplyScalar(8.5))
+    const startPosition = camera.position.clone()
+    const duration = 500
+    const startTime = Date.now()
+
+    if (selectionOutline) {
+        selectionOutline.position.set(targetX, location.layer * 1.5 + 0.9, targetZ)
+        selectionOutline.visible = true
+    }
+
+    function lerpMove() {
+        const elapsed = Date.now() - startTime
+        const progress = Math.min(elapsed / duration, 1)
+        const easedProgress = 1 - Math.pow(1 - progress, 3)
+
+        camera.position.lerpVectors(startPosition, targetPosition, easedProgress)
+        controls.target.set(targetX, targetY, targetZ)
+
+        if (progress < 1) {
+            requestAnimationFrame(lerpMove)
+        }
+    }
+
+    lerpMove()
 }
 
-// 閲嶇疆鐩告満
+function getCanvasHit(event) {
+    if (!canvasContainer.value || pickableMeshes.length === 0) {
+        return null
+    }
+
+    const rect = canvasContainer.value.getBoundingClientRect()
+    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
+    mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
+
+    raycaster.setFromCamera(mouse, camera)
+    const intersects = raycaster.intersectObjects(pickableMeshes, false)
+
+    if (intersects.length === 0) {
+        return null
+    }
+
+    const instanceId = intersects[0].instanceId
+    if (instanceId === undefined || instanceId >= renderedLocations.length) {
+        return null
+    }
+
+    return {
+        instanceId,
+        location: renderedLocations[instanceId]
+    }
+}
+
+function onCanvasPointerDown(event) {
+    updateControlMouseBinding(event.shiftKey)
+
+    if (event.button !== 0) {
+        pointerDownHit = null
+        pointerDownPosition = null
+        return
+    }
+
+    if (event.shiftKey) {
+        pointerDownHit = null
+        pointerDownPosition = null
+        return
+    }
+
+    const hit = getCanvasHit(event)
+    pointerDownHit = hit
+    pointerDownPosition = {
+        x: event.clientX,
+        y: event.clientY
+    }
+}
+
+function onCanvasPointerUp(event) {
+    updateControlMouseBinding(event.shiftKey)
+
+    if (event.button !== 0) {
+        pointerDownHit = null
+        pointerDownPosition = null
+        return
+    }
+
+    if (event.shiftKey) {
+        pointerDownHit = null
+        pointerDownPosition = null
+        return
+    }
+
+    const hit = getCanvasHit(event)
+    if (!pointerDownHit || !hit || !pointerDownPosition) {
+        pointerDownHit = null
+        pointerDownPosition = null
+        return
+    }
+
+    const moveDistance = Math.hypot(event.clientX - pointerDownPosition.x, event.clientY - pointerDownPosition.y)
+    const isSameLocation = pointerDownHit.instanceId === hit.instanceId
+
+    if (!isSameLocation || moveDistance > 6) {
+        pointerDownHit = null
+        pointerDownPosition = null
+        return
+    }
+
+    selectedLocation.value = hit.location
+    detailDialogVisible.value = true
+    focusCamera(selectedLocation.value)
+
+    pointerDownHit = null
+    pointerDownPosition = null
+}
+
 function resetCamera() {
-  if (!camera || !controls) return
-  camera.position.set(20, 20, 20)
-  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)
+    if (!camera || !controls) {
+        return
     }
-  } finally {
-    refreshing.value = false
-  }
+    camera.position.set(22, 20, 22)
+    controls.target.set(0, 0, 0)
+    if (selectionOutline) {
+        selectionOutline.visible = false
+    }
 }
 
-// 浠撳簱鍒囨崲
+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)
+    await loadWarehouseData(warehouseId)
 }
 
-// 鐩戝惉绛涢�夊彉鍖�
-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() {
-  if (!canvasContainer.value || !camera || !renderer) return
-  const width = canvasContainer.value.clientWidth
-  const height = canvasContainer.value.clientHeight
-  camera.aspect = width / height
-  camera.updateProjectionMatrix()
-  renderer.setSize(width, height)
+    if (!canvasContainer.value || !camera || !renderer) {
+        return
+    }
+
+    const width = canvasContainer.value.clientWidth
+    const height = canvasContainer.value.clientHeight
+    camera.aspect = width / height
+    camera.updateProjectionMatrix()
+    renderer.setSize(width, height)
 }
 
-// 缁勪欢鎸傝浇
-onMounted(() => {
-  initThreeJS()
-  loadWarehouseList()
-  initSignalR()
-  window.addEventListener('resize', onWindowResize)
+watch(filterStockStatus, async () => {
+    await nextTick()
+    renderLocations()
 })
 
-// 缁勪欢鍗歌浇
+watch(filterMaterielCode, async () => {
+    await nextTick()
+    renderLocations()
+})
+
+watch(filterBatchNo, async () => {
+    await nextTick()
+    renderLocations()
+})
+
+onMounted(() => {
+    initThreeJS()
+    loadWarehouseList()
+    initSignalR()
+    window.addEventListener('keydown', onKeyDown)
+    window.addEventListener('keyup', onKeyUp)
+    window.addEventListener('blur', onWindowBlur)
+    window.addEventListener('resize', onWindowResize)
+})
+
 onUnmounted(() => {
-  if (animationId) {
-    cancelAnimationFrame(animationId)
-  }
-  if (canvasContainer.value) {
-    canvasContainer.value.removeEventListener('mousedown', onCanvasClick)
-  }
-  window.removeEventListener('resize', onWindowResize)
-  if (renderer) {
-    renderer.dispose()
-  }
-  if (connection) {
-    connection.stop()
-  }
+    if (animationId) {
+        cancelAnimationFrame(animationId)
+    }
+    if (canvasContainer.value) {
+        canvasContainer.value.removeEventListener('contextmenu', preventCanvasContextMenu)
+        canvasContainer.value.removeEventListener('mousedown', preventRightMouseDefault)
+        canvasContainer.value.removeEventListener('pointerdown', onCanvasPointerDown)
+        canvasContainer.value.removeEventListener('pointerup', onCanvasPointerUp)
+    }
+    window.removeEventListener('keydown', onKeyDown)
+    window.removeEventListener('keyup', onKeyUp)
+    window.removeEventListener('blur', onWindowBlur)
+    window.removeEventListener('resize', onWindowResize)
+    clearLocationMeshes()
+    clearAisleModels()
+    if (selectionOutline) {
+        scene.remove(selectionOutline)
+        selectionOutline.geometry.dispose()
+        selectionOutline.material.dispose()
+    }
+    if (renderer) {
+        renderer.domElement.removeEventListener('contextmenu', preventCanvasContextMenu)
+        renderer.domElement.removeEventListener('mousedown', preventRightMouseDefault)
+        renderer.dispose()
+    }
+    if (connection) {
+        connection.stop()
+    }
 })
 </script>
 
 <style scoped>
 .stock-chat-container {
-  width: 100%;
-  height: calc(100vh - 120px);
-  position: relative;
-  overflow: visible;
+    width: 100%;
+    height: calc(100vh - 120px);
+    position: relative;
+    overflow: visible;
+    background: linear-gradient(180deg, #f6f9fc 0%, #edf2f7 100%);
+    border-radius: 12px;
 }
+
 .toolbar {
-  position: absolute;
-  top: 60px;
-  left: 20px;
-  z-index: 10;
-  display: flex;
-  gap: 10px;
-  background: rgba(255, 255, 255, 0.9);
-  padding: 10px;
-  border-radius: 4px;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+    position: absolute;
+    top: 60px;
+    left: 20px;
+    z-index: 10;
+    display: flex;
+    gap: 10px;
+    background: rgba(255, 255, 255, 0.82);
+    padding: 12px;
+    border-radius: 12px;
+    border: 1px solid rgba(148, 163, 184, 0.18);
+    box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
+    backdrop-filter: blur(14px);
 }
+
 .canvas-container {
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    border-radius: 12px;
 }
+
 .legend {
-  position: absolute;
-  bottom: 20px;
-  right: 20px;
-  background: rgba(0, 0, 0, 0.7);
-  padding: 10px;
-  border-radius: 4px;
-  color: white;
-  z-index: 10;
-  max-height: 400px;
-  overflow-y: auto;
+    position: absolute;
+    bottom: 20px;
+    right: 20px;
+    z-index: 10;
+    width: 220px;
+    background: rgba(15, 23, 42, 0.78);
+    padding: 14px 16px;
+    border-radius: 14px;
+    color: #fff;
+    box-shadow: 0 16px 36px rgba(15, 23, 42, 0.24);
+    backdrop-filter: blur(10px);
+    max-height: 460px;
+    overflow-y: auto;
 }
+
 .legend-title {
-  font-weight: bold;
-  font-size: 13px;
-  margin-bottom: 6px;
-  color: #fff;
+    font-weight: 700;
+    font-size: 14px;
+    margin-bottom: 8px;
 }
+
+.legend-subtitle {
+    font-size: 12px;
+    color: rgba(226, 232, 240, 0.8);
+    margin-bottom: 6px;
+}
+
 .legend-divider {
-  height: 1px;
-  background: rgba(255, 255, 255, 0.3);
-  margin: 8px 0;
+    height: 1px;
+    background: rgba(255, 255, 255, 0.16);
+    margin: 10px 0;
 }
+
 .legend-item {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  margin-bottom: 4px;
-  font-size: 12px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 6px;
+    font-size: 12px;
 }
+
+.legend-item--shape {
+    color: rgba(255, 255, 255, 0.92);
+}
+
 .color-box {
-  width: 16px;
-  height: 16px;
-  border-radius: 2px;
-  flex-shrink: 0;
+    width: 16px;
+    height: 16px;
+    border-radius: 5px;
+    flex-shrink: 0;
+    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
 }
+
+.shape-pill {
+    display: inline-block;
+    width: 24px;
+    height: 12px;
+    border-radius: 6px;
+    position: relative;
+    flex-shrink: 0;
+}
+
+.shape-pill::after {
+    content: '';
+    position: absolute;
+    left: 2px;
+    right: 2px;
+    bottom: -4px;
+    height: 4px;
+    border-radius: 4px;
+}
+
+.shape-pill--empty {
+    width: 26px;
+    height: 6px;
+    border-radius: 4px;
+    background: linear-gradient(180deg, #b8c6d8 0%, #7f90a8 100%);
+}
+
+.shape-pill--empty::after {
+    display: none;
+}
+
+.shape-pill--pallet {
+    background: linear-gradient(180deg, #e7c997 0%, #bf8851 100%);
+}
+
+.shape-pill--pallet::after {
+    background: #7f8fa5;
+}
+
+.shape-pill--cargo {
+    background: linear-gradient(180deg, #7ab6ff 0%, #2b7fff 100%);
+    height: 14px;
+}
+
+.shape-pill--cargo::after {
+    background: #bf8851;
+}
+
 .detail-content {
-  padding: 20px;
+    padding: 20px;
 }
+
 .detail-table {
-  margin-top: 20px;
+    margin-top: 20px;
 }
+
 .detail-table h4 {
-  margin-bottom: 10px;
-  color: #303133;
+    margin-bottom: 10px;
+    color: #303133;
 }
+
 .no-detail {
-  margin-top: 20px;
+    margin-top: 20px;
+}
+
+:deep(.el-tabs__nav-wrap::after) {
+    background-color: rgba(148, 163, 184, 0.22);
 }
 
 :deep(.el-drawer__body) {
-  padding: 20px;
+    padding: 20px;
 }
 </style>
diff --git a/Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/CodeChunks.db b/Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/CodeChunks.db
index 607e8a1..0ba89cc 100644
--- a/Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/CodeChunks.db
+++ b/Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/CodeChunks.db
Binary files differ
diff --git a/Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/SemanticSymbols.db b/Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/SemanticSymbols.db
index d72e7c0..aaf963f 100644
--- a/Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/SemanticSymbols.db
+++ b/Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/CopilotIndices/18.0.988.22099/SemanticSymbols.db
Binary files differ
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/LocationInfoService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/LocationInfoService.cs
index af5cc9a..aff926c 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/LocationInfoService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/LocationInfoService.cs
@@ -11,6 +11,7 @@
 using WIDESEA_DTO.Basic;
 using WIDESEA_DTO.Task;
 using WIDESEA_IBasicService;
+using WIDESEA_IRecordService;
 using WIDESEA_Model.Models;
 
 namespace WIDESEA_BasicService
@@ -23,6 +24,7 @@
         private readonly IMapper _mapper;
         private readonly IRepository<Dt_Task> _taskRepository;
         private readonly IRepository<Dt_StockInfo> _stockInfoRepository;
+        private readonly IRecordService _recordService;
 
         /// <summary>
         /// 鏋勯�犲嚱鏁�
@@ -34,11 +36,13 @@
             IRepository<Dt_LocationInfo> baseDal,
             IRepository<Dt_Task> taskRepository,
             IRepository<Dt_StockInfo> stockInfoRepository,
-            IMapper mapper) : base(baseDal)
+            IMapper mapper,
+            IRecordService recordService) : base(baseDal)
         {
             _taskRepository = taskRepository;
             _stockInfoRepository = stockInfoRepository;
             _mapper = mapper;
+            _recordService = recordService;
         }
 
         /// <summary>
@@ -172,7 +176,48 @@
         /// <returns>鏇存柊鏄惁鎴愬姛</returns>
         public async Task<bool> UpdateLocationInfoAsync(Dt_LocationInfo locationInfo)
         {
-            return await BaseDal.UpdateDataAsync(locationInfo);
+            var beforeLocation = await BaseDal.QueryFirstAsync(x => x.Id == locationInfo.Id);
+            var result = await BaseDal.UpdateDataAsync(locationInfo);
+            if (!result)
+                return false;
+
+            return beforeLocation == null
+                || await _recordService.AddLocationChangeRecordAsync(beforeLocation, locationInfo, LocationChangeType.HandUpdate, remark: "璐т綅鏇存柊");
+        }
+
+        public override WebResponseContent UpdateData(Dt_LocationInfo entity)
+        {
+            var beforeLocation = BaseDal.QueryFirst(x => x.Id == entity.Id);
+            var result = base.UpdateData(entity);
+            if (!result.Status || beforeLocation == null)
+                return result;
+
+            var saveRecordResult = _recordService.AddLocationChangeRecordAsync(beforeLocation, entity, LocationChangeType.HandUpdate, remark: "璐т綅鏇存柊").GetAwaiter().GetResult();
+            return saveRecordResult ? result : WebResponseContent.Instance.Error("璐т綅鐘舵�佸彉鏇磋褰曚繚瀛樺け璐�");
+        }
+
+        public override WebResponseContent UpdateData(List<Dt_LocationInfo> entities)
+        {
+            var beforeLocations = entities
+                .Select(entity => BaseDal.QueryFirst(x => x.Id == entity.Id))
+                .Where(location => location != null)
+                .ToDictionary(location => location!.Id, location => location!);
+
+            var result = base.UpdateData(entities);
+            if (!result.Status)
+                return result;
+
+            foreach (var entity in entities)
+            {
+                if (!beforeLocations.TryGetValue(entity.Id, out var beforeLocation))
+                    continue;
+
+                var saveRecordResult = _recordService.AddLocationChangeRecordAsync(beforeLocation, entity, LocationChangeType.HandUpdate, remark: "鎵归噺璐т綅鏇存柊").GetAwaiter().GetResult();
+                if (!saveRecordResult)
+                    return WebResponseContent.Instance.Error("璐т綅鐘舵�佸彉鏇磋褰曚繚瀛樺け璐�");
+            }
+
+            return result;
         }
 
         /// <summary>
@@ -281,6 +326,9 @@
             };
 
             var createdTask = await _taskRepository.Db.Insertable(newTransferTask).ExecuteReturnEntityAsync();
+            var beforeStock = CloneStockSnapshot(stockInfo);
+            var beforeSourceLocation = stockInfo.LocationDetails == null ? null : CloneLocationSnapshot(stockInfo.LocationDetails);
+            var beforeTargetLocation = CloneLocationSnapshot(emptyLocation);
 
             // 鍒涘缓绉诲簱浠诲姟鍚庯紝绔嬪嵆閿佸畾搴撳瓨鍜岀浉鍏宠揣浣嶏紝閬垮厤骞跺彂閲嶅鍒嗛厤
             stockInfo.StockStatus = StockStatusEmun.绉诲簱閿佸畾.GetHashCode();
@@ -302,9 +350,104 @@
                 throw new Exception("鍒涘缓绉诲簱浠诲姟鍚庢洿鏂板簱瀛樼姸鎬佹垨璐т綅鐘舵�佸け璐�");
             }
 
+            var saveStockRecordResult = await _recordService.AddStockChangeRecordAsync(
+                beforeStock,
+                stockInfo,
+                StockChangeTypeEnum.Relocation,
+                createdTask.TaskNum,
+                createdTask.OrderNo,
+                "绉诲簱浠诲姟棰勫崰搴撳瓨");
+            if (!saveStockRecordResult)
+            {
+                throw new Exception("鍒涘缓绉诲簱浠诲姟鍚庤褰曞簱瀛樺彉鏇村け璐�");
+            }
+
+            if (beforeSourceLocation != null && stockInfo.LocationDetails != null)
+            {
+                var saveSourceLocationRecordResult = await _recordService.AddLocationChangeRecordAsync(
+                    beforeSourceLocation,
+                    stockInfo.LocationDetails,
+                    LocationChangeType.RelocationAssignLocation,
+                    createdTask.TaskNum,
+                    createdTask.OrderNo,
+                    null,
+                    "绉诲簱浠诲姟閿佸畾婧愯揣浣�");
+                if (!saveSourceLocationRecordResult)
+                {
+                    throw new Exception("鍒涘缓绉诲簱浠诲姟鍚庤褰曟簮璐т綅鍙樻洿澶辫触");
+                }
+            }
+
+            var saveTargetLocationRecordResult = await _recordService.AddLocationChangeRecordAsync(
+                beforeTargetLocation,
+                emptyLocation,
+                LocationChangeType.RelocationAssignLocation,
+                createdTask.TaskNum,
+                createdTask.OrderNo,
+                null,
+                "绉诲簱浠诲姟閿佸畾鐩爣璐т綅");
+            if (!saveTargetLocationRecordResult)
+            {
+                throw new Exception("鍒涘缓绉诲簱浠诲姟鍚庤褰曠洰鏍囪揣浣嶅彉鏇村け璐�");
+            }
+
             return createdTask;
         }
 
+        private static Dt_LocationInfo CloneLocationSnapshot(Dt_LocationInfo location)
+        {
+            return new Dt_LocationInfo
+            {
+                Id = location.Id,
+                WarehouseId = location.WarehouseId,
+                LocationCode = location.LocationCode,
+                LocationName = location.LocationName,
+                RoadwayNo = location.RoadwayNo,
+                Row = location.Row,
+                Column = location.Column,
+                Layer = location.Layer,
+                Depth = location.Depth,
+                LocationType = location.LocationType,
+                LocationStatus = location.LocationStatus,
+                EnableStatus = location.EnableStatus,
+                Remark = location.Remark
+            };
+        }
+
+        private static Dt_StockInfo CloneStockSnapshot(Dt_StockInfo stockInfo)
+        {
+            return new Dt_StockInfo
+            {
+                Id = stockInfo.Id,
+                PalletCode = stockInfo.PalletCode,
+                PalletType = stockInfo.PalletType,
+                LocationId = stockInfo.LocationId,
+                LocationCode = stockInfo.LocationCode,
+                WarehouseId = stockInfo.WarehouseId,
+                StockStatus = stockInfo.StockStatus,
+                Remark = stockInfo.Remark,
+                OutboundDate = stockInfo.OutboundDate,
+                Details = stockInfo.Details?.Select(detail => new Dt_StockInfoDetail
+                {
+                    Id = detail.Id,
+                    StockId = detail.StockId,
+                    MaterielCode = detail.MaterielCode,
+                    MaterielName = detail.MaterielName,
+                    OrderNo = detail.OrderNo,
+                    BatchNo = detail.BatchNo,
+                    ProductionDate = detail.ProductionDate,
+                    EffectiveDate = detail.EffectiveDate,
+                    SerialNumber = detail.SerialNumber,
+                    StockQuantity = detail.StockQuantity,
+                    OutboundQuantity = detail.OutboundQuantity,
+                    Status = detail.Status,
+                    Unit = detail.Unit,
+                    InboundOrderRowNo = detail.InboundOrderRowNo,
+                    Remark = detail.Remark
+                }).ToList()
+            };
+        }
+
         /// <summary>
         /// 妫�鏌ヨ揣浣嶆槸鍚﹂渶瑕佺Щ搴�
         /// </summary>
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
index a2aaed2..69d869c 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
@@ -1,3 +1,5 @@
+using SqlSugar;
+
 namespace WIDESEA_DTO.Stock
 {
     /// <summary>
@@ -117,6 +119,11 @@
         public string? BatchNo { get; set; }
 
         /// <summary>
+        /// 鍑哄簱鏃ユ湡
+        /// </summary>
+        public DateTime OutboundDate { get; set; }
+
+        /// <summary>
         /// 搴撳瓨鏄庣粏鍒楄〃
         /// </summary>
         public List<StockDetailItemDTO> Details { get; set; } = new();
@@ -137,5 +144,7 @@
         public string? EffectiveDate { get; set; }
         public string? OrderNo { get; set; }
         public int Status { get; set; }
+        public string SerialNumber { get; set; }
+        public int InboundOrderRowNo { get; set; }
     }
 }
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/ILocationStatusChangeRecordService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/ILocationStatusChangeRecordService.cs
index e89344c..7c441b9 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/ILocationStatusChangeRecordService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/ILocationStatusChangeRecordService.cs
@@ -1,4 +1,5 @@
-锘縰sing WIDESEA_Core.BaseRepository;
+using WIDESEA_Common.LocationEnum;
+using WIDESEA_Core.BaseRepository;
 using WIDESEA_Core.BaseServices;
 using WIDESEA_Model.Models;
 
@@ -13,5 +14,17 @@
         /// 鑾峰彇璐т綅鐘舵�佸彉鏇磋褰曚粨鍌ㄦ帴鍙�
         /// </summary>
         IRepository<Dt_LocationStatusChangeRecord> Repository { get; }
+
+        /// <summary>
+        /// 璁板綍璐т綅鐘舵�佸彉鏇淬��
+        /// </summary>
+        Task<bool> AddChangeRecordAsync(
+            Dt_LocationInfo beforeLocation,
+            Dt_LocationInfo afterLocation,
+            LocationChangeType changeType,
+            int? taskNum = null,
+            string? orderNo = null,
+            int? orderId = null,
+            string? remark = null);
     }
 }
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IRecordService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IRecordService.cs
index 988fa8b..ea24123 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IRecordService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IRecordService.cs
@@ -1,4 +1,7 @@
-锘縰sing WIDESEA_Core;
+using WIDESEA_Common.LocationEnum;
+using WIDESEA_Common.StockEnum;
+using WIDESEA_Core;
+using WIDESEA_Model.Models;
 
 namespace WIDESEA_IRecordService
 {
@@ -16,5 +19,28 @@
         /// 搴撳瓨鏁伴噺鍙樻洿璁板綍鏈嶅姟
         /// </summary>
         IStockQuantityChangeRecordService StockQuantityChangeRecordService { get; }
+
+        /// <summary>
+        /// 鏂板璐т綅鐘舵�佸彉鏇磋褰�
+        /// </summary>
+        Task<bool> AddLocationChangeRecordAsync(
+            Dt_LocationInfo beforeLocation,
+            Dt_LocationInfo afterLocation,
+            LocationChangeType changeType,
+            int? taskNum = null,
+            string? orderNo = null,
+            int? orderId = null,
+            string? remark = null);
+
+        /// <summary>
+        /// 鏂板搴撳瓨鍙樻洿璁板綍
+        /// </summary>
+        Task<bool> AddStockChangeRecordAsync(
+            Dt_StockInfo? beforeStock,
+            Dt_StockInfo? afterStock,
+            StockChangeTypeEnum changeType,
+            int? taskNum = null,
+            string? orderNo = null,
+            string? remark = null);
     }
 }
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IStockQuantityChangeRecordService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IStockQuantityChangeRecordService.cs
index 474e5db..fd025f2 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IStockQuantityChangeRecordService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_IRecordService/IStockQuantityChangeRecordService.cs
@@ -1,4 +1,5 @@
-锘縰sing WIDESEA_Core.BaseRepository;
+using WIDESEA_Common.StockEnum;
+using WIDESEA_Core.BaseRepository;
 using WIDESEA_Core.BaseServices;
 using WIDESEA_Model.Models;
 
@@ -13,5 +14,16 @@
         /// 鑾峰彇搴撳瓨鏁伴噺鍙樻洿璁板綍浠撳偍鎺ュ彛
         /// </summary>
         IRepository<Dt_StockQuantityChangeRecord> Repository { get; }
+
+        /// <summary>
+        /// 璁板綍搴撳瓨鍙樻洿銆�
+        /// </summary>
+        Task<bool> AddChangeRecordAsync(
+            Dt_StockInfo? beforeStock,
+            Dt_StockInfo? afterStock,
+            StockChangeTypeEnum changeType,
+            int? taskNum = null,
+            string? orderNo = null,
+            string? remark = null);
     }
 }
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_Model/Models/Record/Dt_StockQuantityChangeRecord.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_Model/Models/Record/Dt_StockQuantityChangeRecord.cs
index 269dbae..d1db1d4 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_Model/Models/Record/Dt_StockQuantityChangeRecord.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_Model/Models/Record/Dt_StockQuantityChangeRecord.cs
@@ -93,6 +93,42 @@
         public float AfterQuantity { get; set; }
 
         /// <summary>
+        /// 鍙樺姩鍓嶅簱瀛樼姸鎬�
+        /// </summary>
+        [SugarColumn(IsNullable = false, ColumnDescription = "鍙樺姩鍓嶅簱瀛樼姸鎬�", DefaultValue = "0")]
+        public int BeforeStatus { get; set; }
+
+        /// <summary>
+        /// 鍙樺姩鍚庡簱瀛樼姸鎬�
+        /// </summary>
+        [SugarColumn(IsNullable = false, ColumnDescription = "鍙樺姩鍚庡簱瀛樼姸鎬�", DefaultValue = "0")]
+        public int AfterStatus { get; set; }
+
+        /// <summary>
+        /// 鍙樺姩鍓嶈揣浣嶄富閿�
+        /// </summary>
+        [SugarColumn(IsNullable = true, ColumnDescription = "鍙樺姩鍓嶈揣浣嶄富閿�")]
+        public int? BeforeLocationId { get; set; }
+
+        /// <summary>
+        /// 鍙樺姩鍚庤揣浣嶄富閿�
+        /// </summary>
+        [SugarColumn(IsNullable = true, ColumnDescription = "鍙樺姩鍚庤揣浣嶄富閿�")]
+        public int? AfterLocationId { get; set; }
+
+        /// <summary>
+        /// 鍙樺姩鍓嶈揣浣嶇紪鍙�
+        /// </summary>
+        [SugarColumn(IsNullable = true, Length = 30, ColumnDescription = "鍙樺姩鍓嶈揣浣嶇紪鍙�")]
+        public string BeforeLocationCode { get; set; }
+
+        /// <summary>
+        /// 鍙樺姩鍚庤揣浣嶇紪鍙�
+        /// </summary>
+        [SugarColumn(IsNullable = true, Length = 30, ColumnDescription = "鍙樺姩鍚庤揣浣嶇紪鍙�")]
+        public string AfterLocationCode { get; set; }
+
+        /// <summary>
         /// 澶囨敞
         /// </summary>
         [SugarColumn(IsNullable = true, ColumnDescription = "澶囨敞")]
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/LocationStatusChangeRecordService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/LocationStatusChangeRecordService.cs
index 28b3d83..a46a6b4 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/LocationStatusChangeRecordService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/LocationStatusChangeRecordService.cs
@@ -1,4 +1,5 @@
-锘縰sing WIDESEA_Core.BaseRepository;
+using WIDESEA_Common.LocationEnum;
+using WIDESEA_Core.BaseRepository;
 using WIDESEA_Core.BaseServices;
 using WIDESEA_IRecordService;
 using WIDESEA_Model.Models;
@@ -13,7 +14,6 @@
         /// <summary>
         /// 鏋勯�犲嚱鏁�
         /// </summary>
-        /// <param name="baseDal">鍩虹鏁版嵁璁块棶瀵硅薄</param>
         public LocationStatusChangeRecordService(IRepository<Dt_LocationStatusChangeRecord> baseDal) : base(baseDal)
         {
         }
@@ -22,5 +22,39 @@
         /// 鑾峰彇璐т綅鐘舵�佸彉鏇磋褰曚粨鍌ㄦ帴鍙�
         /// </summary>
         public IRepository<Dt_LocationStatusChangeRecord> Repository => BaseDal;
+
+        /// <summary>
+        /// 璁板綍璐т綅鐘舵�佸彉鏇淬��
+        /// </summary>
+        public async Task<bool> AddChangeRecordAsync(
+            Dt_LocationInfo beforeLocation,
+            Dt_LocationInfo afterLocation,
+            LocationChangeType changeType,
+            int? taskNum = null,
+            string? orderNo = null,
+            int? orderId = null,
+            string? remark = null)
+        {
+            if (beforeLocation == null || afterLocation == null)
+                return false;
+
+            if (beforeLocation.LocationStatus == afterLocation.LocationStatus)
+                return true;
+
+            Dt_LocationStatusChangeRecord record = new Dt_LocationStatusChangeRecord
+            {
+                LocationId = afterLocation.Id,
+                LocationCode = afterLocation.LocationCode,
+                BeforeStatus = beforeLocation.LocationStatus,
+                AfterStatus = afterLocation.LocationStatus,
+                ChangeType = (int)changeType,
+                OrderId = orderId,
+                OrderNo = orderNo,
+                TaskNum = taskNum,
+                Remark = remark
+            };
+
+            return await BaseDal.AddDataAsync(record) > 0;
+        }
     }
 }
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/RecordService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/RecordService.cs
index 7a8c6b8..ecdd199 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/RecordService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/RecordService.cs
@@ -1,4 +1,7 @@
-锘縰sing WIDESEA_IRecordService;
+using WIDESEA_Common.LocationEnum;
+using WIDESEA_Common.StockEnum;
+using WIDESEA_IRecordService;
+using WIDESEA_Model.Models;
 
 namespace WIDESEA_RecordService
 {
@@ -20,8 +23,6 @@
         /// <summary>
         /// 鏋勯�犲嚱鏁�
         /// </summary>
-        /// <param name="locationStatusChangeRecordService">璐т綅鐘舵�佸彉鏇磋褰曟湇鍔�</param>
-        /// <param name="stockQuantityChangeRecordService">搴撳瓨鏁伴噺鍙樻洿璁板綍鏈嶅姟</param>
         public RecordService(
             ILocationStatusChangeRecordService locationStatusChangeRecordService,
             IStockQuantityChangeRecordService stockQuantityChangeRecordService)
@@ -29,5 +30,47 @@
             LocationStatusChangeRecordService = locationStatusChangeRecordService;
             StockQuantityChangeRecordService = stockQuantityChangeRecordService;
         }
+
+        /// <summary>
+        /// 鏂板璐т綅鐘舵�佸彉鏇磋褰�
+        /// </summary>
+        public Task<bool> AddLocationChangeRecordAsync(
+            Dt_LocationInfo beforeLocation,
+            Dt_LocationInfo afterLocation,
+            LocationChangeType changeType,
+            int? taskNum = null,
+            string? orderNo = null,
+            int? orderId = null,
+            string? remark = null)
+        {
+            return LocationStatusChangeRecordService.AddChangeRecordAsync(
+                beforeLocation,
+                afterLocation,
+                changeType,
+                taskNum,
+                orderNo,
+                orderId,
+                remark);
+        }
+
+        /// <summary>
+        /// 鏂板搴撳瓨鍙樻洿璁板綍
+        /// </summary>
+        public Task<bool> AddStockChangeRecordAsync(
+            Dt_StockInfo? beforeStock,
+            Dt_StockInfo? afterStock,
+            StockChangeTypeEnum changeType,
+            int? taskNum = null,
+            string? orderNo = null,
+            string? remark = null)
+        {
+            return StockQuantityChangeRecordService.AddChangeRecordAsync(
+                beforeStock,
+                afterStock,
+                changeType,
+                taskNum,
+                orderNo,
+                remark);
+        }
     }
 }
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/StockQuantityChangeRecordService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/StockQuantityChangeRecordService.cs
index 8d501f5..d8c0093 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/StockQuantityChangeRecordService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_RecordService/StockQuantityChangeRecordService.cs
@@ -1,4 +1,4 @@
-using MapsterMapper;
+using WIDESEA_Common.StockEnum;
 using WIDESEA_Core.BaseRepository;
 using WIDESEA_Core.BaseServices;
 using WIDESEA_IRecordService;
@@ -11,21 +11,87 @@
     /// </summary>
     public partial class StockQuantityChangeRecordService : ServiceBase<Dt_StockQuantityChangeRecord, IRepository<Dt_StockQuantityChangeRecord>>, IStockQuantityChangeRecordService
     {
-        private readonly IMapper _mapper;
-
         /// <summary>
         /// 鏋勯�犲嚱鏁�
         /// </summary>
-        /// <param name="baseDal">鍩虹鏁版嵁璁块棶瀵硅薄</param>
-        /// <param name="mapper">瀵硅薄鏄犲皠鍣�</param>
-        public StockQuantityChangeRecordService(IRepository<Dt_StockQuantityChangeRecord> baseDal, IMapper mapper) : base(baseDal)
+        public StockQuantityChangeRecordService(IRepository<Dt_StockQuantityChangeRecord> baseDal) : base(baseDal)
         {
-            _mapper = mapper;
         }
 
         /// <summary>
         /// 鑾峰彇搴撳瓨鏁伴噺鍙樻洿璁板綍浠撳偍鎺ュ彛
         /// </summary>
         public IRepository<Dt_StockQuantityChangeRecord> Repository => BaseDal;
+
+        /// <summary>
+        /// 璁板綍搴撳瓨鍙樻洿銆�
+        /// </summary>
+        public async Task<bool> AddChangeRecordAsync(
+            Dt_StockInfo? beforeStock,
+            Dt_StockInfo? afterStock,
+            StockChangeTypeEnum changeType,
+            int? taskNum = null,
+            string? orderNo = null,
+            string? remark = null)
+        {
+            if (beforeStock == null && afterStock == null)
+                return false;
+
+            var beforeQuantity = GetStockQuantity(beforeStock);
+            var afterQuantity = GetStockQuantity(afterStock);
+            var beforeStatus = beforeStock?.StockStatus ?? 0;
+            var afterStatus = afterStock?.StockStatus ?? 0;
+            int? beforeLocationId = beforeStock?.LocationId > 0 ? beforeStock.LocationId : (int?)null;
+            int? afterLocationId = afterStock?.LocationId > 0 ? afterStock.LocationId : (int?)null;
+            var beforeLocationCode = beforeStock?.LocationCode;
+            var afterLocationCode = afterStock?.LocationCode;
+
+            if (beforeQuantity == afterQuantity &&
+                beforeStatus == afterStatus &&
+                beforeLocationId == afterLocationId &&
+                beforeLocationCode == afterLocationCode)
+            {
+                return true;
+            }
+
+            var currentStock = afterStock ?? beforeStock!;
+            Dt_StockQuantityChangeRecord record = new Dt_StockQuantityChangeRecord
+            {
+                StockDetailId = currentStock.Id,
+                PalleCode = currentStock.PalletCode,
+                MaterielCode = GetFirstValue(currentStock.Details?.Select(x => x.MaterielCode)),
+                MaterielName = GetFirstValue(currentStock.Details?.Select(x => x.MaterielName)),
+                BatchNo = GetFirstValue(currentStock.Details?.Select(x => x.BatchNo)),
+                SerilNumber = GetFirstValue(currentStock.Details?.Select(x => x.SerialNumber)),
+                OrderNo = orderNo,
+                TaskNum = taskNum,
+                ChangeType = (int)changeType,
+                ChangeQuantity = afterQuantity - beforeQuantity,
+                BeforeQuantity = beforeQuantity,
+                AfterQuantity = afterQuantity,
+                BeforeStatus = beforeStatus,
+                AfterStatus = afterStatus,
+                BeforeLocationId = beforeLocationId,
+                AfterLocationId = afterLocationId,
+                BeforeLocationCode = beforeLocationCode,
+                AfterLocationCode = afterLocationCode,
+                Remark = remark
+            };
+
+            return await BaseDal.AddDataAsync(record) > 0;
+        }
+
+        private static float GetStockQuantity(Dt_StockInfo? stockInfo)
+        {
+            if (stockInfo?.Details == null || !stockInfo.Details.Any())
+                return 0;
+
+            return stockInfo.Details.Sum(x => x.StockQuantity);
+        }
+
+        private static string GetFirstValue(IEnumerable<string>? values)
+        {
+            return values?.FirstOrDefault() ?? string.Empty;
+        }
     }
 }
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
index 81f3390..0fd9881 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
@@ -1,8 +1,10 @@
 using WIDESEA_Common.StockEnum;
+using WIDESEA_Core;
 using WIDESEA_Core.BaseRepository;
 using WIDESEA_Core.BaseServices;
 using WIDESEA_DTO.Stock;
 using WIDESEA_IBasicService;
+using WIDESEA_IRecordService;
 using WIDESEA_IStockService;
 using WIDESEA_Model.Models;
 
@@ -27,15 +29,21 @@
         /// 浠撳簱淇℃伅鏈嶅姟鎺ュ彛锛堢敤浜庤幏鍙栦粨搴撳熀鏈俊鎭級
         /// </summary>
         private readonly IWarehouseService _warehouseService;
+        private readonly IRecordService _recordService;
 
         /// <summary>
         /// 鏋勯�犲嚱鏁�
         /// </summary>
         /// <param name="baseDal">鍩虹鏁版嵁璁块棶瀵硅薄</param>
-        public StockInfoService(IRepository<Dt_StockInfo> baseDal, ILocationInfoService locationInfoService, IWarehouseService warehouseService) : base(baseDal)
+        public StockInfoService(
+            IRepository<Dt_StockInfo> baseDal,
+            ILocationInfoService locationInfoService,
+            IWarehouseService warehouseService,
+            IRecordService recordService) : base(baseDal)
         {
             _locationInfoService = locationInfoService;
             _warehouseService = warehouseService;
+            _recordService = recordService;
         }
 
         /// <summary>
@@ -79,7 +87,100 @@
         /// <returns>鏇存柊鏄惁鎴愬姛</returns>
         public async Task<bool> UpdateStockAsync(Dt_StockInfo stockInfo)
         {
-            return await BaseDal.UpdateDataAsync(stockInfo);
+            var beforeStock = await BaseDal.QueryDataNavFirstAsync(x => x.Id == stockInfo.Id);
+            var result = await BaseDal.UpdateDataAsync(stockInfo);
+            if (!result)
+                return false;
+
+            var afterStock = await BaseDal.QueryDataNavFirstAsync(x => x.Id == stockInfo.Id) ?? stockInfo;
+            var changeType = ResolveChangeType(beforeStock, afterStock);
+            return await _recordService.AddStockChangeRecordAsync(beforeStock, afterStock, changeType, remark: "搴撳瓨鏇存柊");
+        }
+
+        public override WebResponseContent AddData(Dt_StockInfo entity)
+        {
+            var result = base.AddData(entity);
+            if (!result.Status)
+                return result;
+
+            var saveRecordResult = _recordService.AddStockChangeRecordAsync(null, entity, StockChangeTypeEnum.Inbound, remark: "搴撳瓨鏂板").GetAwaiter().GetResult();
+            return saveRecordResult ? result : WebResponseContent.Instance.Error("搴撳瓨鍙樻洿璁板綍淇濆瓨澶辫触");
+        }
+
+        public override WebResponseContent UpdateData(Dt_StockInfo entity)
+        {
+            var beforeStock = BaseDal.QueryFirst(x => x.Id == entity.Id);
+            var result = base.UpdateData(entity);
+            if (!result.Status)
+                return result;
+
+            var changeType = ResolveChangeType(beforeStock, entity);
+            var saveRecordResult = _recordService.AddStockChangeRecordAsync(beforeStock, entity, changeType, remark: "搴撳瓨鏇存柊").GetAwaiter().GetResult();
+            return saveRecordResult ? result : WebResponseContent.Instance.Error("搴撳瓨鍙樻洿璁板綍淇濆瓨澶辫触");
+        }
+
+        public override WebResponseContent DeleteData(Dt_StockInfo entity)
+        {
+            var beforeStock = CloneStockSnapshot(entity);
+            var result = base.DeleteData(entity);
+            if (!result.Status)
+                return result;
+
+            var saveRecordResult = _recordService.AddStockChangeRecordAsync(beforeStock, null, StockChangeTypeEnum.Outbound, remark: "搴撳瓨鍒犻櫎").GetAwaiter().GetResult();
+            return saveRecordResult ? result : WebResponseContent.Instance.Error("搴撳瓨鍙樻洿璁板綍淇濆瓨澶辫触");
+        }
+
+        private static StockChangeTypeEnum ResolveChangeType(Dt_StockInfo? beforeStock, Dt_StockInfo? afterStock)
+        {
+            if (beforeStock == null)
+                return StockChangeTypeEnum.Inbound;
+
+            if (afterStock == null)
+                return StockChangeTypeEnum.Outbound;
+
+            if (!string.Equals(beforeStock.LocationCode, afterStock.LocationCode, StringComparison.OrdinalIgnoreCase))
+                return StockChangeTypeEnum.Relocation;
+
+            if (beforeStock.StockStatus != afterStock.StockStatus)
+                return StockChangeTypeEnum.StockLock;
+
+            var beforeQuantity = beforeStock.Details?.Sum(x => x.StockQuantity) ?? 0;
+            var afterQuantity = afterStock.Details?.Sum(x => x.StockQuantity) ?? 0;
+            return afterQuantity >= beforeQuantity ? StockChangeTypeEnum.Inbound : StockChangeTypeEnum.Outbound;
+        }
+
+        private static Dt_StockInfo CloneStockSnapshot(Dt_StockInfo stockInfo)
+        {
+            return new Dt_StockInfo
+            {
+                Id = stockInfo.Id,
+                PalletCode = stockInfo.PalletCode,
+                PalletType = stockInfo.PalletType,
+                LocationId = stockInfo.LocationId,
+                LocationCode = stockInfo.LocationCode,
+                WarehouseId = stockInfo.WarehouseId,
+                StockStatus = stockInfo.StockStatus,
+                Remark = stockInfo.Remark,
+                OutboundDate = stockInfo.OutboundDate,
+                Details = stockInfo.Details?.Select(detail => new Dt_StockInfoDetail
+                {
+                    Id = detail.Id,
+                    StockId = detail.StockId,
+                    MaterielCode = detail.MaterielCode,
+                    MaterielName = detail.MaterielName,
+                    OrderNo = detail.OrderNo,
+                    BatchNo = detail.BatchNo,
+                    ProductionDate = detail.ProductionDate,
+                    EffectiveDate = detail.EffectiveDate,
+                    SerialNumber = detail.SerialNumber,
+                    StockQuantity = detail.StockQuantity,
+                    OutboundQuantity = detail.OutboundQuantity,
+                    Status = detail.Status,
+                    Unit = detail.Unit,
+                    InboundOrderRowNo = detail.InboundOrderRowNo,
+                    Remark = detail.Remark
+                }).ToList()
+            };
         }
 
         /// <summary>
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
index fe278a6..2e33fbc 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -19,6 +19,7 @@
 using WIDESEA_DTO.Stock;
 using WIDESEA_DTO.Task;
 using WIDESEA_IBasicService;
+using WIDESEA_IRecordService;
 using WIDESEA_IStockService;
 using WIDESEA_ITaskInfoService;
 using WIDESEA_Model.Models;
@@ -37,6 +38,7 @@
         private readonly ITask_HtyService _task_HtyService;
         private readonly IStockInfo_HtyService _stockInfo_HtyService;
         private readonly IUnitOfWorkManage _unitOfWorkManage;
+        private readonly IRecordService _recordService;
 
         public IRepository<Dt_Task> Repository => BaseDal;
 
@@ -60,7 +62,8 @@
             IMesService mesService,
             ITask_HtyService task_HtyService,
             IStockInfo_HtyService stockInfo_HtyService,
-            IUnitOfWorkManage unitOfWorkManage) : base(BaseDal)
+            IUnitOfWorkManage unitOfWorkManage,
+            IRecordService recordService) : base(BaseDal)
         {
             _mapper = mapper;
             _stockInfoService = stockInfoService;
@@ -72,6 +75,7 @@
             _task_HtyService = task_HtyService;
             _stockInfo_HtyService = stockInfo_HtyService;
             _unitOfWorkManage = unitOfWorkManage;
+            _recordService = recordService;
         }
 
         /// <summary>
@@ -165,6 +169,5 @@
 
             return _roundRobinService.GetNextAddress(matchedPrefix, addresses);
         }
-
     }
-}
+}
\ No newline at end of file
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs
index 074c48e..201ccea 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs
@@ -1,8 +1,10 @@
 using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using System;
 using System.Collections.Concurrent;
+using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
@@ -14,20 +16,35 @@
 namespace WIDESEA_WMSServer.BackgroundServices
 {
     /// <summary>
-    /// 搴撳瓨鐩戞帶鍚庡彴鏈嶅姟
-    /// 瀹氭湡妫�鏌ュ簱瀛樺拰璐т綅鏁版嵁鍙樺寲骞堕�氳繃SignalR鎺ㄩ�佸埌鍓嶇
+    /// 搴撳瓨鐩戞帶鍚庡彴鏈嶅姟銆�
+    /// 鍚姩鏃跺垵濮嬪寲涓�娆″叏閲忓揩鐓э紝鍚庣画浠呮寜鏇存柊鏃堕棿澧為噺妫�鏌ュ彈褰卞搷璐т綅锛屽苟閫氳繃 SignalR 鎺ㄩ�佸彉鍖栥��
     /// </summary>
     public class StockMonitorBackgroundService : BackgroundService
     {
+        private const int MonitorIntervalMs = 3000;
+
         private readonly ILogger<StockMonitorBackgroundService> _logger;
         private readonly IHubContext<StockHub> _hubContext;
         private readonly IServiceProvider _serviceProvider;
 
-        // 璐т綅鐘舵�佸揩鐓э細key = LocationId
-        private ConcurrentDictionary<int, LocationSnapshot> _lastLocationSnapshots = new();
+        /// <summary>
+        /// 璐т綅鐘舵�佸揩鐓э細key = LocationId銆�
+        /// </summary>
+        private readonly ConcurrentDictionary<int, LocationSnapshot> _lastLocationSnapshots = new();
 
-        // 鐩戞帶闂撮殧锛堟绉掞級
-        private const int MonitorIntervalMs = 3000;
+        /// <summary>
+        /// 搴撳瓨鎵�鍦ㄨ揣浣嶆槧灏勶細key = StockId, value = LocationId銆�
+        /// 鐢ㄤ簬璇嗗埆搴撳瓨浠庢棫璐т綅绉诲姩鍒版柊璐т綅鏃讹紝涓よ竟閮介渶瑕佹帹閫佸埛鏂般��
+        /// </summary>
+        private readonly ConcurrentDictionary<int, int> _lastStockLocationMap = new();
+
+        /// <summary>
+        /// 涓婃澧為噺妫�鏌ユ椂闂存埑銆�
+        /// 鍒嗗埆璺熻釜璐т綅銆佸簱瀛樹富琛ㄣ�佸簱瀛樻槑缁嗭紝閬垮厤姣忔鍏ㄨ〃鎵弿銆�
+        /// </summary>
+        private DateTime _lastLocationCheckTime = DateTime.MinValue;
+        private DateTime _lastStockCheckTime = DateTime.MinValue;
+        private DateTime _lastDetailCheckTime = DateTime.MinValue;
 
         public StockMonitorBackgroundService(
             ILogger<StockMonitorBackgroundService> logger,
@@ -43,8 +60,9 @@
         {
             _logger.LogInformation("搴撳瓨鐩戞帶鍚庡彴鏈嶅姟宸插惎鍔�");
 
-            // 绛夊緟搴旂敤瀹屽叏鍚姩
+            // 绛夊緟搴旂敤鍒濆鍖栧畬鎴愶紝閬垮厤鍚姩闃舵涓庡叾浠栧垵濮嬪寲浠诲姟浜夋姠鏁版嵁搴撹祫婧愩��
             await Task.Delay(5000, stoppingToken);
+            await InitializeSnapshotsAsync();
 
             while (!stoppingToken.IsCancellationRequested)
             {
@@ -54,7 +72,7 @@
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "妫�鏌ユ暟鎹彉鍖栨椂鍙戠敓閿欒");
+                    _logger.LogError(ex, "妫�鏌ュ簱瀛樺彉鏇存椂鍙戠敓寮傚父");
                 }
 
                 await Task.Delay(MonitorIntervalMs, stoppingToken);
@@ -64,110 +82,252 @@
         }
 
         /// <summary>
-        /// 妫�鏌ヨ揣浣嶅拰搴撳瓨鍙樺寲
+        /// 鍒濆鍖栧叏閲忓揩鐓с�備粎鍦ㄦ湇鍔″惎鍔ㄦ椂鎵ц涓�娆★紝鍚庣画璧板閲忔鏌ャ��
+        /// </summary>
+        private async Task InitializeSnapshotsAsync()
+        {
+            using var scope = _serviceProvider.CreateScope();
+            var stockService = scope.ServiceProvider.GetRequiredService<IStockInfoService>();
+            var locationRepo = scope.ServiceProvider.GetRequiredService<IRepository<Dt_LocationInfo>>();
+            var initializedAt = DateTime.Now;
+
+            var allLocations = await locationRepo.Db.Queryable<Dt_LocationInfo>()
+                .Where(x => x.LocationStatus != 99)
+                .ToListAsync();
+
+            var allStocks = await stockService.Repository.Db.Queryable<Dt_StockInfo>()
+                .Includes(x => x.Details)
+                .Where(x => x.LocationId != 0)
+                .ToListAsync();
+
+            var stockDict = allStocks
+                .Where(x => x.LocationId > 0)
+                .GroupBy(x => x.LocationId)
+                .ToDictionary(x => x.Key, x => x.OrderByDescending(item => item.ModifyDate ?? item.CreateDate).First());
+
+            foreach (var location in allLocations)
+            {
+                stockDict.TryGetValue(location.Id, out var stock);
+                _lastLocationSnapshots[location.Id] = BuildLocationSnapshot(location, stock);
+            }
+
+            foreach (var stock in allStocks.Where(x => x.LocationId > 0))
+            {
+                _lastStockLocationMap[stock.Id] = stock.LocationId;
+            }
+
+            _lastLocationCheckTime = initializedAt;
+            _lastStockCheckTime = initializedAt;
+            _lastDetailCheckTime = initializedAt;
+
+            _logger.LogInformation("搴撳瓨鐩戞帶蹇収鍒濆鍖栧畬鎴愶紝璐т綅鏁�={LocationCount}锛屽簱瀛樻暟={StockCount}", allLocations.Count, allStocks.Count);
+        }
+
+        /// <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>>();
+            var checkStartedAt = DateTime.Now;
 
-            // 1. 鑾峰彇鎵�鏈夎揣浣嶆暟鎹�
-            var allLocations = await locationRepo.QueryDataAsync(x => x.LocationStatus != 99); // 鎺掗櫎绂佺敤鐨勮揣浣�
-
-            // 2. 鑾峰彇鎵�鏈夊簱瀛樻暟鎹紙鍖呭惈鏄庣粏锛�
-            var allStockData = await stockService.Repository.Db.Queryable<Dt_StockInfo>()
-                .Includes(x => x.Details)
+            var changedLocationIds = await locationRepo.Db.Queryable<Dt_LocationInfo>()
+                .Where(x => x.LocationStatus != 99 && (x.ModifyDate ?? x.CreateDate) > _lastLocationCheckTime)
+                .Select(x => x.Id)
                 .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())
+            var changedStocks = await stockService.Repository.Db.Queryable<Dt_StockInfo>()
+                .Where(x => (x.ModifyDate ?? x.CreateDate) > _lastStockCheckTime)
+                .Select(x => new StockLocationChange
                 {
-                    totalQuantity = stock.Details.Sum(d => d.StockQuantity);
-                    detailsHash = GenerateDetailsHash(stock.Details.ToList());
+                    StockId = x.Id,
+                    LocationId = x.LocationId
+                })
+                .ToListAsync();
+
+            var changedDetailStockIds = await stockService.Repository.Db.Queryable<Dt_StockInfoDetail>()
+                .Where(x => (x.ModifyDate ?? x.CreateDate) > _lastDetailCheckTime)
+                .Select(x => x.StockId)
+                .Distinct()
+                .ToListAsync();
+
+            var affectedLocationIds = new HashSet<int>(changedLocationIds);
+
+            foreach (var stock in changedStocks)
+            {
+                var previousLocationId = _lastStockLocationMap.TryGetValue(stock.StockId, out var oldLocationId)
+                    ? oldLocationId
+                    : 0;
+
+                if (previousLocationId > 0 && previousLocationId != stock.LocationId)
+                {
+                    affectedLocationIds.Add(previousLocationId);
                 }
 
-                var snapshot = new LocationSnapshot
+                if (stock.LocationId > 0)
                 {
-                    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))
+                    affectedLocationIds.Add(stock.LocationId);
+                    _lastStockLocationMap[stock.StockId] = stock.LocationId;
+                }
+                else
                 {
-                    // 妫�娴嬪彉鍖栵細璐т綅鐘舵�併�佸簱瀛樼姸鎬併�佹暟閲忋�佹槑缁嗗彉鍖�
-                    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);
-                    }
+                    _lastStockLocationMap.TryRemove(stock.StockId, out _);
                 }
             }
 
-            // 鏇存柊蹇収鏁版嵁
-            _lastLocationSnapshots = currentSnapshots;
+            if (changedDetailStockIds.Count > 0)
+            {
+                var detailAffectedLocationIds = await stockService.Repository.Db.Queryable<Dt_StockInfo>()
+                    .Where(x => changedDetailStockIds.Contains(x.Id) && x.LocationId != 0)
+                    .Select(x => x.LocationId)
+                    .Distinct()
+                    .ToListAsync();
+
+                foreach (var locationId in detailAffectedLocationIds)
+                {
+                    affectedLocationIds.Add(locationId);
+                }
+            }
+
+            if (affectedLocationIds.Count == 0)
+            {
+                UpdateCheckTimes(checkStartedAt);
+                return;
+            }
+
+            var affectedLocations = await locationRepo.Db.Queryable<Dt_LocationInfo>()
+                .Where(x => affectedLocationIds.Contains(x.Id) && x.LocationStatus != 99)
+                .ToListAsync();
+
+            var locationDict = affectedLocations.ToDictionary(x => x.Id, x => x);
+
+            var affectedStocks = await stockService.Repository.Db.Queryable<Dt_StockInfo>()
+                .Includes(x => x.Details)
+                .Where(x => affectedLocationIds.Contains(x.LocationId))
+                .ToListAsync();
+
+            var stockDict = affectedStocks
+                .Where(x => x.LocationId > 0)
+                .GroupBy(x => x.LocationId)
+                .ToDictionary(x => x.Key, x => x.OrderByDescending(item => item.ModifyDate ?? item.CreateDate).First());
+
+            foreach (var locationId in affectedLocationIds)
+            {
+                if (!locationDict.TryGetValue(locationId, out var location))
+                {
+                    _lastLocationSnapshots.TryRemove(locationId, out _);
+                    continue;
+                }
+
+                stockDict.TryGetValue(locationId, out var stock);
+                var snapshot = BuildLocationSnapshot(location, stock);
+
+                if (HasSnapshotChanged(locationId, snapshot))
+                {
+                    var update = new StockUpdateDTO
+                    {
+                        LocationId = snapshot.LocationId,
+                        WarehouseId = snapshot.WarehouseId,
+                        PalletCode = snapshot.PalletCode,
+                        StockQuantity = snapshot.StockQuantity,
+                        StockStatus = snapshot.StockStatus,
+                        LocationStatus = snapshot.LocationStatus,
+                        OutboundDate = snapshot.OutboundDate,
+                        Details = BuildDetailDtos(stock?.Details?.ToList())
+                    };
+
+                    await _hubContext.Clients.All.SendAsync("StockUpdated", update);
+                    _logger.LogDebug(
+                        "澧為噺搴撳瓨鍙樻洿鎺ㄩ�侊紝LocationId={LocationId}锛孡ocationStatus={LocationStatus}锛孲tockStatus={StockStatus}锛孮uantity={Quantity}",
+                        snapshot.LocationId,
+                        snapshot.LocationStatus,
+                        snapshot.StockStatus, 
+                        snapshot.OutboundDate,
+                        snapshot.StockQuantity);
+                }
+
+                _lastLocationSnapshots[locationId] = snapshot;
+            }
+
+            UpdateCheckTimes(checkStartedAt);
         }
 
         /// <summary>
-        /// 鐢熸垚鏄庣粏鏁版嵁鍝堝笇
+        /// 鏋勫缓璐т綅蹇収锛岀敤浜庡垽鏂揣浣嶆槸鍚﹂渶瑕佹帹閫佹洿鏂般��
+        /// </summary>
+        private LocationSnapshot BuildLocationSnapshot(Dt_LocationInfo location, Dt_StockInfo 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());
+            }
+
+            return 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,
+                OutboundDate = stock?.OutboundDate ?? default
+            };
+        }
+
+        /// <summary>
+        /// 瀵规瘮蹇収鍙樺寲锛屼粎鍦ㄥ叧閿瓧娈靛彉鍖栨椂瑙﹀彂鎺ㄩ�併��
+        /// </summary>
+        private bool HasSnapshotChanged(int locationId, LocationSnapshot snapshot)
+        {
+            if (!_lastLocationSnapshots.TryGetValue(locationId, out var lastSnapshot))
+            {
+                return true;
+            }
+
+            return lastSnapshot.LocationStatus != snapshot.LocationStatus ||
+                   lastSnapshot.StockStatus != snapshot.StockStatus ||
+                   lastSnapshot.PalletCode != snapshot.PalletCode ||
+                   Math.Abs(lastSnapshot.StockQuantity - snapshot.StockQuantity) > 0.001f ||
+                   lastSnapshot.DetailsHash != snapshot.DetailsHash;
+        }
+
+        /// <summary>
+        /// 鐢熸垚搴撳瓨鏄庣粏鍝堝笇锛岀‘淇濇槑缁嗗唴瀹瑰彉鍖栨椂鑳借Е鍙戝墠绔埛鏂般��
         /// </summary>
         private string GenerateDetailsHash(List<Dt_StockInfoDetail> details)
         {
-            if (details == null || !details.Any()) return string.Empty;
+            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}"));
+            var hashString = string.Join(
+                "|",
+                details
+                    .OrderBy(d => d.Id)
+                    .Select(d => $"{d.Id}:{d.MaterielCode}:{d.BatchNo}:{d.StockQuantity}:{d.SerialNumber}:{d.InboundOrderRowNo}:{d.Status}"));
+
             return hashString.GetHashCode().ToString();
         }
 
         /// <summary>
-        /// 鏋勫缓鏄庣粏DTO鍒楄〃
+        /// 鏋勫缓鎺ㄩ�佺粰鍓嶇鐨勬槑缁嗘暟鎹��
         /// </summary>
         private List<StockDetailUpdateDTO> BuildDetailDtos(List<Dt_StockInfoDetail> details)
         {
-            if (details == null || !details.Any()) return new List<StockDetailUpdateDTO>();
+            if (details == null || !details.Any())
+            {
+                return new List<StockDetailUpdateDTO>();
+            }
 
             return details.Select(d => new StockDetailUpdateDTO
             {
@@ -177,12 +337,24 @@
                 BatchNo = d.BatchNo,
                 StockQuantity = d.StockQuantity,
                 Unit = d.Unit,
-                Status = d.Status
+                Status = d.Status,
+                SerialNumber = d.SerialNumber,
+                InboundOrderRowNo = d.InboundOrderRowNo
             }).ToList();
         }
 
         /// <summary>
-        /// 璐т綅蹇収
+        /// 鏇存柊鍚勭被澧為噺妫�鏌ユ椂闂淬��
+        /// </summary>
+        private void UpdateCheckTimes(DateTime checkStartedAt)
+        {
+            _lastLocationCheckTime = checkStartedAt;
+            _lastStockCheckTime = checkStartedAt;
+            _lastDetailCheckTime = checkStartedAt;
+        }
+
+        /// <summary>
+        /// 璐т綅蹇収銆�
         /// </summary>
         private class LocationSnapshot
         {
@@ -194,6 +366,16 @@
             public int StockStatus { get; set; }
             public float StockQuantity { get; set; }
             public string DetailsHash { get; set; }
+            public DateTime OutboundDate { get; set; }
+        }
+
+        /// <summary>
+        /// 搴撳瓨鍙樻洿瀹氫綅淇℃伅銆�
+        /// </summary>
+        private class StockLocationChange
+        {
+            public int StockId { get; set; }
+            public int LocationId { 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 af48512..31647a5 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 SqlSugar;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 
@@ -26,6 +27,7 @@
         public float StockQuantity { get; set; }
         public int StockStatus { get; set; }
         public int LocationStatus { get; set; }
+        public DateTime OutboundDate { get; set; }
         public List<StockDetailUpdateDTO> Details { get; set; } = new();
     }
 
@@ -41,5 +43,7 @@
         public float StockQuantity { get; set; }
         public string Unit { get; set; }
         public int Status { get; set; }
+        public string SerialNumber { get; set; }
+        public int InboundOrderRowNo { get; set; }
     }
 }
\ No newline at end of file
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json
index a37ed70..49d1eae 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json
@@ -63,7 +63,7 @@
   "PDAVersion": "4",
   "WebSocketPort": 9296,
   "AutoOutboundTask": {
-    "Enable": true, /// 鏄惁鍚敤鑷姩鍑哄簱浠诲姟
+    "Enable": false, /// 鏄惁鍚敤鑷姩鍑哄簱浠诲姟
     "CheckIntervalSeconds": 300, /// 妫�鏌ラ棿闅旓紙绉掞級
     "TargetAddresses": { /// 鎸夊贩閬撳墠缂�閰嶇疆鐩爣鍦板潃锛堟敮鎸佸鍑哄簱鍙o級
       "GW": [ "11001", "11010", "11068" ],

--
Gitblit v1.9.3