| Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Stock/Stock3DLayoutDTO.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockInfoService.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/BackgroundServices/StockMonitorBackgroundService.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Hubs/StockHub.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Code/WMS/docs/superpowers/plans/2026-03-30-stock-chat-implementation-plan.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
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> </el-dialog> <div v-else class="no-detail"> <el-empty description="ææ åºåæç»" /> </div> </div> </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=æ£é宿 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: 'æ£å¸¸', 1: 'æ£å¸¸', 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: 'æ£é宿', 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() 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> 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> /// åºåæç»é¡¹DTO /// </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; } } } 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,9 +145,15 @@ }; // å°è¯ä»åºååå ¸ä¸è·ååºåä¿¡æ¯ if (stockDict.TryGetValue(loc.Id, out var stockInfo) && stockInfo.Details != null) if (stockDict.TryGetValue(loc.Id, out var stockInfo)) { // 空æç乿åºåè®°å½ï¼åªæ¯ä¸å å«æç» item.PalletCode = stockInfo.PalletCode; item.StockStatus = stockInfo.StockStatus; // ç´æ¥ä½¿ç¨å端åºåç¶æ // åªæå½Detailsä¸ä¸ºnullä¸ææ°æ®æ¶æå¤çåºåæç» if (stockInfo.Details != null && stockInfo.Details.Any()) { item.StockQuantity = stockInfo.Details.Sum(d => d.StockQuantity); // è·å第ä¸ä¸ªæç»çç©æä¿¡æ¯ï¼å¦æåå¨ï¼ @@ -159,20 +165,32 @@ item.BatchNo = firstDetail.BatchNo; } // 计ç®åºåç¶æ 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) else item.StockStatus = 0; // æ è´§ (EMPTY) // å¡«å åºåæç»å表 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; // ç©ºé² item.StockQuantity = 0; } 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(); // æå»ºåºååå ¸ï¼LocationId -> 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; } } } } 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ï¼SignalRæ¨éç¨ï¼ /// </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; } } } 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(); 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 ä¸å®ä¹çåæ®µ - å½åç©ºé´æ£ç¡® - å¯ä»¥è¢« 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` **详ç»è§èï¼** 卿¥å£ä¸æ·»å æ¹æ³ç¾åï¼ ```csharp /// <summary> /// è·åä»åº3Då¸å±æ°æ® /// </summary> /// <param name="warehouseId">ä»åºID</param> /// <returns>3Då¸å±DTO</returns> Task<Stock3DLayoutDTO> Get3DLayoutAsync(int warehouseId); ``` **éªæ¶æ åï¼** - æ¹æ³ç¾åæ£ç¡® - æ·»å äºææ¡£æ³¨é - å¼ç¨äº 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); } ``` **éªæ¶æ åï¼** - è·¯ç±æ£ç¡®ï¼GET /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') } ``` **éªæ¶æ åï¼** - è·¯ç±æ³¨åæ£ç¡® - ä¸å ¶ä»è·¯ç±æ ¼å¼ä¸è´ --- ### Task 8: å端 - å建 stockChat.vue 主ç»ä»¶ **Files:** - Create: `WIDESEA_WMSClient/src/views/stock/stockChat.vue` **详ç»è§èï¼** ç»ä»¶å¿ é¡»å å«ï¼ 1. ä»åº Tabsï¼el-tabsï¼ 2. å·¥å ·æ ï¼çé + éç½®è§è§æé®ï¼ 3. 3D Canvas å®¹å¨ 4. ç¶æå¾ä¾ 5. 详æ å¼¹çªï¼el-dialog fullscreenï¼ Three.js åºæ¯ï¼ 1. åºæ¯åå§åï¼èæ¯è² 0x1a1a2eï¼ 2. éè§ç¸æº 3. WebGLRenderer 4. OrbitControlsï¼é»å°¼å¯ç¨çè½¨éæ§å¶å¨ï¼ 5. ç¯å¢å + å®åå 6. å°é¢ï¼PlaneGeometryï¼ç½æ ¼ï¼ 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 **éªæ¶æ åï¼** - 页é¢å¯ä»¥æ£å¸¸å è½½ - Three.js åºæ¯æ£ç¡®åå§å - ç¹å»è´§ä½è½æ¾ç¤ºè¯¦æ å¼¹çª - é¢è²ç¼ç æ£ç¡® --- ### 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 è¿æ¥æ£å¸¸å»ºç« - æ¶å°æ´æ°æ¶è´§ä½é¢è²è½å¨æåå