|
|
<template> <div class="warehouse-3d-screen"> <!-- 装饰背景 --> <div class="bg-decoration"> <div class="decoration-line line-1"></div> <div class="decoration-line line-2"></div> <div class="decoration-line line-3"></div> <div class="decoration-circle circle-1"></div> <div class="decoration-circle circle-2"></div> <div class="grid-bg"></div> </div>
<!-- 顶部标题栏 --> <div class="screen-header"> <!-- CCL Logo --> <div class="header-logo"> <img src="~@/assets/img/ccl.png" alt="CCL Logo" class="logo-img"> </div>
<div class="header-decoration left"></div> <div class="header-center"> <div class="title-glow"></div> <h1 class="screen-title">智能立体仓库可视化监控中心</h1> <div class="title-subtitle">Intelligent 3D Warehouse Visualization & Monitoring Center</div> </div> <div class="header-decoration right"></div> <div class="header-time"> <div class="time-icon"></div> <div class="time-text">{{ currentTime }}</div> </div> </div>
<!-- 主内容区 --> <div class="screen-content"> <!-- 第一行:任务统计 + 库位利用率 + 设备状态 --> <div class="content-row row-1"> <!-- 左侧:任务统计 --> <div class="panel-card task-summary"> <div class="card-header"> <div class="header-icon"></div> <span class="header-title">任务统计总览</span> <span class="header-subtitle">Task Overview</span> </div> <div class="card-body"> <div class="task-stats"> <div class="stat-item total"> <div class="stat-icon"> <i class="el-icon-tickets"></i> </div> <div class="stat-content"> <div class="stat-label">累计任务总数</div> <div class="stat-value">{{ taskData.totalTasks }}</div>
</div> </div> <div class="stat-divider"></div> <div class="stat-item monthly"> <div class="stat-icon"> <i class="el-icon-date"></i> </div> <div class="stat-content"> <div class="stat-label">月度作业总数</div> <div class="stat-value">{{ taskData.monthlyTasks }}</div>
</div> </div> </div> <div class="task-breakdown"> <div class="breakdown-item outbound"> <div class="item-icon">📦</div> <div class="item-label">出库作业</div> <div class="item-value">{{ taskData.outboundTasks }}</div> <div class="item-percent">{{ taskData.outboundPercent }}%</div> </div> <div class="breakdown-item inbound"> <div class="item-icon">📥</div> <div class="item-label">入库作业</div> <div class="item-value">{{ taskData.inboundTasks }}</div> <div class="item-percent">{{ taskData.inboundPercent }}%</div> </div> </div> </div> </div>
<!-- 中间:库位利用率 --> <div class="panel-card storage-utilization"> <div class="card-header"> <div class="header-icon"></div> <span class="header-title">库位利用率分析</span> <span class="header-subtitle">Storage Utilization</span> </div> <div class="card-body"> <div class="utilization-summary"> <div class="summary-item"> <span class="summary-label">总库位数</span> <span class="summary-value">{{ storageData.totalSlots }}</span> </div> <div class="summary-item"> <span class="summary-label">已使用</span> <span class="summary-value used">{{ storageData.usedSlots }}</span> </div> <div class="summary-item"> <span class="summary-label">利用率</span> <span class="summary-value rate">{{ storageData.utilizationRate }}%</span> </div> </div> <div id="storageChart" style="margin-left: 70px" class="chart-container"></div> </div> </div>
<!-- 右侧:设备工作状态 --> <div class="panel-card device-status"> <div class="card-header"> <div class="header-icon"></div> <span class="header-title">设备运行状态</span> <span class="header-subtitle">Device Status</span> </div> <div class="card-body"> <!-- 拣选机器人 --> <div class="device-group"> <div class="group-title">🤖 拣选机器人</div> <div class="device-list"> <div v-for="robot in robotData" :key="robot.id" :class="['device-item', robot.status]" > <div class="device-header"> <div class="device-name">{{ robot.name }}</div> <div class="device-status-badge"> <span class="status-dot"></span> <span class="status-text">{{ robot.statusText }}</span> </div> </div> <div class="device-metrics"> <span class="metric">效率: {{ robot.efficiency }}%</span> <span class="metric">任务: {{ robot.tasks }}</span> </div> </div> </div> </div>
<!-- AGV --> <div class="device-group"> <div class="group-title">🚗 AGV搬运车</div> <div class="device-list agv-grid"> <!-- AGV 列表 --> <div v-for="agv in agvData" :key="agv.id" :class="['device-item-compact', agv.status]" > <div class="compact-header"> <span class="compact-name">{{ agv.name }}</span> <span class="compact-status"> <span class="status-dot"></span> {{ agv.statusText }} </span> </div> <div class="compact-info"> <span>电量: {{ agv.battery }}%</span> <span>任务: {{ agv.tasks }}</span> </div> </div>
<!-- 空状态提示 --> <div v-if="agvData.length === 0" class="empty-state"> <i class="el-icon-warning"></i> <span>暂无AGV数据</span> </div> </div> </div> </div> </div> </div>
<!-- 第二行:库存趋势 + 呆滞情况 --> <div class="content-row row-2"> <!-- 成品库存量趋势 --> <div class="panel-card inventory-trend"> <div class="card-header"> <div class="header-icon"></div> <span class="header-title">成品库存量趋势</span> <span class="header-subtitle">Finished Goods Inventory Trend</span> </div> <div class="card-body"> <div id="finishedGoodsTrendChart" class="chart-container"></div> </div> </div>
<!-- 原材库存量趋势 --> <div class="panel-card inventory-trend"> <div class="card-header"> <div class="header-icon"></div> <span class="header-title">原材库存量趋势</span> <span class="header-subtitle">Raw Material Inventory Trend</span> </div> <div class="card-body"> <div id="rawMaterialTrendChart" class="chart-container"></div> </div> </div>
<!-- 呆滞情况分析 --> <div class="panel-card stagnant-analysis"> <div class="card-header"> <div class="header-icon"></div> <span class="header-title">库存呆滞分析</span> <span class="header-subtitle">Stagnant Inventory Analysis</span> </div> <div class="card-body"> <div id="stagnantChart" class="chart-container"></div> </div> </div> </div>
<!-- 第三行:当日作业统计 --> <div class="content-row row-3"> <!-- 领料申请单统计 --> <div class="panel-card daily-operation"> <div class="card-header"> <div class="header-icon"></div> <span class="header-title">领料申请单统计</span> <span class="header-subtitle">Material Request Today</span> </div> <div class="card-body"> <div class="operation-visual"> <div class="visual-left"> <div id="materialRequestChart" class="mini-chart"></div> </div> <div class="visual-right"> <div class="operation-stat"> <div class="stat-row"> <span class="stat-label">总申请单数</span> <span class="stat-number total">{{ materialRequestData.total }}</span> </div> <div class="stat-row"> <span class="stat-label">已完成</span> <span class="stat-number completed">{{ materialRequestData.completed }}</span> </div> <div class="stat-row"> <span class="stat-label">进行中</span> <span class="stat-number processing">{{ materialRequestData.processing }}</span> </div> <div class="stat-row"> <span class="stat-label">待处理</span> <span class="stat-number pending">{{ materialRequestData.pending }}</span> </div> </div> <div class="completion-rate"> <div class="rate-label">完成率</div> <div class="rate-value">{{ materialRequestData.completionRate }}%</div> <div class="rate-bar"> <div class="rate-fill" :style="{width: materialRequestData.completionRate + '%'}"></div> </div> </div> </div> </div> </div> </div>
<!-- 当日发货统计 --> <div class="panel-card daily-operation"> <div class="card-header"> <div class="header-icon"></div> <span class="header-title">当日发货统计</span> <span class="header-subtitle">Shipment Today</span> </div> <div class="card-body"> <div class="operation-visual"> <div class="visual-left"> <div id="shipmentChart" class="mini-chart"></div> </div> <div class="visual-right"> <div class="operation-stat"> <div class="stat-row"> <span class="stat-label">总发货单数</span> <span class="stat-number total">{{ shipmentData.total }}</span> </div> <div class="stat-row"> <span class="stat-label">已发货</span> <span class="stat-number completed">{{ shipmentData.completed }}</span> </div> <div class="stat-row"> <span class="stat-label">拣选中</span> <span class="stat-number processing">{{ shipmentData.processing }}</span> </div> <div class="stat-row"> <span class="stat-label">待拣选</span> <span class="stat-number pending">{{ shipmentData.pending }}</span> </div> </div> <div class="completion-rate"> <div class="rate-label">完成率</div> <div class="rate-value">{{ shipmentData.completionRate }}%</div> <div class="rate-bar"> <div class="rate-fill" :style="{width: shipmentData.completionRate + '%'}"></div> </div> </div> </div> </div> </div> </div> </div> </div> </div></template>
<script>import WebSocketClient from '@/utils/websocket'
export default { name: 'Warehouse3DBoard', data() { return { currentTime: '',
// WebSocket相关
useWebSocket: true, // 是否使用WebSocket(可切换为false降级到本地数据)
wsConnected: false, // WebSocket连接状态
wsSubscription: null, // WebSocket订阅ID
// 任务统计数据
taskData: { totalTasks: 125680, monthlyTasks: 8540, outboundTasks: 5120, inboundTasks: 3420, outboundPercent: 60, inboundPercent: 40 },
// 库位数据
storageData: { totalSlots: 2400, usedSlots: 1856, utilizationRate: 77.3, steelPallet: 680, guardPallet: 520, flatPallet: 656 },
// 机器人数据
robotData: [ { id: 1, name: '机器人#1', status: 'working', statusText: '工作中', efficiency: 95, tasks: 48 }, { id: 2, name: '机器人#2', status: 'working', statusText: '工作中', efficiency: 92, tasks: 45 } ],
// AGV数据(从TUSK系统实时获取)
agvData: [],
// 领料申请单数据
materialRequestData: { total: 45, completed: 32, processing: 8, pending: 5, completionRate: 71 },
// 发货数据
shipmentData: { total: 38, completed: 28, processing: 6, pending: 4, completionRate: 74 },
// 图表实例
charts: {} } },
mounted() { this.updateTime() this.timeInterval = setInterval(this.updateTime, 1000)
// 延迟初始化图表,确保DOM已渲染
this.$nextTick(() => { setTimeout(() => { this.initCharts() // 图表初始化后再次调用resize确保尺寸正确
setTimeout(() => { this.handleResize() }, 300) }, 100) })
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize)
// 根据配置选择使用WebSocket或本地数据
if (this.useWebSocket) { this.initWebSocket() } },
beforeDestroy() { if (this.timeInterval) { clearInterval(this.timeInterval) } // 移除窗口resize监听
window.removeEventListener('resize', this.handleResize) // 销毁所有图表
Object.values(this.charts).forEach(chart => { if (chart) chart.dispose() }) // 断开WebSocket连接
this.disconnectWebSocket() },
methods: { /** * 处理窗口大小变化 */ handleResize() { // 遍历所有图表实例,调用resize方法
Object.values(this.charts).forEach(chart => { if (chart && chart.resize) { chart.resize() } }) },
/** * 更新时间显示 */ updateTime() { const now = new Date() const year = now.getFullYear() const month = String(now.getMonth() + 1).padStart(2, '0') const date = String(now.getDate()).padStart(2, '0') const hours = String(now.getHours()).padStart(2, '0') const minutes = String(now.getMinutes()).padStart(2, '0') const seconds = String(now.getSeconds()).padStart(2, '0') const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] const weekDay = weekDays[now.getDay()]
this.currentTime = `${year}-${month}-${date} ${hours}:${minutes}:${seconds} ${weekDay}` },
/** * 初始化WebSocket连接 */ initWebSocket() { var apiServer = (process.env.NODE_ENV !== 'production' && process.env.OPEN_PROXY ? '/proxyApi/' : window.SITE_CONFIG.baseUrl); // 连接WebSocket服务器
const wsUrl = apiServer + 'ws/dashboard'
WebSocketClient.connect( wsUrl, () => { this.wsConnected = true console.log('[智能立体仓库看板] WebSocket连接成功')
// 订阅智能立体仓库看板主题
this.wsSubscription = WebSocketClient.subscribe( '/topic/dashboard/warehouse-3d', this.handleWebSocketMessage ) console.log('[智能立体仓库看板] 已订阅主题: /topic/dashboard/warehouse-3d') }, (error) => { // 连接失败回调
console.error('[智能立体仓库看板] WebSocket连接失败,将继续使用本地数据', error) this.wsConnected = false } ) },
/** * 处理WebSocket推送的消息 * * <p><b>数据结构说明:</b></p> * <ul> * <li>taskData - 任务统计数据</li> * <li>storageData - 库位利用率数据</li> * <li>robotData - 机器人状态数据</li> * <li>agvData - AGV状态数据</li> * <li>materialRequestData - 领料申请单统计</li> * <li>shipmentData - 发货统计</li> * </ul> * * @param {object} message WebSocket推送的消息 */ handleWebSocketMessage(message) { if (message && message.code === 0) { console.log('[智能立体仓库看板] 收到WebSocket推送数据:', message.data)
// 更新任务统计数据
if (message.data.taskData && Object.keys(message.data.taskData).length > 0) { this.taskData = Object.assign({}, this.taskData, message.data.taskData) }
// 更新库位数据
if (message.data.storageData && Object.keys(message.data.storageData).length > 0) { this.storageData = Object.assign({}, this.storageData, message.data.storageData) // 重新初始化库位利用率图表
this.$nextTick(() => { this.initStorageChart() }) }
// 更新机器人数据
if (message.data.robotData !== undefined) { //this.robotData = message.data.robotData
console.log('[智能立体仓库看板] 机器人数据已更新:', this.robotData.length, '条') }
// 更新AGV数据(使用真实数据,包括空数组)
if (message.data.agvData !== undefined) { this.agvData = message.data.agvData console.log('[智能立体仓库看板] AGV数据已更新:', this.agvData.length, '条') }
// 更新领料申请单数据
if (message.data.materialRequestData && Object.keys(message.data.materialRequestData).length > 0) { this.materialRequestData = Object.assign({}, this.materialRequestData, message.data.materialRequestData) // 重新初始化领料申请单图表
this.$nextTick(() => { this.initMaterialRequestChart() }) }
// 更新发货数据
if (message.data.shipmentData && Object.keys(message.data.shipmentData).length > 0) { this.shipmentData = Object.assign({}, this.shipmentData, message.data.shipmentData) // 重新初始化发货图表
this.$nextTick(() => { this.initShipmentChart() }) } } },
/** * 断开WebSocket连接 */ disconnectWebSocket() { if (this.wsSubscription) { WebSocketClient.unsubscribe(this.wsSubscription) this.wsSubscription = null console.log('[智能立体仓库看板] 已取消订阅') }
if (this.wsConnected) { WebSocketClient.disconnect() this.wsConnected = false console.log('[智能立体仓库看板] WebSocket已断开') } },
/** * 初始化所有图表 */ initCharts() { // 加载 ECharts
if (typeof echarts === 'undefined') { const script = document.createElement('script') script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js' script.onload = () => { this.renderAllCharts() } document.head.appendChild(script) } else { this.renderAllCharts() } },
/** * 渲染所有图表 */ renderAllCharts() { this.initStorageChart() this.initFinishedGoodsTrendChart() this.initRawMaterialTrendChart() this.initStagnantChart() this.initMaterialRequestChart() this.initShipmentChart() },
/** * 初始化库位利用率饼图 */ initStorageChart() { const chartDom = document.getElementById('storageChart') if (!chartDom) return
this.charts.storage = echarts.init(chartDom)
const option = { color: ['#00d4ff', '#7b68ee', '#00ff88'], tooltip: { trigger: 'item', backgroundColor: 'rgba(20, 40, 70, 0.95)', borderColor: '#00d4ff', borderWidth: 1, textStyle: { color: '#fff' } }, legend: { orient: 'vertical', right: '5%', top: 'center', textStyle: { color: '#fff', fontSize: 12 }, itemWidth: 12, itemHeight: 12 }, series: [ { name: '库位类型', type: 'pie', radius: ['45%', '70%'], center: ['35%', '50%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 8, borderColor: '#143050', borderWidth: 2 }, label: { show: true, position: 'inside', formatter: '{d}%', color: '#fff', fontSize: 12, fontWeight: 'bold' }, emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' }, itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 212, 255, 0.5)' } }, data: [ { value: this.storageData.steelPallet, name: '钢托盘', itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 1, y2: 1, colorStops: [ { offset: 0, color: '#00d4ff' }, { offset: 1, color: '#0084ff' } ] } } }, { value: this.storageData.guardPallet, name: '围挡托盘', itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 1, y2: 1, colorStops: [ { offset: 0, color: '#7b68ee' }, { offset: 1, color: '#9370db' } ] } } }, { value: this.storageData.flatPallet, name: '平托', itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 1, y2: 1, colorStops: [ { offset: 0, color: '#00ff88' }, { offset: 1, color: '#00cc70' } ] } } } ] } ] }
this.charts.storage.setOption(option) },
/** * 初始化成品库存趋势图 */ initFinishedGoodsTrendChart() { const chartDom = document.getElementById('finishedGoodsTrendChart') if (!chartDom) return
this.charts.finishedGoods = echarts.init(chartDom)
// 生成当月每日数据
const days = [] const values = [] const today = new Date() const currentMonth = today.getMonth() const daysInMonth = new Date(today.getFullYear(), currentMonth + 1, 0).getDate()
for (let i = 1; i <= daysInMonth; i++) { days.push(`${i}日`) // 模拟数据:基础值 + 随机波动
values.push(Math.floor(12000 + Math.random() * 3000)) }
const option = { color: ['#00d4ff'], tooltip: { trigger: 'axis', backgroundColor: 'rgba(20, 40, 70, 0.95)', borderColor: '#00d4ff', borderWidth: 1, textStyle: { color: '#fff' }, axisPointer: { type: 'cross', label: { backgroundColor: '#00d4ff' } } }, grid: { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: days, axisLine: { lineStyle: { color: '#3a7fb0' } }, axisLabel: { color: '#8ab8d6', fontSize: 10 } }, yAxis: { type: 'value', splitLine: { lineStyle: { color: '#3a7fb0', type: 'dashed' } }, axisLine: { lineStyle: { color: '#3a7fb0' } }, axisLabel: { color: '#8ab8d6', fontSize: 10 } }, series: [ { name: '库存数量', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, lineStyle: { color: '#00d4ff', width: 2 }, itemStyle: { color: '#00d4ff', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(0, 212, 255, 0.3)' }, { offset: 1, color: 'rgba(0, 212, 255, 0.05)' } ] } }, data: values } ] }
this.charts.finishedGoods.setOption(option) },
/** * 初始化原材库存趋势图 */ initRawMaterialTrendChart() { const chartDom = document.getElementById('rawMaterialTrendChart') if (!chartDom) return
this.charts.rawMaterial = echarts.init(chartDom)
// 生成当月每日数据
const days = [] const values = [] const today = new Date() const currentMonth = today.getMonth() const daysInMonth = new Date(today.getFullYear(), currentMonth + 1, 0).getDate()
for (let i = 1; i <= daysInMonth; i++) { days.push(`${i}日`) // 模拟数据:基础值 + 随机波动
values.push(Math.floor(8000 + Math.random() * 2000)) }
const option = { color: ['#00ff88'], tooltip: { trigger: 'axis', backgroundColor: 'rgba(20, 40, 70, 0.95)', borderColor: '#00ff88', borderWidth: 1, textStyle: { color: '#fff' }, axisPointer: { type: 'cross', label: { backgroundColor: '#00ff88' } } }, grid: { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: days, axisLine: { lineStyle: { color: '#3a7fb0' } }, axisLabel: { color: '#8ab8d6', fontSize: 10 } }, yAxis: { type: 'value', splitLine: { lineStyle: { color: '#3a7fb0', type: 'dashed' } }, axisLine: { lineStyle: { color: '#3a7fb0' } }, axisLabel: { color: '#8ab8d6', fontSize: 10 } }, series: [ { name: '库存数量', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, lineStyle: { color: '#00ff88', width: 2 }, itemStyle: { color: '#00ff88', borderColor: '#fff', borderWidth: 2 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(0, 255, 136, 0.3)' }, { offset: 1, color: 'rgba(0, 255, 136, 0.05)' } ] } }, data: values } ] }
this.charts.rawMaterial.setOption(option) },
/** * 初始化呆滞情况柱状图 */ initStagnantChart() { const chartDom = document.getElementById('stagnantChart') if (!chartDom) return
this.charts.stagnant = echarts.init(chartDom)
const option = { color: ['#00ff88', '#ffaa00', '#ff9900', '#9370db'], tooltip: { trigger: 'axis', backgroundColor: 'rgba(20, 40, 70, 0.95)', borderColor: '#ffaa00', borderWidth: 1, textStyle: { color: '#fff' }, axisPointer: { type: 'shadow' } }, grid: { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }, xAxis: { type: 'category', data: ['30天内', '30-90天', '90-180天', '180天以上'], axisLine: { lineStyle: { color: '#3a7fb0' } }, axisLabel: { color: '#8ab8d6', fontSize: 11 } }, yAxis: { type: 'value', name: '数量(件)', nameTextStyle: { color: '#8ab8d6' }, splitLine: { lineStyle: { color: '#3a7fb0', type: 'dashed' } }, axisLine: { lineStyle: { color: '#3a7fb0' } }, axisLabel: { color: '#8ab8d6', fontSize: 10 } }, series: [ { name: '呆滞数量', type: 'bar', barWidth: '50%', data: [ { value: 320, itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: '#00ff88' }, { offset: 1, color: '#00cc70' } ] } } }, { value: 180, itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: '#ffaa00' }, { offset: 1, color: '#ff8800' } ] } } }, { value: 95, itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: '#ff9900' }, { offset: 1, color: '#ff7700' } ] } } }, { value: 42, itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: '#9370db' }, { offset: 1, color: '#7b68ee' } ] } } } ], label: { show: true, position: 'top', color: '#fff', fontSize: 11, fontWeight: 'bold' }, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(255, 165, 0, 0.5)' } } } ] }
this.charts.stagnant.setOption(option) },
/** * 初始化领料申请单环形图 */ initMaterialRequestChart() { const chartDom = document.getElementById('materialRequestChart') if (!chartDom) return
this.charts.materialRequest = echarts.init(chartDom)
const option = { color: ['#00ff88', '#00d4ff', '#ffaa00'], tooltip: { trigger: 'item', backgroundColor: 'rgba(20, 40, 70, 0.95)', borderColor: '#00d4ff', borderWidth: 1, textStyle: { color: '#fff' } }, legend: { orient: 'vertical', left: '3%', top: 'center', textStyle: { color: '#8ab8d6', fontSize: 12 }, itemWidth: 12, itemHeight: 12, itemGap: 10 }, series: [ { name: '申请单状态', type: 'pie', radius: ['36%', '52%'], center: ['52%', '50%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 6, borderColor: '#143050', borderWidth: 2 }, label: { show: false }, emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } }, data: [ { value: this.materialRequestData.completed, name: '已完成', itemStyle: { color: '#00ff88' } }, { value: this.materialRequestData.processing, name: '进行中', itemStyle: { color: '#00d4ff' } }, { value: this.materialRequestData.pending, name: '待处理', itemStyle: { color: '#ffaa00' } } ] } ] }
this.charts.materialRequest.setOption(option) },
/** * 初始化发货环形图 */ initShipmentChart() { const chartDom = document.getElementById('shipmentChart') if (!chartDom) return
this.charts.shipment = echarts.init(chartDom)
const option = { color: ['#00ff88', '#00d4ff', '#ffaa00'], tooltip: { trigger: 'item', backgroundColor: 'rgba(20, 40, 70, 0.95)', borderColor: '#00d4ff', borderWidth: 1, textStyle: { color: '#fff' } }, legend: { orient: 'vertical', left: '3%', top: 'center', textStyle: { color: '#8ab8d6', fontSize: 12 }, itemWidth: 12, itemHeight: 12, itemGap: 10 }, series: [ { name: '发货状态', type: 'pie', radius: ['36%', '52%'], center: ['52%', '50%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 6, borderColor: '#143050', borderWidth: 2 }, label: { show: false }, emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } }, data: [ { value: this.shipmentData.completed, name: '已发货', itemStyle: { color: '#00ff88' } }, { value: this.shipmentData.processing, name: '拣选中', itemStyle: { color: '#00d4ff' } }, { value: this.shipmentData.pending, name: '待拣选', itemStyle: { color: '#ffaa00' } } ] } ] }
this.charts.shipment.setOption(option) } }}</script>
<style scoped lang="scss">.warehouse-3d-screen { width: 100vw; height: 100vh; background: linear-gradient(135deg, #1a2f4a 0%, #2a4060 50%, #1f3a56 100%); overflow: hidden; position: relative; font-family: 'Microsoft YaHei', Arial, sans-serif;}
/* ========== 装饰背景 ========== */.bg-decoration { position: absolute; width: 100%; height: 100%; pointer-events: none; z-index: 0;
.grid-bg { position: absolute; width: 100%; height: 100%; background-image: linear-gradient(rgba(0, 212, 255, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 212, 255, 0.03) 1px, transparent 1px); background-size: 50px 50px; }
.decoration-line { position: absolute; height: 2px; background: linear-gradient(90deg, transparent, #00d4ff, transparent); opacity: 0.3; animation: lineMove 8s linear infinite;
&.line-1 { width: 40%; top: 20%; left: -40%; }
&.line-2 { width: 30%; top: 50%; right: -30%; animation-delay: 2s; }
&.line-3 { width: 50%; top: 80%; left: -50%; animation-delay: 4s; } }
.decoration-circle { position: absolute; border: 2px solid rgba(0, 212, 255, 0.2); border-radius: 50%; animation: circleScale 6s ease-in-out infinite;
&.circle-1 { width: 300px; height: 300px; top: 10%; right: 5%; }
&.circle-2 { width: 200px; height: 200px; bottom: 15%; left: 10%; animation-delay: 3s; } }}
@keyframes lineMove { 0% { transform: translateX(0); } 100% { transform: translateX(200%); }}
@keyframes circleScale { 0%, 100% { transform: scale(1); opacity: 0.2; } 50% { transform: scale(1.2); opacity: 0.4; }}
/* ========== 顶部标题栏 ========== */.screen-header { position: relative; z-index: 10; height: 80px; display: flex; align-items: center; justify-content: space-between; padding: 0 40px; background: linear-gradient(180deg, rgba(20, 50, 80, 0.9) 0%, rgba(15, 40, 65, 0.8) 100%); border-bottom: 2px solid rgba(0, 212, 255, 0.4); box-shadow: 0 2px 20px rgba(0, 212, 255, 0.3);
.header-logo { flex-shrink: 0; width: 120px; height: 50px; display: flex; align-items: center;
.logo-img { max-width: 100%; max-height: 100%; object-fit: contain; filter: drop-shadow(0 0 10px rgba(0, 212, 255, 0.5)); } }
.header-decoration { width: 100px; height: 2px; background: linear-gradient(90deg, transparent, #00d4ff);
&.right { background: linear-gradient(90deg, #00d4ff, transparent); } }
.header-center { flex: 1; text-align: center; position: relative;
.title-glow { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500px; height: 60px; background: radial-gradient(ellipse, rgba(0, 212, 255, 0.2), transparent); filter: blur(20px); }
.screen-title { font-size: 32px; font-weight: bold; color: #fff; margin: 0; text-shadow: 0 0 10px rgba(0, 212, 255, 0.8), 0 0 20px rgba(0, 212, 255, 0.5), 0 0 30px rgba(0, 212, 255, 0.3); letter-spacing: 4px; position: relative; }
.title-subtitle { font-size: 13px; color: #8ab8d6; margin-top: 4px; letter-spacing: 2px; text-transform: uppercase; } }
.header-time { flex-shrink: 0; display: flex; align-items: center; padding: 8px 16px; background: rgba(0, 212, 255, 0.1); border: 1px solid rgba(0, 212, 255, 0.3); border-radius: 6px;
.time-icon { width: 6px; height: 6px; background: #00ff88; border-radius: 50%; margin-right: 10px; animation: blink 2s ease-in-out infinite; }
.time-text { font-size: 14px; color: #8ab8d6; font-weight: 500; letter-spacing: 1px; } }}
@keyframes blink { 0%, 100% { opacity: 1; box-shadow: 0 0 5px #00ff88; } 50% { opacity: 0.3; box-shadow: none; }}
/* ========== 主内容区 ========== */.screen-content { position: relative; z-index: 1; padding: 15px; height: calc(100vh - 100px); overflow-y: auto; display: flex; flex-direction: column; gap: 12px;
&::-webkit-scrollbar { width: 6px; }
&::-webkit-scrollbar-track { background: rgba(30, 60, 100, 0.5); }
&::-webkit-scrollbar-thumb { background: rgba(0, 212, 255, 0.5); border-radius: 3px;
&:hover { background: rgba(0, 212, 255, 0.7); } }}
.content-row { display: flex; gap: 12px;
&.row-1 { height: 330px; }
&.row-2 { height: 300px; }
&.row-3 { height: 330px; }}
/* ========== 卡片面板通用样式 ========== */.panel-card { flex: 1; background: rgba(20, 50, 80, 0.6); border: 1px solid rgba(0, 212, 255, 0.4); border-radius: 12px; backdrop-filter: blur(10px); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); display: flex; flex-direction: column; overflow: hidden; transition: all 0.3s ease;
&:hover { border-color: rgba(0, 212, 255, 0.6); box-shadow: 0 6px 30px rgba(0, 212, 255, 0.3); transform: translateY(-2px); }
.card-header { padding: 10px 14px; background: linear-gradient(90deg, rgba(0, 212, 255, 0.25) 0%, rgba(0, 212, 255, 0.1) 100%); border-bottom: 1px solid rgba(0, 212, 255, 0.4); display: flex; align-items: center; gap: 10px;
.header-icon { width: 4px; height: 20px; background: linear-gradient(180deg, #00d4ff, #0084ff); border-radius: 2px; box-shadow: 0 0 10px rgba(0, 212, 255, 0.6); }
.header-title { font-size: 16px; font-weight: bold; color: #fff; letter-spacing: 1px; }
.header-subtitle { font-size: 11px; color: #8ab8d6; margin-left: auto; text-transform: uppercase; letter-spacing: 1px; } }
.card-body { flex: 1; padding: 12px; overflow: hidden; }}
/* ========== 任务统计卡片 ========== */.task-summary { .task-stats { display: flex; gap: 16px; margin-bottom: 20px;
.stat-item { flex: 1; display: flex; align-items: center; gap: 12px; padding: 16px; background: linear-gradient(135deg, rgba(0, 212, 255, 0.1) 0%, rgba(0, 132, 255, 0.1) 100%); border: 1px solid rgba(0, 212, 255, 0.3); border-radius: 10px;
.stat-icon { width: 50px; height: 50px; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #00d4ff, #0084ff); border-radius: 10px; font-size: 24px; color: #fff; box-shadow: 0 4px 15px rgba(0, 212, 255, 0.4); }
.stat-content { flex: 1;
.stat-label { font-size: 12px; color: #8ab8d6; margin-bottom: 6px; }
.stat-value { font-size: 28px; font-weight: bold; color: #00d4ff; line-height: 1; text-shadow: 0 0 10px rgba(0, 212, 255, 0.5); }
.stat-unit { font-size: 12px; color: #8ab8d6; margin-top: 4px; } } }
.stat-divider { width: 2px; background: linear-gradient(180deg, transparent, rgba(0, 212, 255, 0.5), transparent); } }
.task-breakdown { display: flex; gap: 12px;
.breakdown-item { flex: 1; padding: 16px; background: rgba(25, 50, 85, 0.7); border: 1px solid rgba(0, 212, 255, 0.2); border-radius: 8px; display: flex; flex-direction: column; align-items: center; gap: 8px;
.item-icon { font-size: 32px; margin-bottom: 4px; }
.item-label { font-size: 13px; color: #8ab8d6; }
.item-value { font-size: 24px; font-weight: bold; color: #fff; }
.item-percent { font-size: 12px; color: #00ff88; }
&.outbound { border-top: 3px solid #00d4ff; }
&.inbound { border-top: 3px solid #00ff88; } } }}
/* ========== 库位利用率卡片 ========== */.storage-utilization { .utilization-summary { display: flex; justify-content: space-around; margin-bottom: 16px; padding: 12px; background: rgba(25, 50, 85, 0.7); border-radius: 8px;
.summary-item { display: flex; flex-direction: column; align-items: center; gap: 6px;
.summary-label { font-size: 12px; color: #8ab8d6; }
.summary-value { font-size: 22px; font-weight: bold; color: #fff;
&.used { color: #00d4ff; }
&.rate { color: #00ff88; } } } }
.chart-container { height: calc(100% - 70px); }}
/* ========== 设备状态卡片 ========== */.device-status { .device-group { margin-bottom: 6px;
&:last-child { margin-bottom: 0; }
.group-title { font-size: 13px; color: #8ab8d6; margin-bottom: 2px; padding-bottom: 4px; border-bottom: 1px solid rgba(0, 212, 255, 0.2); }
.device-list { display: flex; flex-direction: column; gap: 6px;
&.agv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; } }
.device-item { padding: 8px 10px; background: rgba(25, 50, 85, 0.7); border: 1px solid rgba(0, 212, 255, 0.2); border-left: 3px solid #00d4ff; border-radius: 6px;
&.working { border-left-color: #00ff88;
.status-dot { background: #00ff88; box-shadow: 0 0 10px #00ff88; } }
&.idle { border-left-color: #ffaa00;
.status-dot { background: #ffaa00; box-shadow: 0 0 10px #ffaa00; } }
&.charging { border-left-color: #00d4ff;
.status-dot { background: #00d4ff; box-shadow: 0 0 10px #00d4ff; } }
.device-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.device-name { font-size: 14px; color: #fff; font-weight: bold; }
.device-status-badge { display: flex; align-items: center; gap: 6px;
.status-dot { width: 8px; height: 8px; border-radius: 50%; animation: pulse 2s ease-in-out infinite; }
.status-text { font-size: 12px; color: #8ab8d6; } }
.device-metrics { display: flex; justify-content: space-between;
.metric { font-size: 11px; color: #8ab8d6; } } }
.device-item-compact { padding: 8px; background: rgba(25, 50, 85, 0.7); border: 1px solid rgba(0, 212, 255, 0.2); border-radius: 6px;
&.working { border-left: 3px solid #00ff88;
.status-dot { background: #00ff88; box-shadow: 0 0 8px #00ff88; } }
&.idle { border-left: 3px solid #ffaa00;
.status-dot { background: #ffaa00; box-shadow: 0 0 8px #ffaa00; } }
&.charging { border-left: 3px solid #00d4ff;
.status-dot { background: #00d4ff; box-shadow: 0 0 8px #00d4ff; } }
.compact-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;
.compact-name { font-size: 13px; color: #fff; font-weight: bold; }
.compact-status { display: flex; align-items: center; gap: 4px; font-size: 11px; color: #8ab8d6;
.status-dot { width: 6px; height: 6px; border-radius: 50%; animation: pulse 2s ease-in-out infinite; } } }
.compact-info { display: flex; justify-content: space-between; font-size: 11px; color: #8ab8d6; } } }}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; }}
/* ========== 空状态提示 ========== */.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; color: rgba(255, 255, 255, 0.4); font-size: 13px; grid-column: 1 / -1; /* 占据整个网格 */
i { font-size: 28px; margin-bottom: 8px; opacity: 0.6; }
span { opacity: 0.8; }}
/* ========== 库存趋势卡片 ========== */.inventory-trend { .chart-container { width: 100%; height: 100%; }}
/* ========== 呆滞分析卡片 ========== */.stagnant-analysis { .chart-container { width: 100%; height: 100%; }}
/* ========== 当日作业统计卡片 ========== */.daily-operation { .operation-visual { display: flex; gap: 8px; height: calc(100% - 10px);
.visual-left { width: 280px; height: 250px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; padding: 15px;
.mini-chart { width: 100%; height: 100%; } }
.visual-right { flex: 1; display: flex; flex-direction: column; justify-content: space-between;
.operation-stat { flex: 1; display: flex; flex-direction: column; justify-content: space-around;
.stat-row { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; background: rgba(25, 50, 85, 0.7); border-radius: 6px; border-left: 3px solid transparent;
.stat-label { font-size: 12px; color: #8ab8d6; }
.stat-number { font-size: 16px; font-weight: bold; color: #fff;
&.total { color: #00d4ff; }
&.completed { color: #00ff88; }
&.processing { color: #ffaa00; }
&.pending { color: #9370db; } }
&:nth-child(1) { border-left-color: #00d4ff; } &:nth-child(2) { border-left-color: #00ff88; } &:nth-child(3) { border-left-color: #ffaa00; } &:nth-child(4) { border-left-color: #9370db; } } }
.completion-rate { margin-top: 8px; padding: 8px; background: rgba(0, 212, 255, 0.1); border-radius: 8px;
.rate-label { font-size: 10px; color: #8ab8d6; margin-bottom: 3px; }
.rate-value { font-size: 20px; font-weight: bold; color: #00ff88; margin-bottom: 5px; text-shadow: 0 0 10px rgba(0, 255, 136, 0.5); }
.rate-bar { height: 6px; background: rgba(25, 50, 85, 0.9); border-radius: 3px; overflow: hidden;
.rate-fill { height: 100%; background: linear-gradient(90deg, #00ff88, #00d4ff); border-radius: 4px; transition: width 0.5s ease; box-shadow: 0 0 10px rgba(0, 255, 136, 0.5); } } } } }}
/* ========== 响应式设计 ========== */@media screen and (max-width: 1600px) { .screen-header { height: 70px; padding: 0 30px;
.screen-title { font-size: 28px; }
.title-subtitle { font-size: 12px; } }
.content-row { &.row-1 { height: 310px; } &.row-2 { height: 280px; } &.row-3 { height: 310px; } }}</style>
|