From 8482760e3db0581ee34d79424e73fed69e7948d9 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期一, 30 三月 2026 18:10:20 +0800
Subject: [PATCH] feat(stockChat): 库存3D查看器完整实现
---
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs | 26 +
Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md | 314 +++++++++++++++++++
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs | 1
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue | 328 ++++++++++++++++----
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs | 58 ++-
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs | 199 ++++++++++++
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs | 21 +
7 files changed, 854 insertions(+), 93 deletions(-)
diff --git a/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
index fdb35a2..a463460 100644
--- a/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
+++ b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
@@ -12,18 +12,22 @@
<!-- 宸ュ叿鏍� -->
<div class="toolbar">
- <el-select v-model="filterStockStatus" placeholder="搴撳瓨鐘舵�佺瓫閫�" clearable>
- <el-option label="鏈夎揣" :value="1" />
- <el-option label="搴撳瓨绱у紶" :value="2" />
- <el-option label="宸叉弧" :value="3" />
+ <el-select v-model="filterStockStatus" placeholder="搴撳瓨鐘舵�佺瓫閫�" style="width: 160px" clearable>
+ <el-option label="缁勭洏鏆傚瓨(1)" :value="1" />
+ <el-option label="鍏ュ簱纭(3)" :value="3" />
+ <el-option label="鍏ュ簱瀹屾垚(6)" :value="6" />
+ <el-option label="鍑哄簱閿佸畾(7)" :value="7" />
+ <el-option label="鍑哄簱瀹屾垚(8)" :value="8" />
+ <el-option label="绌烘墭鐩�(22)" :value="22" />
</el-select>
- <el-select v-model="filterMaterielCode" placeholder="鐗╂枡绛涢��" clearable>
+ <el-select v-model="filterMaterielCode" placeholder="鐗╂枡绛涢��" style="width: 140px" clearable>
<el-option v-for="code in materielCodeList" :key="code" :label="code" :value="code" />
</el-select>
- <el-select v-model="filterBatchNo" placeholder="鎵规绛涢��" clearable>
+ <el-select v-model="filterBatchNo" placeholder="鎵规绛涢��" style="width: 140px" clearable>
<el-option v-for="batch in batchNoList" :key="batch" :label="batch" :value="batch" />
</el-select>
<el-button @click="resetCamera">閲嶇疆瑙嗚</el-button>
+ <el-button type="primary" @click="refreshData" :loading="refreshing">鍒锋柊鏁版嵁</el-button>
</div>
<!-- 3D Canvas -->
@@ -31,32 +35,46 @@
<!-- 鐘舵�佸浘渚� -->
<div class="legend">
+ <div class="legend-title">璐т綅鐘舵��</div>
<div v-for="item in legendItems" :key="item.status" class="legend-item">
<span class="color-box" :style="{ background: item.color }" />
<span>{{ item.label }}</span>
</div>
</div>
- <!-- 璇︽儏寮圭獥 -->
- <el-dialog v-model="detailDialogVisible" title="搴撳瓨璇︽儏" fullscreen>
+ <!-- 璇︽儏渚ц竟闈㈡澘 -->
+ <el-drawer v-model="detailDialogVisible" title="搴撳瓨璇︽儏" direction="rtl" size="500px">
<div v-if="selectedLocation" class="detail-content">
<el-descriptions :column="2" border>
<el-descriptions-item label="璐т綅缂栧彿">{{ selectedLocation.locationCode }}</el-descriptions-item>
<el-descriptions-item label="璐т綅鐘舵��">{{ getLocationStatusText(selectedLocation.locationStatus) }}</el-descriptions-item>
<el-descriptions-item label="鎵樼洏缂栧彿">{{ selectedLocation.palletCode || '鏃�' }}</el-descriptions-item>
<el-descriptions-item label="搴撳瓨鐘舵��">{{ getStockStatusText(selectedLocation.stockStatus) }}</el-descriptions-item>
- <el-descriptions-item label="褰撳墠搴撳瓨">{{ selectedLocation.stockQuantity }}</el-descriptions-item>
- <el-descriptions-item label="鐗╂枡缂栧彿">{{ selectedLocation.materielCode || '鏃�' }}</el-descriptions-item>
- <el-descriptions-item label="鐗╂枡鍚嶇О">{{ selectedLocation.materielName || '鏃�' }}</el-descriptions-item>
- <el-descriptions-item label="鎵规鍙�">{{ selectedLocation.batchNo || '鏃�' }}</el-descriptions-item>
+ <el-descriptions-item label="鎬诲簱瀛�">{{ selectedLocation.stockQuantity }}{{ selectedLocation.unit || '' }}</el-descriptions-item>
</el-descriptions>
+
+ <!-- 搴撳瓨鏄庣粏琛ㄦ牸 -->
+ <div v-if="selectedLocation.details && selectedLocation.details.length > 0" class="detail-table">
+ <h4>搴撳瓨鏄庣粏</h4>
+ <el-table :data="selectedLocation.details" border size="small" max-height="400">
+ <el-table-column prop="materielCode" label="鐗╂枡缂栫爜" width="100" />
+ <el-table-column prop="materielName" label="鐗╂枡鍚嶇О" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="batchNo" label="鎵规鍙�" width="100" show-overflow-tooltip />
+ <el-table-column prop="stockQuantity" label="鏁伴噺" width="70" align="right" />
+ <el-table-column prop="unit" label="鍗曚綅" width="50" align="center" />
+ <el-table-column prop="effectiveDate" label="鏈夋晥鏈�" width="100" />
+ </el-table>
+ </div>
+ <div v-else class="no-detail">
+ <el-empty description="鏆傛棤搴撳瓨鏄庣粏" />
+ </div>
</div>
- </el-dialog>
+ </el-drawer>
</div>
</template>
<script setup>
-import { ref, reactive, onMounted, onUnmounted, watch, getCurrentInstance } from 'vue'
+import { ref, reactive, onMounted, onUnmounted, watch, nextTick, getCurrentInstance } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as signalR from '@microsoft/signalr'
@@ -66,24 +84,44 @@
// SignalR 杩炴帴
let connection = null
-// 棰滆壊甯搁噺
+// 棰滆壊甯搁噺 - 鍩轰簬瀹為檯鏋氫妇鍊�
+// 璐т綅鐘舵��(locationStatus): 0=绌洪棽, 1=閿佸畾, 10=鏈夎揣閿佸畾, 20=绌洪棽閿佸畾, 99=澶ф墭鐩橀攣瀹�, 100=鏈夎揣
+// 搴撳瓨鐘舵��(stockStatus): 1=缁勭洏鏆傚瓨, 3=鍏ュ簱纭, 6=鍏ュ簱瀹屾垚, 7=鍑哄簱閿佸畾, 8=鍑哄簱瀹屾垚, 9=绉诲簱閿佸畾绛�
const COLOR_MAP = {
- DISABLED: 0x2d2d2d, // 绂佺敤 - 娣辩伆
- LOCKED: 0xF56C6C, // 閿佸畾 - 绾㈣壊
- EMPTY: 0x4a4a4a, // 绌鸿揣浣� - 鏆楃伆
- HAS_STOCK: 0x409EFF, // 鏈夎揣 - 钃濊壊
- LOW_STOCK: 0xE6A23C, // 搴撳瓨绱у紶 - 姗欒壊
- FULL: 0x67C23A, // 宸叉弧 - 缁胯壊
+ // 璐т綅鐘舵�侀鑹�
+ LOC_FREE: 0x90EE90, // 0=绌洪棽 - 娴呯豢鑹�
+ LOC_LOCK: 0xFF6B6B, // 1=閿佸畾 - 绾㈣壊
+ LOC_INSTOCK_LOCK: 0xFFA500, // 10=鏈夎揣閿佸畾 - 姗欒壊
+ LOC_FREE_LOCK: 0xFFD700, // 20=绌洪棽閿佸畾 - 閲戣壊
+ LOC_PALLET_LOCK: 0x9370DB, // 99=澶ф墭鐩橀攣瀹� - 绱壊
+ LOC_INSTOCK: 0x409EFF, // 100=鏈夎揣 - 钃濊壊
+
+ // 搴撳瓨鐘舵�侀鑹�
+ STOCK_PENDING: 0x00CED1, // 1=缁勭洏鏆傚瓨 - 娣遍潚鑹�
+ STOCK_CONFIRMED: 0x87CEEB, // 3=鍏ュ簱纭 - 澶╄摑鑹�
+ STOCK_COMPLETED: 0x32CD32, // 6=鍏ュ簱瀹屾垚 - 浜豢鑹�
+ STOCK_OUT_LOCK: 0xFF6347, // 7=鍑哄簱閿佸畾 - 鐣寗绾�
+ STOCK_OUT_COMPLETED: 0x228B22, // 8=鍑哄簱瀹屾垚 - 妫灄缁�
+ STOCK_TRANSFER_LOCK: 0xFF8C00, // 9=绉诲簱閿佸畾 - 娣辨鑹�
+ STOCK_COMPLETED_NO_ORDER: 0x20B2AA, // 10=鍏ュ簱瀹屾垚鏈缓鍑哄簱鍗�
+ STOCK_RETURN: 0xFF4500, // 11=閫�搴�
+ STOCK_MANUAL_PENDING: 0x48D1CC, // 12=鎵嬪姩缁勭洏鏆傚瓨
+ STOCK_MANUAL_CONFIRMED: 0x7FFFD4, // 13=鎵嬪姩缁勭洏鍏ュ簱纭
+ STOCK_PICK_COMPLETED: 0x6B8E23, // 14=鎷i�夊畬鎴�
+ STOCK_MES_RETURN: 0xDC143C, // 21=MES閫�搴�
+ STOCK_EMPTY_TRAY: 0xDDA0DD, // 22=绌烘墭鐩樺簱瀛� - 姊呯孩鑹�
+ STOCK_GROUP_CANCEL: 0xDEB887, // 99=缁勭洏鎾ら攢 - 鏆楅噾鑹�
+ STOCK_IN_CANCEL: 0xA0522D, // 199=鍏ュ簱鎾ら攢 - 璧壊
}
-// 鍥句緥椤�
+// 鍥句緥椤� - 璐т綅鐘舵��
const legendItems = [
- { status: 'disabled', label: '绂佺敤', color: '#2d2d2d' },
- { status: 'locked', label: '閿佸畾', color: '#F56C6C' },
- { status: 'empty', label: '绌鸿揣浣�', color: '#4a4a4a' },
- { status: 'hasStock', label: '鏈夎揣', color: '#409EFF' },
- { status: 'lowStock', label: '搴撳瓨绱у紶', color: '#E6A23C' },
- { status: 'full', label: '宸叉弧', color: '#67C23A' },
+ { status: 'loc_free', label: '绌洪棽(0)', color: '#90EE90' },
+ { status: 'loc_lock', label: '閿佸畾(1)', color: '#FF6B6B' },
+ { status: 'loc_instock_lock', label: '鏈夎揣閿佸畾(10)', color: '#FFA500' },
+ { status: 'loc_free_lock', label: '绌洪棽閿佸畾(20)', color: '#FFD700' },
+ { status: 'loc_pallet_lock', label: '澶ф墭鐩橀攣瀹�(99)', color: '#9370DB' },
+ { status: 'loc_instock', label: '鏈夎揣(100)', color: '#409EFF' },
]
// Refs
@@ -99,12 +137,15 @@
const batchNoList = ref([])
const detailDialogVisible = ref(false)
const selectedLocation = ref(null)
+const refreshing = ref(false)
// Three.js 鐩稿叧
let scene, camera, renderer, controls, raycaster, mouse
let locationMesh = null
let locationData = []
+let originalLocationData = [] // 淇濆瓨鍘熷瀹屾暣鏁版嵁锛岀敤浜庣瓫閫夋仮澶�
let animationId = null
+let locationIdToInstanceId = new Map() // locationId -> instanceId 鏄犲皠
// SignalR 鍒濆鍖�
function initSignalR() {
@@ -117,51 +158,100 @@
connection.start().catch((err) => console.log('SignalR杩炴帴澶辫触:', err));
connection.on('StockUpdated', (update) => {
+ console.log('鏀跺埌搴撳瓨鏇存柊:', update)
// 鏇存柊瀵瑰簲璐т綅鐨勬暟鎹�
const idx = locationData.findIndex(x => x.locationId === update.locationId);
if (idx !== -1) {
locationData[idx].stockQuantity = update.stockQuantity;
locationData[idx].stockStatus = update.stockStatus;
- // 閲嶆柊娓叉煋鍗曚釜璐т綅棰滆壊
- updateInstanceColor(idx, update.stockStatus);
+ locationData[idx].palletCode = update.palletCode;
+ locationData[idx].locationStatus = update.locationStatus;
+ // 鏇存柊搴撳瓨鏄庣粏
+ if (update.details && update.details.length > 0) {
+ locationData[idx].details = update.details;
+ }
+ // 閫氳繃鏄犲皠鎵惧埌瀹炰緥ID锛屾洿鏂伴鑹�
+ const instanceId = locationIdToInstanceId.get(update.locationId);
+ if (instanceId !== undefined) {
+ updateInstanceColor(instanceId, update.locationStatus);
+ }
}
});
});
}
// 鏇存柊鍗曚釜璐т綅棰滆壊
-function updateInstanceColor(instanceId, stockStatus) {
+function updateInstanceColor(instanceId, locationStatus) {
if (!locationMesh) return;
- const loc = locationData[instanceId];
- if (!loc) return;
- const color = getLocationColor(loc);
+ // 鏍规嵁璐т綅鐘舵�佽幏鍙栭鑹�
+ const color = getLocationColorByStatus(locationStatus);
locationMesh.setColorAt(instanceId, new THREE.Color(color));
locationMesh.instanceColor.needsUpdate = true;
}
-// 鑾峰彇璐т綅棰滆壊
-function getLocationColor(location) {
- if (location.locationStatus === 3) return COLOR_MAP.DISABLED // 绂佺敤
- if (location.locationStatus === 2) return COLOR_MAP.LOCKED // 閿佸畾
- if (location.locationStatus === 1) {
- if (location.stockStatus === 0) return COLOR_MAP.EMPTY // 鏃犺揣
- if (location.stockStatus === 1) return COLOR_MAP.HAS_STOCK // 鏈夎揣
- if (location.stockStatus === 2) return COLOR_MAP.LOW_STOCK // 搴撳瓨绱у紶
- if (location.stockStatus === 3) return COLOR_MAP.FULL // 宸叉弧
+// 鏍规嵁璐т綅鐘舵�佽幏鍙栭鑹�
+function getLocationColorByStatus(locStatus) {
+ switch (locStatus) {
+ case 0: return COLOR_MAP.LOC_FREE // 绌洪棽
+ case 1: return COLOR_MAP.LOC_LOCK // 閿佸畾
+ case 10: return COLOR_MAP.LOC_INSTOCK_LOCK // 鏈夎揣閿佸畾
+ case 20: return COLOR_MAP.LOC_FREE_LOCK // 绌洪棽閿佸畾
+ case 99: return COLOR_MAP.LOC_PALLET_LOCK // 澶ф墭鐩橀攣瀹�
+ case 100: return COLOR_MAP.LOC_INSTOCK // 鏈夎揣
+ default: return COLOR_MAP.LOC_FREE // 榛樿绌洪棽鑹�
}
- return COLOR_MAP.EMPTY // 榛樿绌�
+}
+
+// 鑾峰彇璐т綅棰滆壊 - 鍙牴鎹揣浣嶇姸鎬�
+function getLocationColor(location) {
+ const locStatus = location.locationStatus
+
+ // 鏍规嵁璐т綅鐘舵�佸垽鏂鑹�
+ switch (locStatus) {
+ case 0: return COLOR_MAP.LOC_FREE // 绌洪棽
+ case 1: return COLOR_MAP.LOC_LOCK // 閿佸畾
+ case 10: return COLOR_MAP.LOC_INSTOCK_LOCK // 鏈夎揣閿佸畾
+ case 20: return COLOR_MAP.LOC_FREE_LOCK // 绌洪棽閿佸畾
+ case 99: return COLOR_MAP.LOC_PALLET_LOCK // 澶ф墭鐩橀攣瀹�
+ case 100: return COLOR_MAP.LOC_INSTOCK // 鏈夎揣
+ default: return COLOR_MAP.LOC_FREE // 榛樿绌洪棽鑹�
+ }
}
// 鑾峰彇璐т綅鐘舵�佹枃鏈�
function getLocationStatusText(status) {
- const map = { 0: '姝e父', 1: '姝e父', 2: '閿佸畾', 3: '绂佺敤' }
- return map[status] || '鏈煡'
+ const map = {
+ 0: '绌洪棽',
+ 1: '閿佸畾',
+ 10: '鏈夎揣閿佸畾',
+ 20: '绌洪棽閿佸畾',
+ 99: '澶ф墭鐩橀攣瀹�',
+ 100: '鏈夎揣'
+ }
+ return map[status] || '鏈煡(' + status + ')'
}
// 鑾峰彇搴撳瓨鐘舵�佹枃鏈�
function getStockStatusText(status) {
- const map = { 0: '鏃犺揣', 1: '鏈夎揣', 2: '搴撳瓨绱у紶', 3: '宸叉弧' }
- return map[status] || '鏈煡'
+ const map = {
+ 0: '鏃犲簱瀛�',
+ 1: '缁勭洏鏆傚瓨',
+ 3: '鍏ュ簱纭',
+ 6: '鍏ュ簱瀹屾垚',
+ 7: '鍑哄簱閿佸畾',
+ 8: '鍑哄簱瀹屾垚',
+ 9: '绉诲簱閿佸畾',
+ 10: '鍏ュ簱瀹屾垚鏈缓鍑哄簱鍗�',
+ 11: '閫�搴�',
+ 12: '鎵嬪姩缁勭洏鏆傚瓨',
+ 13: '鎵嬪姩缁勭洏鍏ュ簱纭',
+ 14: '鎷i�夊畬鎴�',
+ 21: 'MES閫�搴�',
+ 22: '绌烘墭鐩樺簱瀛�',
+ 99: '缁勭洏鎾ら攢',
+ 199: '鍏ュ簱鎾ら攢'
+ }
+ return map[status] || '鏈煡(' + status + ')'
}
// 鍔犺浇浠撳簱鍒楄〃
@@ -184,15 +274,13 @@
async function loadWarehouseData(warehouseId) {
try {
const res = await proxy.http.get(`/api/StockInfo/Get3DLayout?warehouseId=${warehouseId}`)
- console.log('Get3DLayout response:', res)
if (res.status && res.data) {
const data = res.data
- console.log('data.locations:', data.locations)
- locationData = data.locations || []
+ originalLocationData = data.locations || [] // 淇濆瓨鍘熷瀹屾暣鏁版嵁
+ locationData = [...originalLocationData] // 褰撳墠鏄剧ず鏁版嵁
// 浣跨敤鍚庣杩斿洖鐨勭瓫閫夊垪琛�
materielCodeList.value = data.materielCodeList || []
batchNoList.value = data.batchNoList || []
- console.log('locationData set:', locationData.length, 'items')
// 娓叉煋璐т綅
renderLocations()
}
@@ -215,6 +303,7 @@
// 鍒涘缓鐩告満
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(20, 20, 20)
+ camera.lookAt(0, 0, 0)
// 鍒涘缓娓叉煋鍣�
renderer = new THREE.WebGLRenderer({ antialias: true })
@@ -228,13 +317,18 @@
controls.dampingFactor = 0.05
// 鍒涘缓鐜鍏�
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
- // 鍒涘缓瀹氬悜鍏�
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
- directionalLight.position.set(10, 20, 10)
+ // 鍒涘缓涓诲畾鍚戝厜
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0)
+ directionalLight.position.set(50, 100, 50)
scene.add(directionalLight)
+
+ // 鍒涘缓琛ュ厜
+ const fillLight = new THREE.DirectionalLight(0xffffff, 0.3)
+ fillLight.position.set(-50, 50, -50)
+ scene.add(fillLight)
// 鍒涘缓鍦伴潰
const groundGeometry = new THREE.PlaneGeometry(100, 100)
@@ -253,7 +347,7 @@
mouse = new THREE.Vector2()
// 娣诲姞鐐瑰嚮浜嬩欢
- canvasContainer.value.addEventListener('click', onCanvasClick)
+ canvasContainer.value.addEventListener('mousedown', onCanvasClick)
// 寮�濮嬫覆鏌撳惊鐜�
animate()
@@ -261,19 +355,32 @@
// 娓叉煋璐т綅
function renderLocations() {
- console.log('renderLocations called', { scene: !!scene, locationDataLength: locationData.length })
if (!scene) return
+ console.log('娓叉煋璐т綅锛屽師濮嬫暟鎹�绘暟:', originalLocationData.length)
+ // 濡傛灉鍘熷鏁版嵁涓虹┖锛屽皾璇曢噸鏂板姞杞�
+ if (originalLocationData.length === 0) {
+ console.warn('鍘熷鏁版嵁涓虹┖锛岄噸鏂板姞杞�...')
+ if (activeWarehouse.value) {
+ loadWarehouseData(activeWarehouse.value)
+ }
+ return
+ }
// 绉婚櫎鏃х殑璐т綅缃戞牸
+ console.log("馃殌 ~ renderLocations ~ locationMesh:", locationMesh)
if (locationMesh) {
scene.remove(locationMesh)
locationMesh.geometry.dispose()
- locationMesh.material.forEach(m => m.dispose())
+ if (Array.isArray(locationMesh.material)) {
+ locationMesh.material.forEach(m => m.dispose())
+ } else {
+ locationMesh.material.dispose()
+ }
locationMesh = null
}
- // 杩囨护鏁版嵁
- let filteredData = [...locationData]
+ // 杩囨护鏁版嵁 - 濮嬬粓浠庡師濮嬪畬鏁存暟鎹繃婊�
+ let filteredData = [...originalLocationData]
if (filterStockStatus.value !== null) {
filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
}
@@ -284,14 +391,27 @@
filteredData = filteredData.filter(loc => loc.batchNo === filterBatchNo.value)
}
- console.log('filteredData length:', filteredData.length)
- if (filteredData.length === 0) return
+ console.log('杩囨护鍚庢暟鎹暟閲�:', filteredData.length, '绛涢�夋潯浠�:', filterStockStatus.value, filterMaterielCode.value, filterBatchNo.value)
// 鍒涘缓 InstancedMesh
const geometry = new THREE.BoxGeometry(1.5, 1, 1.5)
- const material = new THREE.MeshStandardMaterial({ color: 0xffffff })
+ const material = new THREE.MeshStandardMaterial({
+ color: 0xffffff,
+ roughness: 0.5,
+ metalness: 0.1
+ })
- locationMesh = new THREE.InstancedMesh(geometry, [material], filteredData.length)
+ // 濡傛灉杩囨护鍚庢棤鏁版嵁锛屽垱寤虹┖鐨� InstancedMesh
+ if (filteredData.length === 0) {
+ locationMesh = new THREE.InstancedMesh(geometry, material, 0)
+ scene.add(locationMesh)
+ return
+ }
+
+ locationMesh = new THREE.InstancedMesh(geometry, material, filteredData.length)
+
+ // 娓呯┖骞堕噸寤烘槧灏�
+ locationIdToInstanceId.clear()
const dummy = new THREE.Object3D()
const color = new THREE.Color()
@@ -309,6 +429,9 @@
color.setHex(getLocationColor(location))
locationMesh.setColorAt(i, color)
+ // 寤虹珛鏄犲皠: locationId -> instanceId (i)
+ locationIdToInstanceId.set(location.locationId, i)
+
if (i === 0) {
console.log('First location:', location, { x, y, z })
}
@@ -318,7 +441,6 @@
if (locationMesh.instanceColor) locationMesh.instanceColor.needsUpdate = true
scene.add(locationMesh)
- console.log('locationMesh added to scene')
}
// 鍔ㄧ敾寰幆
@@ -342,7 +464,7 @@
if (intersects.length > 0) {
const instanceId = intersects[0].instanceId
// 鑾峰彇鍘熷鏁版嵁绱㈠紩锛堣�冭檻杩囨护鍚庣殑鏁版嵁锛�
- let filteredData = locationData
+ let filteredData = [...originalLocationData]
if (filterStockStatus.value !== null) {
filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
}
@@ -404,15 +526,47 @@
controls.target.set(0, 0, 0)
}
+// 鍒锋柊鏁版嵁
+async function refreshData() {
+ refreshing.value = true
+ try {
+ // 閲嶇疆绛涢�夋潯浠�
+ filterStockStatus.value = null
+ filterMaterielCode.value = null
+ filterBatchNo.value = null
+ // 閲嶆柊鍔犺浇褰撳墠浠撳簱鏁版嵁
+ if (activeWarehouse.value) {
+ await loadWarehouseData(activeWarehouse.value)
+ }
+ } finally {
+ refreshing.value = false
+ }
+}
+
// 浠撳簱鍒囨崲
async function onWarehouseChange(warehouseId) {
await loadWarehouseData(warehouseId)
}
// 鐩戝惉绛涢�夊彉鍖�
-watch(filterStockStatus, () => renderLocations())
-watch(filterMaterielCode, () => renderLocations())
-watch(filterBatchNo, () => renderLocations())
+watch(filterStockStatus, async () => {
+ await nextTick()
+ if (originalLocationData.length > 0) {
+ renderLocations()
+ }
+})
+watch(filterMaterielCode, async () => {
+ await nextTick()
+ if (originalLocationData.length > 0) {
+ renderLocations()
+ }
+})
+watch(filterBatchNo, async () => {
+ await nextTick()
+ if (originalLocationData.length > 0) {
+ renderLocations()
+ }
+})
// 绐楀彛澶у皬鍙樺寲
function onWindowResize() {
@@ -438,7 +592,7 @@
cancelAnimationFrame(animationId)
}
if (canvasContainer.value) {
- canvasContainer.value.removeEventListener('click', onCanvasClick)
+ canvasContainer.value.removeEventListener('mousedown', onCanvasClick)
}
window.removeEventListener('resize', onWindowResize)
if (renderer) {
@@ -455,6 +609,7 @@
width: 100%;
height: calc(100vh - 120px);
position: relative;
+ overflow: visible;
}
.toolbar {
position: absolute;
@@ -466,10 +621,12 @@
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.canvas-container {
width: 100%;
height: 100%;
+ overflow: hidden;
}
.legend {
position: absolute;
@@ -480,19 +637,48 @@
border-radius: 4px;
color: white;
z-index: 10;
+ max-height: 400px;
+ overflow-y: auto;
+}
+.legend-title {
+ font-weight: bold;
+ font-size: 13px;
+ margin-bottom: 6px;
+ color: #fff;
+}
+.legend-divider {
+ height: 1px;
+ background: rgba(255, 255, 255, 0.3);
+ margin: 8px 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
+ font-size: 12px;
}
.color-box {
width: 16px;
height: 16px;
border-radius: 2px;
+ flex-shrink: 0;
}
.detail-content {
padding: 20px;
}
+.detail-table {
+ margin-top: 20px;
+}
+.detail-table h4 {
+ margin-bottom: 10px;
+ color: #303133;
+}
+.no-detail {
+ margin-top: 20px;
+}
+
+:deep(.el-drawer__body) {
+ padding: 20px;
+}
</style>
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
index ff752be..a2aaed2 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs
@@ -77,12 +77,12 @@
public int Layer { get; set; }
/// <summary>
- /// 璐т綅鐘舵�� (0=绌�, 1=鍗犵敤, 2=閿佸畾, 3=绂佺敤)
+ /// 璐т綅鐘舵��
/// </summary>
public int LocationStatus { get; set; }
/// <summary>
- /// 搴撳瓨鐘舵�� (0=鏃犺揣, 1=鏈夎揣, 2=搴撳瓨绱у紶, 3=宸叉弧)
+ /// 搴撳瓨鐘舵��
/// </summary>
public int StockStatus { get; set; }
@@ -115,5 +115,27 @@
/// 鎵规鍙�
/// </summary>
public string? BatchNo { get; set; }
+
+ /// <summary>
+ /// 搴撳瓨鏄庣粏鍒楄〃
+ /// </summary>
+ public List<StockDetailItemDTO> Details { get; set; } = new();
+ }
+
+ /// <summary>
+ /// 搴撳瓨鏄庣粏椤笵TO
+ /// </summary>
+ public class StockDetailItemDTO
+ {
+ public int Id { get; set; }
+ public string? MaterielCode { get; set; }
+ public string? MaterielName { get; set; }
+ public string? BatchNo { get; set; }
+ public float StockQuantity { get; set; }
+ public string? Unit { get; set; }
+ public string? ProductionDate { get; set; }
+ public string? EffectiveDate { get; set; }
+ public string? OrderNo { get; set; }
+ public int Status { get; set; }
}
}
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
index d992bdf..81f3390 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs
@@ -107,7 +107,7 @@
var locations = await _locationInfoService.Repository.QueryDataAsync(x => x.WarehouseId == warehouseId);
// 3. 鏌ヨ璇ヤ粨搴撴墍鏈夊簱瀛樹俊鎭紙鍖呭惈Details瀵艰埅灞炴�э級
- var stockInfos = await Repository.QueryDataNavAsync(x => x.WarehouseId == warehouseId);
+ var stockInfos = await Repository.QueryDataNavAsync(x => x.WarehouseId == warehouseId && x.LocationId != 0);
// 4. 鎻愬彇鐗╂枡缂栧彿鍜屾壒娆″彿鍒楄〃锛堝幓閲嶏級
var materielCodeList = stockInfos
@@ -145,34 +145,52 @@
};
// 灏濊瘯浠庡簱瀛樺瓧鍏镐腑鑾峰彇搴撳瓨淇℃伅
- if (stockDict.TryGetValue(loc.Id, out var stockInfo) && stockInfo.Details != null)
+ if (stockDict.TryGetValue(loc.Id, out var stockInfo))
{
+ // 绌烘墭鐩樹篃鏈夊簱瀛樿褰曪紝鍙槸涓嶅寘鍚槑缁�
item.PalletCode = stockInfo.PalletCode;
- item.StockQuantity = stockInfo.Details.Sum(d => d.StockQuantity);
+ item.StockStatus = stockInfo.StockStatus; // 鐩存帴浣跨敤鍚庣搴撳瓨鐘舵��
- // 鑾峰彇绗竴涓槑缁嗙殑鐗╂枡淇℃伅锛堝鏋滃瓨鍦級
- var firstDetail = stockInfo.Details.FirstOrDefault();
- if (firstDetail != null)
+ // 鍙湁褰揇etails涓嶄负null涓旀湁鏁版嵁鏃舵墠澶勭悊搴撳瓨鏄庣粏
+ if (stockInfo.Details != null && stockInfo.Details.Any())
{
- item.MaterielCode = firstDetail.MaterielCode;
- item.MaterielName = firstDetail.MaterielName;
- item.BatchNo = firstDetail.BatchNo;
- }
+ item.StockQuantity = stockInfo.Details.Sum(d => d.StockQuantity);
- // 璁$畻搴撳瓨鐘舵��
- var ratio = item.MaxCapacity > 0 ? item.StockQuantity / item.MaxCapacity : 0;
- if (ratio >= 0.9f)
- item.StockStatus = 3; // 宸叉弧 (FULL)
- else if (ratio >= 0.1f)
- item.StockStatus = 1; // 鏈夎揣 (HAS_STOCK)
- else if (ratio > 0)
- item.StockStatus = 2; // 搴撳瓨绱у紶 (LOW_STOCK)
+ // 鑾峰彇绗竴涓槑缁嗙殑鐗╂枡淇℃伅锛堝鏋滃瓨鍦級
+ var firstDetail = stockInfo.Details.FirstOrDefault();
+ if (firstDetail != null)
+ {
+ item.MaterielCode = firstDetail.MaterielCode;
+ item.MaterielName = firstDetail.MaterielName;
+ item.BatchNo = firstDetail.BatchNo;
+ }
+
+ // 濉厖搴撳瓨鏄庣粏鍒楄〃
+ item.Details = stockInfo.Details.Select(d => new StockDetailItemDTO
+ {
+ Id = d.Id,
+ MaterielCode = d.MaterielCode,
+ MaterielName = d.MaterielName,
+ BatchNo = d.BatchNo,
+ StockQuantity = d.StockQuantity,
+ Unit = d.Unit,
+ ProductionDate = d.ProductionDate,
+ EffectiveDate = d.EffectiveDate,
+ OrderNo = d.OrderNo,
+ Status = d.Status
+ }).ToList();
+ }
else
- item.StockStatus = 0; // 鏃犺揣 (EMPTY)
+ {
+ // 绌烘墭鐩橈紙鏃犳槑缁嗭級
+ item.StockQuantity = 0;
+ item.Details = new List<StockDetailItemDTO>(); // 纭繚鏄┖鍒楄〃鑰岄潪null
+ }
}
else
{
- item.StockStatus = 0; // 鏃犺揣 (EMPTY)
+ // 鏃犲簱瀛樿褰曪紝璐т綅涓虹┖
+ item.StockStatus = 0; // 绌洪棽
item.StockQuantity = 0;
}
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs
new file mode 100644
index 0000000..074c48e
--- /dev/null
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs
@@ -0,0 +1,199 @@
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using WIDESEA_Core.BaseRepository;
+using WIDESEA_IStockService;
+using WIDESEA_Model.Models;
+using WIDESEA_WMSServer.Hubs;
+
+namespace WIDESEA_WMSServer.BackgroundServices
+{
+ /// <summary>
+ /// 搴撳瓨鐩戞帶鍚庡彴鏈嶅姟
+ /// 瀹氭湡妫�鏌ュ簱瀛樺拰璐т綅鏁版嵁鍙樺寲骞堕�氳繃SignalR鎺ㄩ�佸埌鍓嶇
+ /// </summary>
+ public class StockMonitorBackgroundService : BackgroundService
+ {
+ private readonly ILogger<StockMonitorBackgroundService> _logger;
+ private readonly IHubContext<StockHub> _hubContext;
+ private readonly IServiceProvider _serviceProvider;
+
+ // 璐т綅鐘舵�佸揩鐓э細key = LocationId
+ private ConcurrentDictionary<int, LocationSnapshot> _lastLocationSnapshots = new();
+
+ // 鐩戞帶闂撮殧锛堟绉掞級
+ private const int MonitorIntervalMs = 3000;
+
+ public StockMonitorBackgroundService(
+ ILogger<StockMonitorBackgroundService> logger,
+ IHubContext<StockHub> hubContext,
+ IServiceProvider serviceProvider)
+ {
+ _logger = logger;
+ _hubContext = hubContext;
+ _serviceProvider = serviceProvider;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("搴撳瓨鐩戞帶鍚庡彴鏈嶅姟宸插惎鍔�");
+
+ // 绛夊緟搴旂敤瀹屽叏鍚姩
+ await Task.Delay(5000, stoppingToken);
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await CheckChangesAsync();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "妫�鏌ユ暟鎹彉鍖栨椂鍙戠敓閿欒");
+ }
+
+ await Task.Delay(MonitorIntervalMs, stoppingToken);
+ }
+
+ _logger.LogInformation("搴撳瓨鐩戞帶鍚庡彴鏈嶅姟宸插仠姝�");
+ }
+
+ /// <summary>
+ /// 妫�鏌ヨ揣浣嶅拰搴撳瓨鍙樺寲
+ /// </summary>
+ private async Task CheckChangesAsync()
+ {
+ using var scope = _serviceProvider.CreateScope();
+ var stockService = scope.ServiceProvider.GetRequiredService<IStockInfoService>();
+ var locationRepo = scope.ServiceProvider.GetRequiredService<IRepository<Dt_LocationInfo>>();
+
+ // 1. 鑾峰彇鎵�鏈夎揣浣嶆暟鎹�
+ var allLocations = await locationRepo.QueryDataAsync(x => x.LocationStatus != 99); // 鎺掗櫎绂佺敤鐨勮揣浣�
+
+ // 2. 鑾峰彇鎵�鏈夊簱瀛樻暟鎹紙鍖呭惈鏄庣粏锛�
+ var allStockData = await stockService.Repository.Db.Queryable<Dt_StockInfo>()
+ .Includes(x => x.Details)
+ .ToListAsync();
+
+ // 鏋勫缓搴撳瓨瀛楀吀锛歀ocationId -> StockInfo
+ var stockDict = allStockData
+ .Where(s => s.LocationId > 0)
+ .ToDictionary(s => s.LocationId, s => s);
+
+ // 鏋勫缓褰撳墠璐т綅蹇収瀛楀吀
+ var currentSnapshots = new ConcurrentDictionary<int, LocationSnapshot>();
+
+ foreach (var location in allLocations)
+ {
+ // 鑾峰彇璇ヨ揣浣嶇殑搴撳瓨淇℃伅
+ stockDict.TryGetValue(location.Id, out var stock);
+
+ // 璁$畻搴撳瓨鏁伴噺
+ float totalQuantity = 0;
+ string detailsHash = string.Empty;
+ if (stock?.Details != null && stock.Details.Any())
+ {
+ totalQuantity = stock.Details.Sum(d => d.StockQuantity);
+ detailsHash = GenerateDetailsHash(stock.Details.ToList());
+ }
+
+ var snapshot = new LocationSnapshot
+ {
+ LocationId = location.Id,
+ WarehouseId = location.WarehouseId,
+ LocationCode = location.LocationCode,
+ LocationStatus = location.LocationStatus,
+ PalletCode = stock?.PalletCode,
+ StockStatus = stock?.StockStatus ?? 0,
+ StockQuantity = totalQuantity,
+ DetailsHash = detailsHash
+ };
+
+ currentSnapshots.TryAdd(location.Id, snapshot);
+
+ // 妫�鏌ユ槸鍚︽湁鍙樺寲
+ if (_lastLocationSnapshots.TryGetValue(location.Id, out var lastSnapshot))
+ {
+ // 妫�娴嬪彉鍖栵細璐т綅鐘舵�併�佸簱瀛樼姸鎬併�佹暟閲忋�佹槑缁嗗彉鍖�
+ if (lastSnapshot.LocationStatus != snapshot.LocationStatus ||
+ lastSnapshot.StockStatus != snapshot.StockStatus ||
+ lastSnapshot.PalletCode != snapshot.PalletCode ||
+ Math.Abs(lastSnapshot.StockQuantity - snapshot.StockQuantity) > 0.001f ||
+ lastSnapshot.DetailsHash != snapshot.DetailsHash)
+ {
+ // 鏋勫缓鏇存柊DTO
+ var update = new StockUpdateDTO
+ {
+ LocationId = snapshot.LocationId,
+ WarehouseId = snapshot.WarehouseId,
+ PalletCode = snapshot.PalletCode,
+ StockQuantity = snapshot.StockQuantity,
+ StockStatus = snapshot.StockStatus,
+ LocationStatus = snapshot.LocationStatus,
+ Details = BuildDetailDtos(stock?.Details?.ToList())
+ };
+
+ await _hubContext.Clients.All.SendAsync("StockUpdated", update);
+ _logger.LogDebug("鏁版嵁鍙樺寲鎺ㄩ��: LocationId={LocationId}, LocStatus={LocStatus}, StockStatus={StockStatus}, Quantity={Quantity}",
+ snapshot.LocationId, snapshot.LocationStatus, snapshot.StockStatus, snapshot.StockQuantity);
+ }
+ }
+ }
+
+ // 鏇存柊蹇収鏁版嵁
+ _lastLocationSnapshots = currentSnapshots;
+ }
+
+ /// <summary>
+ /// 鐢熸垚鏄庣粏鏁版嵁鍝堝笇
+ /// </summary>
+ private string GenerateDetailsHash(List<Dt_StockInfoDetail> details)
+ {
+ if (details == null || !details.Any()) return string.Empty;
+
+ var hashString = string.Join("|", details
+ .OrderBy(d => d.Id)
+ .Select(d => $"{d.Id}:{d.MaterielCode}:{d.BatchNo}:{d.StockQuantity}"));
+ return hashString.GetHashCode().ToString();
+ }
+
+ /// <summary>
+ /// 鏋勫缓鏄庣粏DTO鍒楄〃
+ /// </summary>
+ private List<StockDetailUpdateDTO> BuildDetailDtos(List<Dt_StockInfoDetail> details)
+ {
+ if (details == null || !details.Any()) return new List<StockDetailUpdateDTO>();
+
+ return details.Select(d => new StockDetailUpdateDTO
+ {
+ Id = d.Id,
+ MaterielCode = d.MaterielCode,
+ MaterielName = d.MaterielName,
+ BatchNo = d.BatchNo,
+ StockQuantity = d.StockQuantity,
+ Unit = d.Unit,
+ Status = d.Status
+ }).ToList();
+ }
+
+ /// <summary>
+ /// 璐т綅蹇収
+ /// </summary>
+ private class LocationSnapshot
+ {
+ public int LocationId { get; set; }
+ public int WarehouseId { get; set; }
+ public string LocationCode { get; set; }
+ public int LocationStatus { get; set; }
+ public string PalletCode { get; set; }
+ public int StockStatus { get; set; }
+ public float StockQuantity { get; set; }
+ public string DetailsHash { get; set; }
+ }
+ }
+}
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs
index d5208b1..af48512 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.SignalR;
+using System.Collections.Generic;
using System.Threading.Tasks;
namespace WIDESEA_WMSServer.Hubs
@@ -14,11 +15,31 @@
}
}
+ /// <summary>
+ /// 搴撳瓨鏇存柊DTO锛圫ignalR鎺ㄩ�佺敤锛�
+ /// </summary>
public class StockUpdateDTO
{
public int LocationId { get; set; }
public int WarehouseId { get; set; }
+ public string PalletCode { get; set; }
public float StockQuantity { get; set; }
public int StockStatus { get; set; }
+ public int LocationStatus { get; set; }
+ public List<StockDetailUpdateDTO> Details { get; set; } = new();
+ }
+
+ /// <summary>
+ /// 搴撳瓨鏄庣粏鏇存柊DTO
+ /// </summary>
+ public class StockDetailUpdateDTO
+ {
+ public int Id { get; set; }
+ public string MaterielCode { get; set; }
+ public string MaterielName { get; set; }
+ public string BatchNo { get; set; }
+ public float StockQuantity { get; set; }
+ public string Unit { get; set; }
+ public int Status { get; set; }
}
}
\ No newline at end of file
diff --git a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
index 59cbd0a..a3e0f16 100644
--- a/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
+++ b/Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
@@ -81,6 +81,7 @@
builder.Services.AddDbSetup(); // Db 鏁版嵁搴撻厤缃�
builder.Services.AddInitializationHostServiceSetup(); // 搴旂敤绋嬪簭鍒濆鍖栨湇鍔℃敞鍐�
builder.Services.AddHostedService<AutoOutboundTaskBackgroundService>(); // 鍚姩鑷姩鍑哄簱浠诲姟鍚庡彴鏈嶅姟
+builder.Services.AddHostedService<StockMonitorBackgroundService>(); // 鍚姩搴撳瓨鐩戞帶鍚庡彴鏈嶅姟
// builder.Services.AddHostedService<PermissionDataHostService>(); // 鏉冮檺鏁版嵁鏈嶅姟
builder.Services.AddAutoMapperSetup();
diff --git a/Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md b/Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md
new file mode 100644
index 0000000..7cddf9c
--- /dev/null
+++ b/Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md
@@ -0,0 +1,314 @@
+# 搴撳瓨3D鏌ョ湅鍣� Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** 瀹炵幇搴撳瓨3D鏌ョ湅鍣紝鐢ㄦ埛鍙湪 Three.js 3D 鍦烘櫙涓贰瑙嗕粨搴撱�佺偣鍑昏揣浣嶆煡鐪嬪簱瀛樿鎯�
+
+**Architecture:** 鍓嶇 Vue 3 + Element Plus + Three.js锛屽悗绔� ASP.NET Core 6 Web API + SignalR 瀹炴椂鎺ㄩ��
+
+**Tech Stack:** Three.js, @microsoft/signalr, Element Plus, Vue 3 Composition API
+
+---
+
+## 鏂囦欢缁撴瀯
+
+```
+鍚庣 (WIDESEA_WMSServer)
+鈹溾攢鈹� WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs # 3D甯冨眬鍝嶅簲DTO [鏂板缓]
+鈹溾攢鈹� WIDESEA_IStockService/IStockInfoService.cs # 娣诲姞Get3DLayoutAsync鏂规硶绛惧悕 [淇敼]
+鈹溾攢鈹� WIDESEA_StockService/StockInfoService.cs # 瀹炵幇Get3DLayoutAsync [淇敼]
+鈹溾攢鈹� WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs # 娣诲姞Get3DLayout绔偣 [淇敼]
+鈹斺攢鈹� WIDESEA_WMSServer/Hubs/StockHub.cs # SignalR Hub [鏂板缓]
+
+鍓嶇 (WIDESEA_WMSClient)
+鈹溾攢鈹� package.json # 娣诲姞three渚濊禆 [淇敼]
+鈹溾攢鈹� src/router/viewGird.js # 娉ㄥ唽璺敱 [淇敼]
+鈹溾攢鈹� src/views/stock/stockChat.vue # 涓婚〉闈㈢粍浠� [鏂板缓]
+鈹斺攢鈹� src/extension/stock/stockChat.js # 鎵╁睍閰嶇疆 [鏂板缓]
+```
+
+---
+
+## 瀹炵幇浠诲姟
+
+### Task 1: 鍚庣 - 鍒涘缓 Stock3DLayoutDTO
+
+**Files:**
+- Create: `WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+鍒涘缓涓や釜 DTO 绫伙細
+1. `Stock3DLayoutDTO` - 鍖呭惈浠撳簱鍩烘湰淇℃伅銆佸昂瀵搞�佺瓫閫夊垪琛ㄣ�佽揣浣嶆暟缁�
+2. `Location3DItemDTO` - 鍖呭惈鍗曚釜璐т綅鐨勬墍鏈�3D娓叉煋鎵�闇�鏁版嵁
+
+**楠屾敹鏍囧噯锛�**
+- DTO 鍖呭惈鎵�鏈� spec 涓畾涔夌殑瀛楁
+- 鍛藉悕绌洪棿姝g‘
+- 鍙互琚� Service 灞傛纭紩鐢�
+
+```csharp
+namespace WIDESEA_DTO.Stock
+{
+ /// <summary>
+ /// 浠撳簱3D甯冨眬鍝嶅簲DTO
+ /// </summary>
+ public class Stock3DLayoutDTO
+ {
+ public int WarehouseId { get; set; }
+ public string WarehouseName { get; set; }
+ public int MaxRow { get; set; }
+ public int MaxColumn { get; set; }
+ public int MaxLayer { get; set; }
+ public List<string> MaterielCodeList { get; set; } = new();
+ public List<string> BatchNoList { get; set; } = new();
+ public List<Location3DItemDTO> Locations { get; set; } = new();
+ }
+
+ /// <summary>
+ /// 璐т綅3D鏁版嵁椤�
+ /// </summary>
+ public class Location3DItemDTO
+ {
+ public int LocationId { get; set; }
+ public string LocationCode { get; set; }
+ public int Row { get; set; }
+ public int Column { get; set; }
+ public int Layer { get; set; }
+ public int LocationStatus { get; set; } // 0=绌�, 1=鍗犵敤, 2=閿佸畾, 3=绂佺敤
+ public int StockStatus { get; set; } // 0=鏃犺揣, 1=鏈夎揣, 2=搴撳瓨绱у紶, 3=宸叉弧
+ public float StockQuantity { get; set; }
+ public float MaxCapacity { get; set; }
+ public string? PalletCode { get; set; }
+ public string? MaterielCode { get; set; }
+ public string? MaterielName { get; set; }
+ public string? BatchNo { get; set; }
+ }
+}
+```
+
+---
+
+### Task 2: 鍚庣 - 鏇存柊 IStockInfoService 鎺ュ彛
+
+**Files:**
+- Modify: `WIDESEA_WMSServer/WIDESEA_IStockService/IStockInfoService.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+鍦ㄦ帴鍙d腑娣诲姞鏂规硶绛惧悕锛�
+```csharp
+/// <summary>
+/// 鑾峰彇浠撳簱3D甯冨眬鏁版嵁
+/// </summary>
+/// <param name="warehouseId">浠撳簱ID</param>
+/// <returns>3D甯冨眬DTO</returns>
+Task<Stock3DLayoutDTO> Get3DLayoutAsync(int warehouseId);
+```
+
+**楠屾敹鏍囧噯锛�**
+- 鏂规硶绛惧悕姝g‘
+- 娣诲姞浜嗘枃妗f敞閲�
+- 寮曠敤浜� Stock3DLayoutDTO
+
+---
+
+### Task 3: 鍚庣 - 瀹炵幇 Get3DLayoutAsync
+
+**Files:**
+- Modify: `WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+瀹炵幇 Get3DLayoutAsync 鏂规硶锛�
+1. 鏌ヨ浠撳簱淇℃伅
+2. 鏌ヨ璇ヤ粨搴撴墍鏈夎揣浣�
+3. 鏌ヨ搴撳瓨淇℃伅锛堝寘鍚槑缁嗭級
+4. 鎻愬彇鐗╂枡缂栧彿鍜屾壒娆″彿鍒楄〃
+5. 鏄犲皠鍒� Location3DItemDTO
+6. 璁$畻浠撳簱灏哄
+
+**楠屾敹鏍囧噯锛�**
+- 鏂规硶鑳芥纭繑鍥� Stock3DLayoutDTO
+- 鎵�鏈� locationStatus 鍜� stockStatus 鍊兼纭槧灏�
+- 鎬ц兘閫傚悎涓瀷浠撳簱锛�1000-5000璐т綅锛�
+
+---
+
+### Task 4: 鍚庣 - 娣诲姞 API 绔偣
+
+**Files:**
+- Modify: `WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Stock/StockInfoController.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+娣诲姞绔偣锛�
+```csharp
+/// <summary>
+/// 鑾峰彇浠撳簱3D甯冨眬
+/// </summary>
+/// <param name="warehouseId">浠撳簱ID</param>
+/// <returns>3D甯冨眬鏁版嵁</returns>
+[HttpGet("Get3DLayout")]
+public async Task<WebResponseContent> Get3DLayout(int warehouseId)
+{
+ var result = await Service.Get3DLayoutAsync(warehouseId);
+ return WebResponseContent.Instance.OK(result);
+}
+```
+
+**楠屾敹鏍囧噯锛�**
+- 璺敱姝g‘锛欸ET /api/StockInfo/Get3DLayout?warehouseId={id}
+- 杩斿洖鏍煎紡绗﹀悎 WebResponseContent 瑙勮寖
+
+---
+
+### Task 5: 鍚庣 - 鍒涘缓 SignalR Hub
+
+**Files:**
+- Create: `WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs`
+- Modify: `WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs`
+
+**璇︾粏瑙勮寖锛�**
+
+1. 鍒涘缓 StockHub 绫伙紝缁ф壙 Microsoft.AspNetCore.SignalR.Hub
+2. 娣诲姞 SendStockUpdate 鏂规硶渚涘閮ㄨ皟鐢�
+3. 鍦� Program.cs 涓敞鍐� SignalR 鏈嶅姟骞舵槧灏� Hub
+
+**楠屾敹鏍囧噯锛�**
+- Hub 鍙鍓嶇杩炴帴
+- SendStockUpdate 鏂规硶瀛樺湪涓斿彲琚皟鐢�
+
+---
+
+### Task 6: 鍓嶇 - 瀹夎 Three.js 渚濊禆
+
+**Files:**
+- Modify: `WIDESEA_WMSClient/package.json`
+
+**璇︾粏瑙勮寖锛�**
+
+娣诲姞 three 渚濊禆鍒� package.json锛�
+```json
+"three": "^0.160.0"
+```
+
+**楠屾敹鏍囧噯锛�**
+- package.json 鍖呭惈 three 渚濊禆
+- 鐗堟湰鍙峰悎鐞嗭紙^0.160.0 鎴栨洿鏂扮ǔ瀹氱増锛�
+
+---
+
+### Task 7: 鍓嶇 - 娉ㄥ唽璺敱
+
+**Files:**
+- Modify: `WIDESEA_WMSClient/src/router/viewGird.js`
+
+**璇︾粏瑙勮寖锛�**
+
+鍦� stockView 璺敱鍚庢坊鍔狅細
+```javascript
+{
+ path: '/stockChat',
+ name: 'stockChat',
+ component: () => import('@/views/stock/stockChat.vue')
+}
+```
+
+**楠屾敹鏍囧噯锛�**
+- 璺敱娉ㄥ唽姝g‘
+- 涓庡叾浠栬矾鐢辨牸寮忎竴鑷�
+
+---
+
+### Task 8: 鍓嶇 - 鍒涘缓 stockChat.vue 涓荤粍浠�
+
+**Files:**
+- Create: `WIDESEA_WMSClient/src/views/stock/stockChat.vue`
+
+**璇︾粏瑙勮寖锛�**
+
+缁勪欢蹇呴』鍖呭惈锛�
+1. 浠撳簱 Tabs锛坋l-tabs锛�
+2. 宸ュ叿鏍忥紙绛涢�� + 閲嶇疆瑙嗚鎸夐挳锛�
+3. 3D Canvas 瀹瑰櫒
+4. 鐘舵�佸浘渚�
+5. 璇︽儏寮圭獥锛坋l-dialog fullscreen锛�
+
+Three.js 鍦烘櫙锛�
+1. 鍦烘櫙鍒濆鍖栵紙鑳屾櫙鑹� 0x1a1a2e锛�
+2. 閫忚鐩告満
+3. WebGLRenderer
+4. OrbitControls锛堥樆灏煎惎鐢ㄧ殑杞ㄩ亾鎺у埗鍣級
+5. 鐜鍏� + 瀹氬悜鍏�
+6. 鍦伴潰锛圥laneGeometry锛岀綉鏍硷級
+7. InstancedMesh 鎵归噺娓叉煋璐т綅
+8. Raycaster 鐐瑰嚮鎷惧彇
+9. 鐩告満 lerp 鑱氱劍鍔ㄧ敾
+
+棰滆壊缂栫爜锛堝墠绔疄鐜帮級锛�
+- DISABLED(3): 0x2d2d2d
+- LOCKED(2): 0xF56C6C
+- EMPTY(0/鏃犺揣): 0x4a4a4a
+- HAS_STOCK(1): 0x409EFF
+- LOW_STOCK(2): 0xE6A23C
+- FULL(3): 0x67C23A
+
+**楠屾敹鏍囧噯锛�**
+- 椤甸潰鍙互姝e父鍔犺浇
+- Three.js 鍦烘櫙姝g‘鍒濆鍖�
+- 鐐瑰嚮璐т綅鑳芥樉绀鸿鎯呭脊绐�
+- 棰滆壊缂栫爜姝g‘
+
+---
+
+### Task 9: 鍓嶇 - 鍒涘缓鎵╁睍閰嶇疆鏂囦欢
+
+**Files:**
+- Create: `WIDESEA_WMSClient/src/extension/stock/stockChat.js`
+
+**璇︾粏瑙勮寖锛�**
+
+鍒涘缓鏍囧噯鎵╁睍鏂囦欢鏍煎紡锛�
+```javascript
+let extension = {
+ components: {
+ gridHeader: '',
+ gridBody: '',
+ gridFooter: '',
+ modelHeader: '',
+ modelBody: '',
+ modelFooter: ''
+ },
+ tableAction: '',
+ buttons: { view: [], box: [], detail: [] },
+ methods: {
+ onInit() {},
+ onInited() {}
+ }
+};
+export default extension;
+```
+
+**楠屾敹鏍囧噯锛�**
+- 绗﹀悎椤圭洰鐜版湁鎵╁睍鏂囦欢妯″紡
+
+---
+
+### Task 10: 鍓嶇 - 闆嗘垚 SignalR 瀹炴椂鏇存柊
+
+**Files:**
+- Modify: `WIDESEA_WMSClient/src/views/stock/stockChat.vue`
+
+**璇︾粏瑙勮寖锛�**
+
+1. 鍦� onMounted 涓垵濮嬪寲 SignalR 杩炴帴
+2. 杩炴帴 /stockHub
+3. 鐩戝惉 StockUpdated 浜嬩欢
+4. 鏇存柊瀵瑰簲璐т綅鐨� stockQuantity 鍜� stockStatus
+5. 鍔ㄦ�佹洿鏂拌揣浣嶉鑹�
+6. 鍦� onUnmounted 涓柇寮�杩炴帴
+
+**楠屾敹鏍囧噯锛�**
+- SignalR 杩炴帴姝e父寤虹珛
+- 鏀跺埌鏇存柊鏃惰揣浣嶉鑹茶兘鍔ㄦ�佸彉鍖�
--
Gitblit v1.9.3