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.
 
 
 
 
 

2462 lines
63 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/cclbai.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">ZhongShan 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.monthlyTasks }}</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.todayTasks }}</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 class="pallet-statistics">
<!-- 空托盘总数突出显示 -->
<div class="pallet-total-section">
<div class="pallet-total-item">
<div class="total-icon">📦</div>
<div class="total-info">
<div class="total-label">空托盘总数</div>
<div class="total-value">{{ emptyPalletTotal }}</div>
</div>
</div>
</div>
<!-- 分类明细 -->
<div class="pallet-detail-section">
<div class="detail-items">
<div class="pallet-item flat">
<div class="pallet-icon">📋</div>
<div class="pallet-info">
<div class="pallet-label">平托盘</div>
<div class="pallet-value">{{ (storageData.emptyContainerInventory && storageData.emptyContainerInventory.flatPallet) || 0 }}</div>
</div>
</div>
<div class="pallet-item guard">
<div class="pallet-icon">🔲</div>
<div class="pallet-info">
<div class="pallet-label">围框托盘</div>
<div class="pallet-value">{{ (storageData.emptyContainerInventory && storageData.emptyContainerInventory.framePallet) || 0 }}</div>
</div>
</div>
<div class="pallet-item steel">
<div class="pallet-icon"></div>
<div class="pallet-info">
<div class="pallet-label">钢托盘</div>
<div class="pallet-value">{{ (storageData.emptyContainerInventory && storageData.emptyContainerInventory.steelPallet) || 0 }}</div>
</div>
</div>
</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 rate">{{ 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"></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" style="margin-top: 10px">
<!-- 成品库存量趋势 -->
<div class="panel-card inventory-trend">
<div class="card-header">
<div class="header-icon"></div>
<span class="header-title">原材料库存(M²)</span>
<span class="header-subtitle">Raw Material Inventory</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">规格料库存(M)</span>
<span class="header-subtitle">Specified Materials Inventory</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">产成品库存(PCS)</span>
<span class="header-subtitle">Finished Goods Inventory</span>
</div>
<div class="card-body">
<div id="stagnantChart" class="chart-container"></div>
</div>
</div>
</div>
</div>
<!-- 底部装饰效果 -->
<div class="bottom-decoration-bar">
<!-- 浮动数据点 -->
<div class="floating-data-points">
<div class="data-point point-1">
<div class="point-ring"></div>
<div class="point-core"></div>
</div>
<div class="data-point point-2">
<div class="point-ring"></div>
<div class="point-core"></div>
</div>
<div class="data-point point-3">
<div class="point-ring"></div>
<div class="point-core"></div>
</div>
<div class="data-point point-4">
<div class="point-ring"></div>
<div class="point-core"></div>
</div>
<div class="data-point point-5">
<div class="point-ring"></div>
<div class="point-core"></div>
</div>
</div>
</div>
</div>
</template>
<script>
import WebSocketClient from '@/utils/websocket'
export default {
name: 'Warehouse3DBoard',
data() {
return {
currentTime: '',
timeInterval: null, // 时间更新定时器
serverTimeOffset: 0, // 服务器时间偏移量(毫秒)
refreshCheckInterval: null, // 定时刷新检查定时器
// WebSocket相关
useWebSocket: true, // 是否使用WebSocket(可切换为false降级到本地数据)
wsConnected: false, // WebSocket连接状态
wsSubscription: null, // WebSocket订阅ID
// 任务统计数据
taskData: {
totalTasks: 0,
monthlyTasks: 0,
todayTasks: 0,
outboundTasks: 0,
inboundTasks: 0,
outboundPercent: 0,
inboundPercent: 0
},
// 库位数据
storageData: {
totalSlots: 1960,// 固定不变的
usedSlots: 0,
utilizationRate: 0,
// 物料盘库存(按托盘类型分类)
materialInventory: {
steelPallet: 0,
framePallet: 0,
flatPallet: 0
},
// 空盘库存(按托盘类型分类)
emptyContainerInventory: {
flatPallet: 0,
framePallet: 0,
steelPallet: 0
}
},
// 机器人数据
robotData: [
{ id: 1, name: '机械臂#1', status: 'working', statusText: '工作中', efficiency: 95, tasks: 1 },
{ id: 2, name: '机械臂#2', status: 'working', statusText: '工作中', efficiency: 92, tasks: 1 }
],
// 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
},
// 库存趋势数据
rawMaterialTrend: [], // 原材料库存趋势
specifiedMaterialTrend: [], // 规格料库存趋势
finishedGoodsTrend: [], // 产成品库存趋势
// 图表实例
charts: {}
}
},
computed: {
/**
* 计算空托盘总数
*/
emptyPalletTotal() {
const empty = this.storageData.emptyContainerInventory || {}
return (empty.flatPallet || 0) + (empty.framePallet || 0) + (empty.steelPallet || 0)
},
/**
* 计算可用库位数(总库位 - 已使用库位)
*/
availableSlots() {
return this.storageData.totalSlots - this.storageData.usedSlots
}
},
mounted() {
// 初始化时间显示
this.currentTime = '等待服务器时间同步...'
// 启动时钟定时器(每秒更新)
this.timeInterval = setInterval(() => {
this.updateTime()
}, 1000)
// 启动定时刷新检查(每分钟检查一次)
this.startRefreshCheck()
// 延迟初始化图表,确保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)
}
// 清理定时刷新检查定时器
if (this.refreshCheckInterval) {
clearInterval(this.refreshCheckInterval)
}
// 移除窗口resize监听
window.removeEventListener('resize', this.handleResize)
// 销毁所有图表
Object.values(this.charts).forEach(chart => {
if (chart) chart.dispose()
})
// 断开WebSocket连接
this.disconnectWebSocket()
},
methods: {
/**
* 启动定时刷新检查
* 每分钟检查一次,如果到达凌晨5点则自动刷新页面
*/
startRefreshCheck() {
console.log('[智能立体仓库看板] 已启动定时刷新检查,将在每天凌晨5:00自动刷新页面')
// 每分钟检查一次
this.refreshCheckInterval = setInterval(() => {
this.checkAndRefreshPage()
}, 60000) // 60秒 = 1分钟
// 立即执行一次检查
this.checkAndRefreshPage()
},
/**
* 检查当前时间,如果是凌晨5点则刷新页面
*/
checkAndRefreshPage() {
const now = new Date()
const hours = now.getHours()
const minutes = now.getMinutes()
// 判断是否为凌晨5:00-5:10之间
if (hours === 5 && minutes === 10) {
location.reload()
}
},
/**
* 处理窗口大小变化
*/
handleResize() {
// 遍历所有图表实例,调用resize方法
Object.values(this.charts).forEach(chart => {
if (chart && chart.resize) {
chart.resize()
}
})
},
/**
* 更新服务器时间偏移量
*
* @param {string} serverTimeString - 服务器时间字符串
*/
updateServerTimeOffset(serverTimeString) {
try {
// 解析服务器时间字符串 "2025-11-01 14:30:25 星期五"
const timeStr = serverTimeString.split(' ')[0] + ' ' + serverTimeString.split(' ')[1]
const serverTime = new Date(timeStr).getTime()
const localTime = new Date().getTime()
if (!isNaN(serverTime)) {
this.serverTimeOffset = serverTime - localTime
console.log('服务器时间偏移量更新:', this.serverTimeOffset, 'ms')
}
} catch (error) {
console.warn('解析服务器时间失败:', error)
}
},
/**
* 更新时间显示(使用服务器时间偏移量)
*/
updateTime() {
// 使用本地时间 + 偏移量来计算服务器时间
const now = new Date(new Date().getTime() + this.serverTimeOffset)
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.serverTime) {
this.updateServerTimeOffset(message.serverTime)
}
// 更新任务统计数据
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 = {
...this.storageData,
...message.data.storageData,
materialInventory: {
...this.storageData.materialInventory,
...(message.data.storageData.materialInventory || {})
},
emptyContainerInventory: {
...this.storageData.emptyContainerInventory,
...(message.data.storageData.emptyContainerInventory || {})
}
}
console.log('[智能立体仓库看板] 库位数据已更新:', this.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
}
// 更新领料申请单数据
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()
})
}
// 更新库存趋势数据
if (message.data.rawMaterialTrend && message.data.rawMaterialTrend.length > 0) {
this.rawMaterialTrend = message.data.rawMaterialTrend
// 重新初始化原材料趋势图表
this.$nextTick(() => {
this.initFinishedGoodsTrendChart()
})
}
if (message.data.specifiedMaterialTrend && message.data.specifiedMaterialTrend.length > 0) {
this.specifiedMaterialTrend = message.data.specifiedMaterialTrend
// 重新初始化规格料趋势图表
this.$nextTick(() => {
this.initRawMaterialTrendChart()
})
}
if (message.data.finishedGoodsTrend && message.data.finishedGoodsTrend.length > 0) {
this.finishedGoodsTrend = message.data.finishedGoodsTrend
// 重新初始化产成品趋势图表
this.$nextTick(() => {
this.initStagnantChart()
})
}
}
},
/**
* 断开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 materialInventory = this.storageData.materialInventory || {}
const materialSteelPallet = materialInventory.steelPallet || 0
const materialFramePallet = materialInventory.framePallet || 0
const materialFlatPallet = materialInventory.flatPallet || 0
// 计算可用库位数
const availableSlots = this.availableSlots
// 调试:打印数据
console.log('初始化饼图 - 物料盘数据:', { materialSteelPallet, materialFramePallet, materialFlatPallet })
console.log('初始化饼图 - 可用库位:', availableSlots)
console.log('原始storageData:', this.storageData)
const option = {
color: ['#00d4ff', '#7b68ee', '#00ff88','#c5d9ed'],
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: ['30%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 8,
borderColor: '#143050',
borderWidth: 2
},
label: {
show: true,
position: 'inside',
formatter: function(params) {
const percent = params.percent ? params.percent.toFixed(1) : '0.0'
return percent + '%'
},
color: '#fff',
fontSize: 12,
fontWeight: 'bold'
},
emphasis: {
label: {
show: true,
formatter: function(params) {
const percent = params.percent ? params.percent.toFixed(1) : '0.0'
return percent + '%'
},
fontSize: 14,
fontWeight: 'bold'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 212, 255, 0.5)'
}
},
data: [
{
value: materialSteelPallet,
name: '钢托盘(' + materialSteelPallet + ')',
itemStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 1, y2: 1,
colorStops: [
{ offset: 0, color: '#00d4ff' },
{ offset: 1, color: '#0084ff' }
]
}
}
},
{
value: materialFramePallet,
name: '围框托盘(' + materialFramePallet + ')',
itemStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 1, y2: 1,
colorStops: [
{ offset: 0, color: '#7b68ee' },
{ offset: 1, color: '#9370db' }
]
}
}
},
{
value: materialFlatPallet,
name: '平托盘(' + materialFlatPallet + ')',
itemStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 1, y2: 1,
colorStops: [
{ offset: 0, color: '#00ff88' },
{ offset: 1, color: '#00cc70' }
]
}
}
},
{
value: availableSlots,
name: '可用库位(' + availableSlots + ')',
itemStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 1, y2: 1,
colorStops: [
{ offset: 0, color: '#c5d9ed' },
{ offset: 1, color: '#f4edb9' }
]
}
}
}
]
}
]
}
this.charts.storage.setOption(option)
},
/**
* 初始化原材料库存趋势图(使用真实数据)
*/
initFinishedGoodsTrendChart() {
const chartDom = document.getElementById('finishedGoodsTrendChart')
if (!chartDom) return
this.charts.finishedGoods = echarts.init(chartDom)
// 使用真实数据或生成空数据
const days = []
const values = []
if (this.rawMaterialTrend && this.rawMaterialTrend.length > 0) {
// 使用从后端获取的真实数据
this.rawMaterialTrend.forEach(item => {
days.push(`${item.dayNum}日`)
values.push(item.quantity || 0)
})
} else {
// 如果没有数据,生成当月空数据占位
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(0)
}
}
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 = []
if (this.specifiedMaterialTrend && this.specifiedMaterialTrend.length > 0) {
// 使用从后端获取的真实数据
this.specifiedMaterialTrend.forEach(item => {
days.push(`${item.dayNum}日`)
values.push(item.quantity || 0)
})
} else {
// 如果没有数据,生成当月空数据占位
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(0)
}
}
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 days = []
const values = []
if (this.finishedGoodsTrend && this.finishedGoodsTrend.length > 0) {
// 使用从后端获取的真实数据
this.finishedGoodsTrend.forEach(item => {
days.push(`${item.dayNum}日`)
values.push(item.quantity || 0)
})
} else {
// 如果没有数据,生成当月空数据占位
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(0)
}
}
const option = {
color: ['#9370db'],
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(20, 40, 70, 0.95)',
borderColor: '#9370db',
borderWidth: 1,
textStyle: {
color: '#fff'
},
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#9370db'
}
}
},
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: '#9370db',
width: 2
},
itemStyle: {
color: '#9370db',
borderColor: '#fff',
borderWidth: 2
},
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(147, 112, 219, 0.3)' },
{ offset: 1, color: 'rgba(147, 112, 219, 0.05)' }
]
}
},
data: values
}
]
}
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: 250px;
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: 28px;
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: 2px;
position: relative;
}
.title-subtitle {
font-size: 12px;
color: #8ab8d6;
margin-top: 4px;
letter-spacing: 1px;
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 - 140px); /* 减去顶部80px和底部装饰效果40px */
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
&::-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: 400px;
}
&.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: 12px;
margin-bottom: 12px;
.stat-item {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
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: 8px;
.stat-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #00d4ff, #0084ff);
border-radius: 8px;
font-size: 20px;
color: #fff;
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.4);
}
.stat-content {
flex: 1;
.stat-label {
font-size: 11px;
color: #8ab8d6;
margin-bottom: 4px;
}
.stat-value {
font-size: 22px;
font-weight: bold;
color: #00d4ff;
line-height: 1;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.stat-unit {
font-size: 11px;
color: #8ab8d6;
margin-top: 3px;
}
}
}
.stat-divider {
width: 2px;
background: linear-gradient(180deg, transparent, rgba(0, 212, 255, 0.5), transparent);
}
}
.task-breakdown {
display: flex;
gap: 10px;
margin-bottom: 10px;
.breakdown-item {
flex: 1;
padding: 10px 12px;
background: rgba(25, 50, 85, 0.7);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
.item-icon {
font-size: 24px;
margin-bottom: 2px;
}
.item-label {
font-size: 11px;
color: #8ab8d6;
}
.item-value {
font-size: 20px;
font-weight: bold;
color: #fff;
}
.item-percent {
font-size: 11px;
color: #00ff88;
}
&.outbound {
border-top: 2px solid #00d4ff;
}
&.inbound {
border-top: 2px solid #00ff88;
}
}
}
/* 托盘数量统计样式 */
.pallet-statistics {
display: flex;
flex-direction: column;
gap: 6px;
/* 空托盘总数区域 */
.pallet-total-section {
.pallet-total-item {
padding: 1px 16px;
background: linear-gradient(135deg, rgba(192, 244, 181, 0.15) 0%, rgba(198, 230, 177, 0.15) 100%);
border: 2px solid rgba(255, 215, 0, 0.4);
border-radius: 8px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.25);
border-color: rgba(255, 215, 0, 0.6);
}
.total-icon {
font-size: 20px;
opacity: 0.95;
}
.total-info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
.total-label {
font-size: 13px;
font-weight: 600;
color: #bded8e;
letter-spacing: 1px;
}
.total-value {
font-size: 20px;
font-weight: bold;
color: #bded8e;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.3);
}
}
}
}
/* 分类明细区域 */
.pallet-detail-section {
margin-left: 5px;
margin-right: 5px;
.detail-title {
font-size: 11px;
color: #8ab8d6;
margin-bottom: 8px;
padding-left: 4px;
letter-spacing: 0.5px;
opacity: 0.9;
}
.detail-items {
display: flex;
gap: 8px;
}
}
.pallet-item {
flex: 1;
min-width: 100px;
padding: 4px 12px;
background: rgba(25, 50, 85, 0.5);
border: 1px solid rgba(0, 212, 255, 0.15);
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
&:hover {
background: rgba(25, 50, 85, 0.8);
border-color: rgba(0, 212, 255, 0.4);
transform: translateY(-2px);
}
.pallet-icon {
font-size: 22px;
opacity: 0.9;
}
.pallet-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
.pallet-label {
font-size: 11px;
color: #8ab8d6;
letter-spacing: 0.3px;
}
.pallet-value {
font-size: 17px;
font-weight: bold;
color: #fff;
}
}
/* 不同托盘类型的边框颜色 */
&.flat {
border-left: 2px solid #00ff88;
.pallet-value {
color: #00ff88;
}
}
&.guard {
border-left: 2px solid #7b68ee;
.pallet-value {
color: #9370db;
}
}
&.steel {
border-left: 2px solid #00d4ff;
.pallet-value {
color: #00d4ff;
}
}
&.other {
border-left: 2px solid #b3f38d;
.pallet-value {
color: #b0ef77;
}
}
}
}
}
/* ========== 库位利用率卡片 ========== */
.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);
}
}
}
}
}
}
/* ========== 底部装饰效果 ========== */
.bottom-decoration-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
z-index: 100;
pointer-events: none;
}
/* 浮动数据点 */
.floating-data-points {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
}
.data-point {
position: absolute;
bottom: 8px;
&.point-1 {
left: 15%;
animation: float-up 3s ease-in-out infinite;
}
&.point-2 {
left: 30%;
animation: float-up 3.5s ease-in-out infinite 0.5s;
}
&.point-3 {
left: 50%;
animation: float-up 3.2s ease-in-out infinite 1s;
}
&.point-4 {
left: 70%;
animation: float-up 3.8s ease-in-out infinite 1.5s;
}
&.point-5 {
left: 85%;
animation: float-up 3.3s ease-in-out infinite 2s;
}
}
.point-ring {
width: 14px;
height: 14px;
border: 2px solid rgba(0, 212, 255, 0.6);
border-radius: 50%;
animation: ring-expand 2s ease-out infinite;
}
.point-core {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 6px;
height: 6px;
background: #00d4ff;
border-radius: 50%;
box-shadow:
0 0 8px #00d4ff,
0 0 12px #00d4ff;
}
/* 动画定义 */
@keyframes glow-slide {
0% {
transform: translateX(-100%);
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
transform: translateX(100%);
opacity: 0;
}
}
@keyframes float-up {
0%, 100% {
transform: translateY(0);
opacity: 0.6;
}
50% {
transform: translateY(-15px);
opacity: 1;
}
}
@keyframes ring-expand {
0% {
transform: scale(0.6);
opacity: 0.8;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
/* ========== 响应式设计 ========== */
@media screen and (max-width: 1920px) {
.screen-header {
.screen-title {
font-size: 26px;
letter-spacing: 1.5px;
}
.title-subtitle {
font-size: 11px;
}
}
}
@media screen and (max-width: 1600px) {
.screen-header {
height: 100px;
padding: 0 30px;
.screen-title {
font-size: 24px;
letter-spacing: 1px;
}
.title-subtitle {
font-size: 11px;
letter-spacing: 0.5px;
}
}
.content-row {
&.row-1 { height: 340px; }
&.row-2 { height: 280px; }
&.row-3 { height: 310px; }
}
}
</style>