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