From b698a2085fd090e90abedb1e91266ec496574b29 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期四, 16 四月 2026 23:31:35 +0800
Subject: [PATCH] 1
---
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue | 1672 +++++++++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 1,275 insertions(+), 397 deletions(-)
diff --git a/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
index 9cc6436..6337909 100644
--- a/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
+++ b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
@@ -1,95 +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="搴撳瓨鐘舵�佺瓫閫�" clearable>
- <el-option label="鏈夎揣" :value="1" />
- <el-option label="搴撳瓨绱у紶" :value="2" />
- <el-option label="宸叉弧" :value="3" />
- </el-select>
- <el-select v-model="filterMaterielCode" placeholder="鐗╂枡绛涢��" clearable>
- <el-option v-for="code in materielCodeList" :key="code" :label="code" :value="code" />
- </el-select>
- <el-select v-model="filterBatchNo" placeholder="鎵规绛涢��" clearable>
- <el-option v-for="batch in batchNoList" :key="batch" :label="batch" :value="batch" />
- </el-select>
- <el-button @click="resetCamera">閲嶇疆瑙嗚</el-button>
+ <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 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>
+
+ <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>
-
- <!-- 3D Canvas -->
- <div ref="canvasContainer" class="canvas-container" />
-
- <!-- 鐘舵�佸浘渚� -->
- <div class="legend">
- <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>
- <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>
- </div>
- </el-dialog>
- </div>
</template>
<script setup>
-import { ref, reactive, onMounted, onUnmounted, watch, 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
-// 棰滆壊甯搁噺
const COLOR_MAP = {
- DISABLED: 0x2d2d2d, // 绂佺敤 - 娣辩伆
- LOCKED: 0xF56C6C, // 閿佸畾 - 绾㈣壊
- EMPTY: 0x4a4a4a, // 绌鸿揣浣� - 鏆楃伆
- HAS_STOCK: 0x409EFF, // 鏈夎揣 - 钃濊壊
- LOW_STOCK: 0xE6A23C, // 搴撳瓨绱у紶 - 姗欒壊
- FULL: 0x67C23A, // 宸叉弧 - 缁胯壊
+ 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: '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' },
+ { status: 'stock_empty_tray', label: '绌烘墭鐩�(22)', color: '#DDA0DD' }
]
-// Refs
const canvasContainer = ref(null)
-
-// 鐘舵��
const activeWarehouse = ref(null)
const warehouseList = ref([])
const filterStockStatus = ref(null)
@@ -99,395 +120,1252 @@
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 scene = null
+let camera = null
+let renderer = null
+let controls = null
+let raycaster = null
+let mouse = null
let animationId = null
-
-// 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) => {
- // 鏇存柊瀵瑰簲璐т綅鐨勬暟鎹�
- 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);
- }
- });
- });
+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, stockStatus) {
- if (!locationMesh) return;
- const loc = locationData[instanceId];
- if (!loc) return;
- const color = getLocationColor(loc);
- 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 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) {
- 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 // 宸叉弧
- }
- return COLOR_MAP.EMPTY // 榛樿绌�
-}
-
-// 鑾峰彇璐т綅鐘舵�佹枃鏈�
-function getLocationStatusText(status) {
- const map = { 0: '姝e父', 1: '姝e父', 2: '閿佸畾', 3: '绂佺敤' }
- return map[status] || '鏈煡'
-}
-
-// 鑾峰彇搴撳瓨鐘舵�佹枃鏈�
-function getStockStatusText(status) {
- const map = { 0: '鏃犺揣', 1: '鏈夎揣', 2: '搴撳瓨绱у紶', 3: '宸叉弧' }
- return map[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) {
- locationData = res.Data
- // 鎻愬彇鐗╂枡缂栧彿鍜屾壒娆″垪琛�
- const codes = new Set()
- const batches = new Set()
- res.Data.forEach(loc => {
- if (loc.materielCode) codes.add(loc.materielCode)
- if (loc.batchNo) batches.add(loc.batchNo)
- })
- materielCodeList.value = Array.from(codes)
- batchNoList.value = Array.from(batches)
- // 娓叉煋璐т綅
- 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)
- }
+ if (hasCargo(location)) {
+ return true
+ }
+ return Boolean(location.palletCode)
}
-// 鍒濆鍖� 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)
-
- // 鍒涘缓娓叉煋鍣�
- 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.6)
- scene.add(ambientLight)
-
- // 鍒涘缓瀹氬悜鍏�
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
- directionalLight.position.set(10, 20, 10)
- scene.add(directionalLight)
-
- // 鍒涘缓鍦伴潰
- 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('click', onCanvasClick)
-
- // 寮�濮嬫覆鏌撳惊鐜�
- animate()
+function getShelfAccentColor(location) {
+ return BASE_COLOR
}
-// 娓叉煋璐т綅
-function renderLocations() {
- if (!scene) return
-
- // 绉婚櫎鏃х殑璐т綅缃戞牸
- if (locationMesh) {
- scene.remove(locationMesh)
- locationMesh.geometry.dispose()
- locationMesh.material.forEach(m => m.dispose())
- locationMesh = null
- }
-
- // 杩囨护鏁版嵁
- let filteredData = locationData
- 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)
- }
-
- if (filteredData.length === 0) return
-
- // 鍒涘缓 InstancedMesh
- const geometry = new THREE.BoxGeometry(1.5, 1, 1.5)
- const material = new THREE.MeshStandardMaterial({ color: 0xffffff })
-
- locationMesh = new THREE.InstancedMesh(geometry, [material], filteredData.length)
-
- 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)
- })
-
- locationMesh.instanceMatrix.needsUpdate = true
- if (locationMesh.instanceColor) locationMesh.instanceColor.needsUpdate = true
-
- scene.add(locationMesh)
+function getPalletColor(location) {
+ return PALLET_COLOR
}
-// 鍔ㄧ敾寰幆
-function animate() {
- animationId = requestAnimationFrame(animate)
- controls.update()
- renderer.render(scene, camera)
+function getCargoColor(location) {
+ const cargoColor = new THREE.Color(getLocationColor(location))
+ return cargoColor.offsetHSL(0, 0.04, 0.08).getHex()
}
-// 鐐瑰嚮鐢诲竷
-function onCanvasClick(event) {
- if (!canvasContainer.value || !locationMesh) return
+function getCargoLidColor(location) {
+ const lidColor = new THREE.Color(getCargoColor(location))
+ return lidColor.offsetHSL(0, -0.02, 0.06).getHex()
+}
- 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 getFilteredLocations() {
+ let filteredData = [...originalLocationData]
- raycaster.setFromCamera(mouse, camera)
- const intersects = raycaster.intersectObject(locationMesh)
-
- if (intersects.length > 0) {
- const instanceId = intersects[0].instanceId
- // 鑾峰彇鍘熷鏁版嵁绱㈠紩锛堣�冭檻杩囨护鍚庣殑鏁版嵁锛�
- let filteredData = locationData
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)
+ if (!camera || !controls) {
+ return
+ }
+ 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, () => renderLocations())
-watch(filterMaterielCode, () => renderLocations())
-watch(filterBatchNo, () => 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('click', 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;
+ 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;
+ 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%;
+ 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;
+ 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: 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.16);
+ margin: 10px 0;
+}
+
.legend-item {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 4px;
+ 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;
+ 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;
+}
+
+.detail-table h4 {
+ margin-bottom: 10px;
+ color: #303133;
+}
+
+.no-detail {
+ margin-top: 20px;
+}
+
+:deep(.el-tabs__nav-wrap::after) {
+ background-color: rgba(148, 163, 184, 0.22);
+}
+
+:deep(.el-drawer__body) {
+ padding: 20px;
}
</style>
--
Gitblit v1.9.3