<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="库存状态筛选" style="width: 160px" clearable>
|
<el-option label="组盘暂存(1)" :value="1" />
|
<el-option label="入库确认(3)" :value="3" />
|
<el-option label="入库完成(6)" :value="6" />
|
<el-option label="出库锁定(7)" :value="7" />
|
<el-option label="出库完成(8)" :value="8" />
|
<el-option label="空托盘(22)" :value="22" />
|
</el-select>
|
<el-select v-model="filterMaterielCode" placeholder="物料筛选" style="width: 140px" clearable>
|
<el-option v-for="code in materielCodeList" :key="code" :label="code" :value="code" />
|
</el-select>
|
<el-select v-model="filterBatchNo" placeholder="批次筛选" style="width: 140px" clearable>
|
<el-option v-for="batch in batchNoList" :key="batch" :label="batch" :value="batch" />
|
</el-select>
|
<el-button @click="resetCamera">重置视角</el-button>
|
<el-button type="primary" @click="refreshData" :loading="refreshing">刷新数据</el-button>
|
</div>
|
|
<!-- 3D Canvas -->
|
<div ref="canvasContainer" class="canvas-container" />
|
|
<!-- 状态图例 -->
|
<div class="legend">
|
<div class="legend-title">货位状态</div>
|
<div v-for="item in legendItems" :key="item.status" class="legend-item">
|
<span class="color-box" :style="{ background: item.color }" />
|
<span>{{ item.label }}</span>
|
</div>
|
</div>
|
|
<!-- 详情侧边面板 -->
|
<el-drawer v-model="detailDialogVisible" title="库存详情" direction="rtl" size="500px">
|
<div v-if="selectedLocation" class="detail-content">
|
<el-descriptions :column="2" border>
|
<el-descriptions-item label="货位编号">{{ selectedLocation.locationCode }}</el-descriptions-item>
|
<el-descriptions-item label="货位状态">{{ getLocationStatusText(selectedLocation.locationStatus) }}</el-descriptions-item>
|
<el-descriptions-item label="托盘编号">{{ selectedLocation.palletCode || '无' }}</el-descriptions-item>
|
<el-descriptions-item label="库存状态">{{ getStockStatusText(selectedLocation.stockStatus) }}</el-descriptions-item>
|
<el-descriptions-item label="总库存">{{ selectedLocation.stockQuantity }}{{ selectedLocation.unit || '' }}</el-descriptions-item>
|
</el-descriptions>
|
|
<!-- 库存明细表格 -->
|
<div v-if="selectedLocation.details && selectedLocation.details.length > 0" class="detail-table">
|
<h4>库存明细</h4>
|
<el-table :data="selectedLocation.details" border size="small" max-height="400">
|
<el-table-column prop="materielCode" label="物料编码" width="100" />
|
<el-table-column prop="materielName" label="物料名称" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="batchNo" label="批次号" width="100" show-overflow-tooltip />
|
<el-table-column prop="stockQuantity" label="数量" width="70" align="right" />
|
<el-table-column prop="unit" label="单位" width="50" align="center" />
|
<el-table-column prop="effectiveDate" label="有效期" width="100" />
|
</el-table>
|
</div>
|
<div v-else class="no-detail">
|
<el-empty description="暂无库存明细" />
|
</div>
|
</div>
|
</el-drawer>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, onUnmounted, watch, nextTick, getCurrentInstance } from 'vue'
|
import * as THREE from 'three'
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
import * as signalR from '@microsoft/signalr'
|
|
const { proxy } = getCurrentInstance()
|
|
// SignalR 连接
|
let connection = null
|
|
// 颜色常量 - 基于实际枚举值
|
// 货位状态(locationStatus): 0=空闲, 1=锁定, 10=有货锁定, 20=空闲锁定, 99=大托盘锁定, 100=有货
|
// 库存状态(stockStatus): 1=组盘暂存, 3=入库确认, 6=入库完成, 7=出库锁定, 8=出库完成, 9=移库锁定等
|
const COLOR_MAP = {
|
// 货位状态颜色
|
LOC_FREE: 0x90EE90, // 0=空闲 - 浅绿色
|
LOC_LOCK: 0xFF6B6B, // 1=锁定 - 红色
|
LOC_INSTOCK_LOCK: 0xFFA500, // 10=有货锁定 - 橙色
|
LOC_FREE_LOCK: 0xFFD700, // 20=空闲锁定 - 金色
|
LOC_PALLET_LOCK: 0x9370DB, // 99=大托盘锁定 - 紫色
|
LOC_INSTOCK: 0x409EFF, // 100=有货 - 蓝色
|
|
// 库存状态颜色
|
STOCK_PENDING: 0x00CED1, // 1=组盘暂存 - 深青色
|
STOCK_CONFIRMED: 0x87CEEB, // 3=入库确认 - 天蓝色
|
STOCK_COMPLETED: 0x32CD32, // 6=入库完成 - 亮绿色
|
STOCK_OUT_LOCK: 0xFF6347, // 7=出库锁定 - 番茄红
|
STOCK_OUT_COMPLETED: 0x228B22, // 8=出库完成 - 森林绿
|
STOCK_TRANSFER_LOCK: 0xFF8C00, // 9=移库锁定 - 深橙色
|
STOCK_COMPLETED_NO_ORDER: 0x20B2AA, // 10=入库完成未建出库单
|
STOCK_RETURN: 0xFF4500, // 11=退库
|
STOCK_MANUAL_PENDING: 0x48D1CC, // 12=手动组盘暂存
|
STOCK_MANUAL_CONFIRMED: 0x7FFFD4, // 13=手动组盘入库确认
|
STOCK_PICK_COMPLETED: 0x6B8E23, // 14=拣选完成
|
STOCK_MES_RETURN: 0xDC143C, // 21=MES退库
|
STOCK_EMPTY_TRAY: 0xDDA0DD, // 22=空托盘库存 - 梅红色
|
STOCK_GROUP_CANCEL: 0xDEB887, // 99=组盘撤销 - 暗金色
|
STOCK_IN_CANCEL: 0xA0522D, // 199=入库撤销 - 赭色
|
}
|
|
// 图例项 - 货位状态
|
const legendItems = [
|
{ status: 'loc_free', label: '空闲(0)', color: '#90EE90' },
|
{ status: 'loc_lock', label: '锁定(1)', color: '#FF6B6B' },
|
{ status: 'loc_instock_lock', label: '有货锁定(10)', color: '#FFA500' },
|
{ status: 'loc_free_lock', label: '空闲锁定(20)', color: '#FFD700' },
|
{ status: 'loc_pallet_lock', label: '大托盘锁定(99)', color: '#9370DB' },
|
{ status: 'loc_instock', label: '有货(100)', color: '#409EFF' },
|
]
|
|
// 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)
|
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() {
|
proxy.http.post('api/User/GetCurrentUserInfo').then((result) => {
|
connection = new signalR.HubConnectionBuilder()
|
.withAutomaticReconnect()
|
.withUrl(`${proxy.http.ipAddress}stockHub?userName=${result.data.userName}`)
|
.build();
|
|
connection.start().catch((err) => console.log('SignalR连接失败:', err));
|
|
connection.on('StockUpdated', (update) => {
|
console.log('收到库存更新:', update)
|
// 更新对应货位的数据
|
const idx = locationData.findIndex(x => x.locationId === update.locationId);
|
if (idx !== -1) {
|
locationData[idx].stockQuantity = update.stockQuantity;
|
locationData[idx].stockStatus = update.stockStatus;
|
locationData[idx].palletCode = update.palletCode;
|
locationData[idx].locationStatus = update.locationStatus;
|
// 更新库存明细
|
if (update.details && update.details.length > 0) {
|
locationData[idx].details = update.details;
|
}
|
// 通过映射找到实例ID,更新颜色
|
const instanceId = locationIdToInstanceId.get(update.locationId);
|
if (instanceId !== undefined) {
|
updateInstanceColor(instanceId, update.locationStatus);
|
}
|
}
|
});
|
});
|
}
|
|
// 更新单个货位颜色
|
function updateInstanceColor(instanceId, locationStatus) {
|
if (!locationMesh) return;
|
// 根据货位状态获取颜色
|
const color = getLocationColorByStatus(locationStatus);
|
locationMesh.setColorAt(instanceId, new THREE.Color(color));
|
locationMesh.instanceColor.needsUpdate = true;
|
}
|
|
// 根据货位状态获取颜色
|
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 // 默认空闲色
|
}
|
}
|
|
// 获取货位颜色 - 只根据货位状态
|
function getLocationColor(location) {
|
const locStatus = location.locationStatus
|
|
// 根据货位状态判断颜色
|
switch (locStatus) {
|
case 0: return COLOR_MAP.LOC_FREE // 空闲
|
case 1: return COLOR_MAP.LOC_LOCK // 锁定
|
case 10: return COLOR_MAP.LOC_INSTOCK_LOCK // 有货锁定
|
case 20: return COLOR_MAP.LOC_FREE_LOCK // 空闲锁定
|
case 99: return COLOR_MAP.LOC_PALLET_LOCK // 大托盘锁定
|
case 100: return COLOR_MAP.LOC_INSTOCK // 有货
|
default: return COLOR_MAP.LOC_FREE // 默认空闲色
|
}
|
}
|
|
// 获取货位状态文本
|
function getLocationStatusText(status) {
|
const map = {
|
0: '空闲',
|
1: '锁定',
|
10: '有货锁定',
|
20: '空闲锁定',
|
99: '大托盘锁定',
|
100: '有货'
|
}
|
return map[status] || '未知(' + status + ')'
|
}
|
|
// 获取库存状态文本
|
function getStockStatusText(status) {
|
const map = {
|
0: '无库存',
|
1: '组盘暂存',
|
3: '入库确认',
|
6: '入库完成',
|
7: '出库锁定',
|
8: '出库完成',
|
9: '移库锁定',
|
10: '入库完成未建出库单',
|
11: '退库',
|
12: '手动组盘暂存',
|
13: '手动组盘入库确认',
|
14: '拣选完成',
|
21: 'MES退库',
|
22: '空托盘库存',
|
99: '组盘撤销',
|
199: '入库撤销'
|
}
|
return map[status] || '未知(' + status + ')'
|
}
|
|
// 加载仓库列表
|
async function loadWarehouseList() {
|
try {
|
const res = await proxy.http.get('/api/Warehouse/GetAll')
|
if (res.status && res.data) {
|
warehouseList.value = res.data
|
if (res.data.length > 0) {
|
activeWarehouse.value = res.data[0].warehouseId || res.data[0].id
|
await loadWarehouseData(activeWarehouse.value)
|
}
|
}
|
} 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) {
|
const data = res.data
|
originalLocationData = data.locations || [] // 保存原始完整数据
|
locationData = [...originalLocationData] // 当前显示数据
|
// 使用后端返回的筛选列表
|
materielCodeList.value = data.materielCodeList || []
|
batchNoList.value = data.batchNoList || []
|
// 渲染货位
|
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)
|
camera.lookAt(0, 0, 0)
|
|
// 创建渲染器
|
renderer = new THREE.WebGLRenderer({ antialias: true })
|
renderer.setPixelRatio(window.devicePixelRatio)
|
renderer.setSize(width, height)
|
canvasContainer.value.appendChild(renderer.domElement)
|
|
// 创建轨道控制器
|
controls = new OrbitControls(camera, renderer.domElement)
|
controls.enableDamping = true
|
controls.dampingFactor = 0.05
|
|
// 创建环境光
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
|
scene.add(ambientLight)
|
|
// 创建主定向光
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0)
|
directionalLight.position.set(50, 100, 50)
|
scene.add(directionalLight)
|
|
// 创建补光
|
const fillLight = new THREE.DirectionalLight(0xffffff, 0.3)
|
fillLight.position.set(-50, 50, -50)
|
scene.add(fillLight)
|
|
// 创建地面
|
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x2d2d2d })
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
ground.rotation.x = -Math.PI / 2
|
ground.position.y = -0.5
|
scene.add(ground)
|
|
// 创建网格
|
const gridHelper = new THREE.GridHelper(100, 50)
|
scene.add(gridHelper)
|
|
// 创建射线检测器
|
raycaster = new THREE.Raycaster()
|
mouse = new THREE.Vector2()
|
|
// 添加点击事件
|
canvasContainer.value.addEventListener('mousedown', onCanvasClick)
|
|
// 开始渲染循环
|
animate()
|
}
|
|
// 渲染货位
|
function renderLocations() {
|
if (!scene) return
|
console.log('渲染货位,原始数据总数:', originalLocationData.length)
|
// 如果原始数据为空,尝试重新加载
|
if (originalLocationData.length === 0) {
|
console.warn('原始数据为空,重新加载...')
|
if (activeWarehouse.value) {
|
loadWarehouseData(activeWarehouse.value)
|
}
|
return
|
}
|
|
// 移除旧的货位网格
|
console.log("🚀 ~ renderLocations ~ locationMesh:", locationMesh)
|
if (locationMesh) {
|
scene.remove(locationMesh)
|
locationMesh.geometry.dispose()
|
if (Array.isArray(locationMesh.material)) {
|
locationMesh.material.forEach(m => m.dispose())
|
} else {
|
locationMesh.material.dispose()
|
}
|
locationMesh = null
|
}
|
|
// 过滤数据 - 始终从原始完整数据过滤
|
let filteredData = [...originalLocationData]
|
if (filterStockStatus.value !== null) {
|
filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
|
}
|
if (filterMaterielCode.value) {
|
filteredData = filteredData.filter(loc => loc.materielCode === filterMaterielCode.value)
|
}
|
if (filterBatchNo.value) {
|
filteredData = filteredData.filter(loc => loc.batchNo === filterBatchNo.value)
|
}
|
|
console.log('过滤后数据数量:', filteredData.length, '筛选条件:', filterStockStatus.value, filterMaterielCode.value, filterBatchNo.value)
|
|
// 创建 InstancedMesh
|
const geometry = new THREE.BoxGeometry(1.5, 1, 1.5)
|
const material = new THREE.MeshStandardMaterial({
|
color: 0xffffff,
|
roughness: 0.5,
|
metalness: 0.1
|
})
|
|
// 如果过滤后无数据,创建空的 InstancedMesh
|
if (filteredData.length === 0) {
|
locationMesh = new THREE.InstancedMesh(geometry, material, 0)
|
scene.add(locationMesh)
|
return
|
}
|
|
locationMesh = new THREE.InstancedMesh(geometry, material, filteredData.length)
|
|
// 清空并重建映射
|
locationIdToInstanceId.clear()
|
|
const dummy = new THREE.Object3D()
|
const color = new THREE.Color()
|
|
filteredData.forEach((location, i) => {
|
const x = (location.column - 1) * 2
|
const y = location.layer * 1.5
|
const z = (location.row - 1) * 2
|
|
dummy.position.set(x, y, z)
|
dummy.updateMatrix()
|
locationMesh.setMatrixAt(i, dummy.matrix)
|
|
// 设置颜色
|
color.setHex(getLocationColor(location))
|
locationMesh.setColorAt(i, color)
|
|
// 建立映射: locationId -> instanceId (i)
|
locationIdToInstanceId.set(location.locationId, i)
|
|
if (i === 0) {
|
console.log('First location:', location, { x, y, z })
|
}
|
})
|
|
locationMesh.instanceMatrix.needsUpdate = true
|
if (locationMesh.instanceColor) locationMesh.instanceColor.needsUpdate = true
|
|
scene.add(locationMesh)
|
}
|
|
// 动画循环
|
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 = [...originalLocationData]
|
if (filterStockStatus.value !== null) {
|
filteredData = filteredData.filter(loc => loc.stockStatus === filterStockStatus.value)
|
}
|
if (filterMaterielCode.value) {
|
filteredData = filteredData.filter(loc => loc.materielCode === filterMaterielCode.value)
|
}
|
if (filterBatchNo.value) {
|
filteredData = filteredData.filter(loc => loc.batchNo === filterBatchNo.value)
|
}
|
|
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 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, async () => {
|
await nextTick()
|
if (originalLocationData.length > 0) {
|
renderLocations()
|
}
|
})
|
watch(filterMaterielCode, async () => {
|
await nextTick()
|
if (originalLocationData.length > 0) {
|
renderLocations()
|
}
|
})
|
watch(filterBatchNo, async () => {
|
await nextTick()
|
if (originalLocationData.length > 0) {
|
renderLocations()
|
}
|
})
|
|
// 窗口大小变化
|
function onWindowResize() {
|
if (!canvasContainer.value || !camera || !renderer) return
|
const width = canvasContainer.value.clientWidth
|
const height = canvasContainer.value.clientHeight
|
camera.aspect = width / height
|
camera.updateProjectionMatrix()
|
renderer.setSize(width, height)
|
}
|
|
// 组件挂载
|
onMounted(() => {
|
initThreeJS()
|
loadWarehouseList()
|
initSignalR()
|
window.addEventListener('resize', onWindowResize)
|
})
|
|
// 组件卸载
|
onUnmounted(() => {
|
if (animationId) {
|
cancelAnimationFrame(animationId)
|
}
|
if (canvasContainer.value) {
|
canvasContainer.value.removeEventListener('mousedown', onCanvasClick)
|
}
|
window.removeEventListener('resize', onWindowResize)
|
if (renderer) {
|
renderer.dispose()
|
}
|
if (connection) {
|
connection.stop()
|
}
|
})
|
</script>
|
|
<style scoped>
|
.stock-chat-container {
|
width: 100%;
|
height: calc(100vh - 120px);
|
position: relative;
|
overflow: visible;
|
}
|
.toolbar {
|
position: absolute;
|
top: 60px;
|
left: 20px;
|
z-index: 10;
|
display: flex;
|
gap: 10px;
|
background: rgba(255, 255, 255, 0.9);
|
padding: 10px;
|
border-radius: 4px;
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
}
|
.canvas-container {
|
width: 100%;
|
height: 100%;
|
overflow: hidden;
|
}
|
.legend {
|
position: absolute;
|
bottom: 20px;
|
right: 20px;
|
background: rgba(0, 0, 0, 0.7);
|
padding: 10px;
|
border-radius: 4px;
|
color: white;
|
z-index: 10;
|
max-height: 400px;
|
overflow-y: auto;
|
}
|
.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>
|