From 14980b36f342dfcac9629891ca4019dc52480433 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期一, 30 三月 2026 13:49:38 +0800
Subject: [PATCH] feat(stockChat): create 3D warehouse visualization component

---
 Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue |  452 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 452 insertions(+), 0 deletions(-)

diff --git a/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
new file mode 100644
index 0000000..0f7402e
--- /dev/null
+++ b/Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
@@ -0,0 +1,452 @@
+<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="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>
+
+    <!-- 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 * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
+
+const { proxy } = getCurrentInstance()
+
+// 棰滆壊甯搁噺
+const COLOR_MAP = {
+  DISABLED: 0x2d2d2d,    // 绂佺敤 - 娣辩伆
+  LOCKED: 0xF56C6C,      // 閿佸畾 - 绾㈣壊
+  EMPTY: 0x4a4a4a,       // 绌鸿揣浣� - 鏆楃伆
+  HAS_STOCK: 0x409EFF,   // 鏈夎揣 - 钃濊壊
+  LOW_STOCK: 0xE6A23C,   // 搴撳瓨绱у紶 - 姗欒壊
+  FULL: 0x67C23A,        // 宸叉弧 - 缁胯壊
+}
+
+// 鍥句緥椤�
+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' },
+]
+
+// Refs
+const canvasContainer = ref(null)
+
+// 鐘舵��
+const activeWarehouse = ref(null)
+const warehouseList = ref([])
+const filterStockStatus = ref(null)
+const filterMaterielCode = ref(null)
+const filterBatchNo = ref(null)
+const materielCodeList = ref([])
+const batchNoList = ref([])
+const detailDialogVisible = ref(false)
+const selectedLocation = ref(null)
+
+// Three.js 鐩稿叧
+let scene, camera, renderer, controls, raycaster, mouse
+let locationMesh = null
+let locationData = []
+let animationId = null
+
+// 鑾峰彇璐т綅棰滆壊
+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)
+      }
+    }
+  } catch (e) {
+    console.error('鍔犺浇浠撳簱鍒楄〃澶辫触', e)
+  }
+}
+
+// 鍔犺浇浠撳簱璐т綅鏁版嵁
+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()
+    }
+  } catch (e) {
+    console.error('鍔犺浇璐т綅鏁版嵁澶辫触', e)
+  }
+}
+
+// 鍒濆鍖� Three.js 鍦烘櫙
+function initThreeJS() {
+  if (!canvasContainer.value) return
+
+  const width = canvasContainer.value.clientWidth
+  const height = canvasContainer.value.clientHeight
+
+  // 鍒涘缓鍦烘櫙
+  scene = new THREE.Scene()
+  scene.background = new THREE.Color(0x1a1a2e)
+
+  // 鍒涘缓鐩告満
+  camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
+  camera.position.set(20, 20, 20)
+
+  // 鍒涘缓娓叉煋鍣�
+  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 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 animate() {
+  animationId = requestAnimationFrame(animate)
+  controls.update()
+  renderer.render(scene, camera)
+}
+
+// 鐐瑰嚮鐢诲竷
+function onCanvasClick(event) {
+  if (!canvasContainer.value || !locationMesh) return
+
+  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.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)
+    }
+    if (filterMaterielCode.value) {
+      filteredData = filteredData.filter(loc => loc.materielCode === filterMaterielCode.value)
+    }
+    if (filterBatchNo.value) {
+      filteredData = filteredData.filter(loc => loc.batchNo === filterBatchNo.value)
+    }
+
+    if (instanceId < filteredData.length) {
+      selectedLocation.value = filteredData[instanceId]
+      detailDialogVisible.value = true
+      // 鑱氱劍鐩告満
+      focusCamera(selectedLocation.value)
+    }
+  }
+}
+
+// 鑱氱劍鐩告満鍒拌揣浣�
+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)
+    }
+  }
+
+  lerpMove()
+}
+
+// 閲嶇疆鐩告満
+function resetCamera() {
+  if (!camera || !controls) return
+  camera.position.set(20, 20, 20)
+  controls.target.set(0, 0, 0)
+}
+
+// 浠撳簱鍒囨崲
+async function onWarehouseChange(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)
+}
+
+// 缁勪欢鎸傝浇
+onMounted(() => {
+  initThreeJS()
+  loadWarehouseList()
+  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()
+  }
+})
+</script>
+
+<style scoped>
+.stock-chat-container {
+  width: 100%;
+  height: calc(100vh - 120px);
+  position: relative;
+}
+.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;
+}
+.canvas-container {
+  width: 100%;
+  height: 100%;
+}
+.legend {
+  position: absolute;
+  bottom: 20px;
+  right: 20px;
+  background: rgba(0, 0, 0, 0.7);
+  padding: 10px;
+  border-radius: 4px;
+  color: white;
+  z-index: 10;
+}
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 4px;
+}
+.color-box {
+  width: 16px;
+  height: 16px;
+  border-radius: 2px;
+}
+.detail-content {
+  padding: 20px;
+}
+</style>

--
Gitblit v1.9.3