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