You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

2031 lines
52 KiB

<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>