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