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

3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
2 months ago
2 months ago
3 months ago
2 months ago
2 months ago
2 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
2 months ago
2 months ago
3 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
2 months ago
2 months ago
3 months ago
2 months ago
2 months ago
2 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
2 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
2 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
2 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
  1. <template>
  2. <div class="warehouse-3d-screen">
  3. <!-- 装饰背景 -->
  4. <div class="bg-decoration">
  5. <div class="decoration-line line-1"></div>
  6. <div class="decoration-line line-2"></div>
  7. <div class="decoration-line line-3"></div>
  8. <div class="decoration-circle circle-1"></div>
  9. <div class="decoration-circle circle-2"></div>
  10. <div class="grid-bg"></div>
  11. </div>
  12. <!-- 顶部标题栏 -->
  13. <div class="screen-header">
  14. <!-- CCL Logo -->
  15. <div class="header-logo">
  16. <img src="~@/assets/img/cclbai.png" alt="CCL Logo" class="logo-img">
  17. </div>
  18. <div class="header-decoration left"></div>
  19. <div class="header-center">
  20. <div class="title-glow"></div>
  21. <h1 class="screen-title">中山智能立体仓库可视化监控中心</h1>
  22. <div class="title-subtitle">ZhongShan Intelligent 3D Warehouse Visualization & Monitoring Center</div>
  23. </div>
  24. <div class="header-decoration right"></div>
  25. <div class="header-time">
  26. <div class="time-icon"></div>
  27. <div class="time-text">{{ currentTime }}</div>
  28. </div>
  29. </div>
  30. <!-- 主内容区 -->
  31. <div class="screen-content">
  32. <!-- 第一行任务统计 + 库位利用率 + 设备状态 -->
  33. <div class="content-row row-1">
  34. <!-- 左侧任务统计 -->
  35. <div class="panel-card task-summary">
  36. <div class="card-header">
  37. <div class="header-icon"></div>
  38. <span class="header-title">任务统计总览</span>
  39. <span class="header-subtitle">Task Overview</span>
  40. </div>
  41. <div class="card-body">
  42. <div class="task-stats">
  43. <div class="stat-item total">
  44. <div class="stat-icon">
  45. <i class="el-icon-tickets"></i>
  46. </div>
  47. <div class="stat-content">
  48. <div class="stat-label">月度作业总数</div>
  49. <div class="stat-value">{{ taskData.monthlyTasks }}</div>
  50. </div>
  51. </div>
  52. <div class="stat-divider"></div>
  53. <div class="stat-item monthly">
  54. <div class="stat-icon">
  55. <i class="el-icon-date"></i>
  56. </div>
  57. <div class="stat-content">
  58. <div class="stat-label">当日作业总数</div>
  59. <div class="stat-value">{{ taskData.todayTasks }}</div>
  60. </div>
  61. </div>
  62. </div>
  63. <div class="task-breakdown">
  64. <div class="breakdown-item outbound">
  65. <div class="item-icon">📦</div>
  66. <div class="item-label">出库作业</div>
  67. <div class="item-value">{{ taskData.outboundTasks }}</div>
  68. <div class="item-percent">{{ taskData.outboundPercent }}%</div>
  69. </div>
  70. <div class="breakdown-item inbound">
  71. <div class="item-icon">📥</div>
  72. <div class="item-label">入库作业</div>
  73. <div class="item-value">{{ taskData.inboundTasks }}</div>
  74. <div class="item-percent">{{ taskData.inboundPercent }}%</div>
  75. </div>
  76. </div>
  77. <!-- 托盘数量统计 -->
  78. <div class="pallet-statistics">
  79. <!-- 空托盘总数突出显示 -->
  80. <div class="pallet-total-section">
  81. <div class="pallet-total-item">
  82. <div class="total-icon">📦</div>
  83. <div class="total-info">
  84. <div class="total-label">空托盘总数</div>
  85. <div class="total-value">{{ emptyPalletTotal }}</div>
  86. </div>
  87. </div>
  88. </div>
  89. <!-- 分类明细 -->
  90. <div class="pallet-detail-section">
  91. <div class="detail-items">
  92. <div class="pallet-item flat">
  93. <div class="pallet-icon">📋</div>
  94. <div class="pallet-info">
  95. <div class="pallet-label">平托盘</div>
  96. <div class="pallet-value">{{ (storageData.emptyContainerInventory && storageData.emptyContainerInventory.flatPallet) || 0 }}</div>
  97. </div>
  98. </div>
  99. <div class="pallet-item guard">
  100. <div class="pallet-icon">🔲</div>
  101. <div class="pallet-info">
  102. <div class="pallet-label">围框托盘</div>
  103. <div class="pallet-value">{{ (storageData.emptyContainerInventory && storageData.emptyContainerInventory.framePallet) || 0 }}</div>
  104. </div>
  105. </div>
  106. <div class="pallet-item steel">
  107. <div class="pallet-icon"></div>
  108. <div class="pallet-info">
  109. <div class="pallet-label">钢托盘</div>
  110. <div class="pallet-value">{{ (storageData.emptyContainerInventory && storageData.emptyContainerInventory.steelPallet) || 0 }}</div>
  111. </div>
  112. </div>
  113. </div>
  114. </div>
  115. </div>
  116. </div>
  117. </div>
  118. <!-- 中间库位利用率 -->
  119. <div class="panel-card storage-utilization">
  120. <div class="card-header">
  121. <div class="header-icon"></div>
  122. <span class="header-title">库位利用率分析</span>
  123. <span class="header-subtitle">Storage Utilization</span>
  124. </div>
  125. <div class="card-body">
  126. <div class="utilization-summary">
  127. <div class="summary-item">
  128. <span class="summary-label">总库位数</span>
  129. <span class="summary-value">{{ storageData.totalSlots }}</span>
  130. </div>
  131. <div class="summary-item">
  132. <span class="summary-label">已使用</span>
  133. <span class="summary-value rate">{{ storageData.usedSlots }}</span>
  134. </div>
  135. <div class="summary-item">
  136. <span class="summary-label">利用率(包含空托盘)</span>
  137. <span class="summary-value rate">{{ storageData.utilizationRate }}%</span>
  138. </div>
  139. </div>
  140. <div id="storageChart" style="margin-left: 70px" class="chart-container"></div>
  141. </div>
  142. </div>
  143. <!-- 右侧设备工作状态 -->
  144. <div class="panel-card device-status">
  145. <div class="card-header">
  146. <div class="header-icon"></div>
  147. <span class="header-title">设备运行状态</span>
  148. <span class="header-subtitle">Device Status</span>
  149. </div>
  150. <div class="card-body">
  151. <!-- 拣选机器人 -->
  152. <div class="device-group">
  153. <div class="group-title">🤖 拣选机器人</div>
  154. <div class="device-list">
  155. <div
  156. v-for="robot in robotData"
  157. :key="robot.id"
  158. :class="['device-item', robot.status]"
  159. >
  160. <div class="device-header">
  161. <div class="device-name">{{ robot.name }}</div>
  162. <div class="device-status-badge">
  163. <span class="status-dot"></span>
  164. <span class="status-text">{{ robot.statusText }}</span>
  165. </div>
  166. </div>
  167. <div class="device-metrics">
  168. <span class="metric"></span>
  169. <span class="metric">任务: {{ robot.tasks }}</span>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. <!-- AGV -->
  175. <div class="device-group">
  176. <div class="group-title">🚗 AGV搬运车</div>
  177. <div class="device-list agv-grid">
  178. <!-- AGV 列表 -->
  179. <div
  180. v-for="agv in agvData"
  181. :key="agv.id"
  182. :class="['device-item-compact', agv.status]"
  183. >
  184. <div class="compact-header">
  185. <span class="compact-name">{{ agv.name }}</span>
  186. <span class="compact-status">
  187. <span class="status-dot"></span>
  188. {{ agv.statusText }}
  189. </span>
  190. </div>
  191. <div class="compact-info">
  192. <span>电量: {{ agv.battery }}%</span>
  193. <span>任务: {{ agv.tasks }}</span>
  194. </div>
  195. </div>
  196. <!-- 空状态提示 -->
  197. <div v-if="agvData.length === 0" class="empty-state">
  198. <i class="el-icon-warning"></i>
  199. <span>暂无AGV数据</span>
  200. </div>
  201. </div>
  202. </div>
  203. </div>
  204. </div>
  205. </div>
  206. <!-- 第二行库存趋势 + 呆滞情况 -->
  207. <div class="content-row row-2" style="margin-top: 10px">
  208. <!-- 成品库存量趋势 -->
  209. <div class="panel-card inventory-trend">
  210. <div class="card-header">
  211. <div class="header-icon"></div>
  212. <span class="header-title">原材料库存M²</span>
  213. <span class="header-subtitle">Raw Material Inventory</span>
  214. </div>
  215. <div class="card-body">
  216. <div id="finishedGoodsTrendChart" class="chart-container"></div>
  217. </div>
  218. </div>
  219. <!-- 原材库存量趋势 -->
  220. <div class="panel-card inventory-trend">
  221. <div class="card-header">
  222. <div class="header-icon"></div>
  223. <span class="header-title">规格料库存M</span>
  224. <span class="header-subtitle">Specified Materials Inventory</span>
  225. </div>
  226. <div class="card-body">
  227. <div id="rawMaterialTrendChart" class="chart-container"></div>
  228. </div>
  229. </div>
  230. <!-- 产成品库存趋势 -->
  231. <div class="panel-card stagnant-analysis">
  232. <div class="card-header">
  233. <div class="header-icon"></div>
  234. <span class="header-title">产成品库存PCS</span>
  235. <span class="header-subtitle">Finished Goods Inventory</span>
  236. </div>
  237. <div class="card-body">
  238. <div id="stagnantChart" class="chart-container"></div>
  239. </div>
  240. </div>
  241. </div>
  242. </div>
  243. <!-- 底部装饰效果 -->
  244. <div class="bottom-decoration-bar">
  245. <!-- 浮动数据点 -->
  246. <div class="floating-data-points">
  247. <div class="data-point point-1">
  248. <div class="point-ring"></div>
  249. <div class="point-core"></div>
  250. </div>
  251. <div class="data-point point-2">
  252. <div class="point-ring"></div>
  253. <div class="point-core"></div>
  254. </div>
  255. <div class="data-point point-3">
  256. <div class="point-ring"></div>
  257. <div class="point-core"></div>
  258. </div>
  259. <div class="data-point point-4">
  260. <div class="point-ring"></div>
  261. <div class="point-core"></div>
  262. </div>
  263. <div class="data-point point-5">
  264. <div class="point-ring"></div>
  265. <div class="point-core"></div>
  266. </div>
  267. </div>
  268. </div>
  269. </div>
  270. </template>
  271. <script>
  272. import WebSocketClient from '@/utils/websocket'
  273. export default {
  274. name: 'Warehouse3DBoard',
  275. data() {
  276. return {
  277. currentTime: '',
  278. timeInterval: null, // 时间更新定时器
  279. serverTimeOffset: 0, // 服务器时间偏移量(毫秒)
  280. refreshCheckInterval: null, // 定时刷新检查定时器
  281. // WebSocket相关
  282. useWebSocket: true, // 是否使用WebSocket(可切换为false降级到本地数据)
  283. wsConnected: false, // WebSocket连接状态
  284. wsSubscription: null, // WebSocket订阅ID
  285. // 任务统计数据
  286. taskData: {
  287. totalTasks: 0,
  288. monthlyTasks: 0,
  289. todayTasks: 0,
  290. outboundTasks: 0,
  291. inboundTasks: 0,
  292. outboundPercent: 0,
  293. inboundPercent: 0
  294. },
  295. // 库位数据
  296. storageData: {
  297. totalSlots: 1960,// 固定不变的
  298. usedSlots: 0,
  299. utilizationRate: 0,
  300. // 物料盘库存(按托盘类型分类)
  301. materialInventory: {
  302. steelPallet: 0,
  303. framePallet: 0,
  304. flatPallet: 0
  305. },
  306. // 空盘库存(按托盘类型分类)
  307. emptyContainerInventory: {
  308. flatPallet: 0,
  309. framePallet: 0,
  310. steelPallet: 0
  311. }
  312. },
  313. // 机器人数据
  314. robotData: [
  315. { id: 1, name: '机械臂#1', status: 'working', statusText: '工作中', efficiency: 95, tasks: 1 },
  316. { id: 2, name: '机械臂#2', status: 'working', statusText: '工作中', efficiency: 92, tasks: 1 }
  317. ],
  318. // AGV数据(从TUSK系统实时获取)
  319. agvData: [],
  320. // 领料申请单数据
  321. materialRequestData: {
  322. total: 45,
  323. completed: 32,
  324. processing: 8,
  325. pending: 5,
  326. completionRate: 71
  327. },
  328. // 发货数据
  329. shipmentData: {
  330. total: 38,
  331. completed: 28,
  332. processing: 6,
  333. pending: 4,
  334. completionRate: 74
  335. },
  336. // 库存趋势数据
  337. rawMaterialTrend: [], // 原材料库存趋势
  338. specifiedMaterialTrend: [], // 规格料库存趋势
  339. finishedGoodsTrend: [], // 产成品库存趋势
  340. // 图表实例
  341. charts: {}
  342. }
  343. },
  344. computed: {
  345. /**
  346. * 计算空托盘总数
  347. */
  348. emptyPalletTotal() {
  349. const empty = this.storageData.emptyContainerInventory || {}
  350. return (empty.flatPallet || 0) + (empty.framePallet || 0) + (empty.steelPallet || 0)
  351. },
  352. /**
  353. * 计算可用库位数总库位 - 已使用库位
  354. */
  355. availableSlots() {
  356. return this.storageData.totalSlots - this.storageData.usedSlots
  357. }
  358. },
  359. mounted() {
  360. // 初始化时间显示
  361. this.currentTime = '等待服务器时间同步...'
  362. // 启动时钟定时器(每秒更新)
  363. this.timeInterval = setInterval(() => {
  364. this.updateTime()
  365. }, 1000)
  366. // 启动定时刷新检查(每分钟检查一次)
  367. this.startRefreshCheck()
  368. // 延迟初始化图表,确保DOM已渲染
  369. this.$nextTick(() => {
  370. setTimeout(() => {
  371. this.initCharts()
  372. // 图表初始化后再次调用resize确保尺寸正确
  373. setTimeout(() => {
  374. this.handleResize()
  375. }, 300)
  376. }, 100)
  377. })
  378. // 监听窗口大小变化
  379. window.addEventListener('resize', this.handleResize)
  380. // 根据配置选择使用WebSocket或本地数据
  381. if (this.useWebSocket) {
  382. this.initWebSocket()
  383. }
  384. },
  385. beforeDestroy() {
  386. // 清理时间更新定时器
  387. if (this.timeInterval) {
  388. clearInterval(this.timeInterval)
  389. }
  390. // 清理定时刷新检查定时器
  391. if (this.refreshCheckInterval) {
  392. clearInterval(this.refreshCheckInterval)
  393. }
  394. // 移除窗口resize监听
  395. window.removeEventListener('resize', this.handleResize)
  396. // 销毁所有图表
  397. Object.values(this.charts).forEach(chart => {
  398. if (chart) chart.dispose()
  399. })
  400. // 断开WebSocket连接
  401. this.disconnectWebSocket()
  402. },
  403. methods: {
  404. /**
  405. * 启动定时刷新检查
  406. * 每分钟检查一次如果到达凌晨5点则自动刷新页面
  407. */
  408. startRefreshCheck() {
  409. console.log('[智能立体仓库看板] 已启动定时刷新检查,将在每天凌晨5:00自动刷新页面')
  410. // 每分钟检查一次
  411. this.refreshCheckInterval = setInterval(() => {
  412. this.checkAndRefreshPage()
  413. }, 60000) // 60秒 = 1分钟
  414. // 立即执行一次检查
  415. this.checkAndRefreshPage()
  416. },
  417. /**
  418. * 检查当前时间如果是凌晨5点则刷新页面
  419. */
  420. checkAndRefreshPage() {
  421. const now = new Date()
  422. const hours = now.getHours()
  423. const minutes = now.getMinutes()
  424. // 判断是否为凌晨5:00-5:10之间
  425. if (hours === 5 && minutes === 10) {
  426. location.reload()
  427. }
  428. },
  429. /**
  430. * 处理窗口大小变化
  431. */
  432. handleResize() {
  433. // 遍历所有图表实例,调用resize方法
  434. Object.values(this.charts).forEach(chart => {
  435. if (chart && chart.resize) {
  436. chart.resize()
  437. }
  438. })
  439. },
  440. /**
  441. * 更新服务器时间偏移量
  442. *
  443. * @param {string} serverTimeString - 服务器时间字符串
  444. */
  445. updateServerTimeOffset(serverTimeString) {
  446. try {
  447. // 解析服务器时间字符串 "2025-11-01 14:30:25 星期五"
  448. const timeStr = serverTimeString.split(' ')[0] + ' ' + serverTimeString.split(' ')[1]
  449. const serverTime = new Date(timeStr).getTime()
  450. const localTime = new Date().getTime()
  451. if (!isNaN(serverTime)) {
  452. this.serverTimeOffset = serverTime - localTime
  453. console.log('服务器时间偏移量更新:', this.serverTimeOffset, 'ms')
  454. }
  455. } catch (error) {
  456. console.warn('解析服务器时间失败:', error)
  457. }
  458. },
  459. /**
  460. * 更新时间显示使用服务器时间偏移量
  461. */
  462. updateTime() {
  463. // 使用本地时间 + 偏移量来计算服务器时间
  464. const now = new Date(new Date().getTime() + this.serverTimeOffset)
  465. const year = now.getFullYear()
  466. const month = String(now.getMonth() + 1).padStart(2, '0')
  467. const date = String(now.getDate()).padStart(2, '0')
  468. const hours = String(now.getHours()).padStart(2, '0')
  469. const minutes = String(now.getMinutes()).padStart(2, '0')
  470. const seconds = String(now.getSeconds()).padStart(2, '0')
  471. const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  472. const weekDay = weekDays[now.getDay()]
  473. this.currentTime = `${year}-${month}-${date} ${hours}:${minutes}:${seconds} ${weekDay}`
  474. },
  475. /**
  476. * 初始化WebSocket连接
  477. */
  478. initWebSocket() {
  479. var apiServer = (process.env.NODE_ENV !== 'production' && process.env.OPEN_PROXY ? '/proxyApi/' : window.SITE_CONFIG.baseUrl);
  480. // 连接WebSocket服务器
  481. const wsUrl = apiServer + '/ws/dashboard'
  482. WebSocketClient.connect(
  483. wsUrl,
  484. () => {
  485. this.wsConnected = true
  486. console.log('[智能立体仓库看板] WebSocket连接成功')
  487. // 订阅智能立体仓库看板主题
  488. this.wsSubscription = WebSocketClient.subscribe(
  489. '/topic/dashboard/warehouse-3d',
  490. this.handleWebSocketMessage
  491. )
  492. console.log('[智能立体仓库看板] 已订阅主题: /topic/dashboard/warehouse-3d')
  493. },
  494. (error) => {
  495. // 连接失败回调
  496. console.error('[智能立体仓库看板] WebSocket连接失败,将继续使用本地数据', error)
  497. this.wsConnected = false
  498. }
  499. )
  500. },
  501. /**
  502. * 处理WebSocket推送的消息
  503. *
  504. * <p><b>数据结构说明</b></p>
  505. * <ul>
  506. * <li>taskData - 任务统计数据</li>
  507. * <li>storageData - 库位利用率数据</li>
  508. * <li>robotData - 机器人状态数据</li>
  509. * <li>agvData - AGV状态数据</li>
  510. * <li>materialRequestData - 领料申请单统计</li>
  511. * <li>shipmentData - 发货统计</li>
  512. * </ul>
  513. *
  514. * @param {object} message WebSocket推送的消息
  515. */
  516. handleWebSocketMessage(message) {
  517. if (message && message.code === 0) {
  518. console.log('[智能立体仓库看板] 收到WebSocket推送数据:', message.data)
  519. // 更新服务器时间偏移量
  520. if (message.serverTime) {
  521. this.updateServerTimeOffset(message.serverTime)
  522. }
  523. // 更新任务统计数据
  524. if (message.data.taskData && Object.keys(message.data.taskData).length > 0) {
  525. this.taskData = Object.assign({}, this.taskData, message.data.taskData)
  526. }
  527. // 更新库位数据
  528. if (message.data.storageData && Object.keys(message.data.storageData).length > 0) {
  529. // 合并数据,保留嵌套结构
  530. this.storageData = {
  531. ...this.storageData,
  532. ...message.data.storageData,
  533. materialInventory: {
  534. ...this.storageData.materialInventory,
  535. ...(message.data.storageData.materialInventory || {})
  536. },
  537. emptyContainerInventory: {
  538. ...this.storageData.emptyContainerInventory,
  539. ...(message.data.storageData.emptyContainerInventory || {})
  540. }
  541. }
  542. console.log('[智能立体仓库看板] 库位数据已更新:', this.storageData)
  543. // 重新初始化库位利用率图表
  544. this.$nextTick(() => {
  545. this.initStorageChart()
  546. })
  547. }
  548. // 更新机器人数据
  549. if (message.data.robotData !== undefined) {
  550. //this.robotData = message.data.robotData
  551. console.log('[智能立体仓库看板] 机器人数据已更新:', this.robotData.length, '条')
  552. }
  553. // 更新AGV数据(使用真实数据,包括空数组)
  554. if (message.data.agvData !== undefined) {
  555. this.agvData = message.data.agvData
  556. }
  557. // 更新领料申请单数据
  558. if (message.data.materialRequestData && Object.keys(message.data.materialRequestData).length > 0) {
  559. this.materialRequestData = Object.assign({}, this.materialRequestData, message.data.materialRequestData)
  560. // 重新初始化领料申请单图表
  561. this.$nextTick(() => {
  562. this.initMaterialRequestChart()
  563. })
  564. }
  565. // 更新发货数据
  566. if (message.data.shipmentData && Object.keys(message.data.shipmentData).length > 0) {
  567. this.shipmentData = Object.assign({}, this.shipmentData, message.data.shipmentData)
  568. // 重新初始化发货图表
  569. this.$nextTick(() => {
  570. this.initShipmentChart()
  571. })
  572. }
  573. // 更新库存趋势数据
  574. if (message.data.rawMaterialTrend && message.data.rawMaterialTrend.length > 0) {
  575. this.rawMaterialTrend = message.data.rawMaterialTrend
  576. // 重新初始化原材料趋势图表
  577. this.$nextTick(() => {
  578. this.initFinishedGoodsTrendChart()
  579. })
  580. }
  581. if (message.data.specifiedMaterialTrend && message.data.specifiedMaterialTrend.length > 0) {
  582. this.specifiedMaterialTrend = message.data.specifiedMaterialTrend
  583. // 重新初始化规格料趋势图表
  584. this.$nextTick(() => {
  585. this.initRawMaterialTrendChart()
  586. })
  587. }
  588. if (message.data.finishedGoodsTrend && message.data.finishedGoodsTrend.length > 0) {
  589. this.finishedGoodsTrend = message.data.finishedGoodsTrend
  590. // 重新初始化产成品趋势图表
  591. this.$nextTick(() => {
  592. this.initStagnantChart()
  593. })
  594. }
  595. }
  596. },
  597. /**
  598. * 断开WebSocket连接
  599. */
  600. disconnectWebSocket() {
  601. if (this.wsSubscription) {
  602. WebSocketClient.unsubscribe(this.wsSubscription)
  603. this.wsSubscription = null
  604. console.log('[智能立体仓库看板] 已取消订阅')
  605. }
  606. if (this.wsConnected) {
  607. WebSocketClient.disconnect()
  608. this.wsConnected = false
  609. console.log('[智能立体仓库看板] WebSocket已断开')
  610. }
  611. },
  612. /**
  613. * 初始化所有图表
  614. */
  615. initCharts() {
  616. // 加载 ECharts
  617. if (typeof echarts === 'undefined') {
  618. const script = document.createElement('script')
  619. script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js'
  620. script.onload = () => {
  621. this.renderAllCharts()
  622. }
  623. document.head.appendChild(script)
  624. } else {
  625. this.renderAllCharts()
  626. }
  627. },
  628. /**
  629. * 渲染所有图表
  630. */
  631. renderAllCharts() {
  632. this.initStorageChart()
  633. this.initFinishedGoodsTrendChart()
  634. this.initRawMaterialTrendChart()
  635. this.initStagnantChart()
  636. this.initMaterialRequestChart()
  637. this.initShipmentChart()
  638. },
  639. /**
  640. * 初始化库位利用率饼图
  641. */
  642. initStorageChart() {
  643. const chartDom = document.getElementById('storageChart')
  644. if (!chartDom) return
  645. this.charts.storage = echarts.init(chartDom)
  646. // 从新的嵌套结构中提取数据
  647. const materialInventory = this.storageData.materialInventory || {}
  648. const materialSteelPallet = materialInventory.steelPallet || 0
  649. const materialFramePallet = materialInventory.framePallet || 0
  650. const materialFlatPallet = materialInventory.flatPallet || 0
  651. // 计算可用库位数
  652. const availableSlots = this.availableSlots
  653. // 调试:打印数据
  654. console.log('初始化饼图 - 物料盘数据:', { materialSteelPallet, materialFramePallet, materialFlatPallet })
  655. console.log('初始化饼图 - 可用库位:', availableSlots)
  656. console.log('原始storageData:', this.storageData)
  657. const option = {
  658. color: ['#00d4ff', '#7b68ee', '#00ff88','#c5d9ed'],
  659. tooltip: {
  660. trigger: 'item',
  661. backgroundColor: 'rgba(20, 40, 70, 0.95)',
  662. borderColor: '#00d4ff',
  663. borderWidth: 1,
  664. textStyle: {
  665. color: '#fff'
  666. }
  667. },
  668. legend: {
  669. orient: 'vertical',
  670. right: '5%',
  671. top: 'center',
  672. textStyle: {
  673. color: '#fff',
  674. fontSize: 12
  675. },
  676. itemWidth: 12,
  677. itemHeight: 12
  678. },
  679. series: [
  680. {
  681. name: '库位类型',
  682. type: 'pie',
  683. radius: ['30%', '70%'],
  684. center: ['50%', '50%'],
  685. avoidLabelOverlap: false,
  686. itemStyle: {
  687. borderRadius: 8,
  688. borderColor: '#143050',
  689. borderWidth: 2
  690. },
  691. label: {
  692. show: true,
  693. position: 'inside',
  694. formatter: function(params) {
  695. const percent = params.percent ? params.percent.toFixed(1) : '0.0'
  696. return percent + '%'
  697. },
  698. color: '#fff',
  699. fontSize: 12,
  700. fontWeight: 'bold'
  701. },
  702. emphasis: {
  703. label: {
  704. show: true,
  705. formatter: function(params) {
  706. const percent = params.percent ? params.percent.toFixed(1) : '0.0'
  707. return percent + '%'
  708. },
  709. fontSize: 14,
  710. fontWeight: 'bold'
  711. },
  712. itemStyle: {
  713. shadowBlur: 10,
  714. shadowOffsetX: 0,
  715. shadowColor: 'rgba(0, 212, 255, 0.5)'
  716. }
  717. },
  718. data: [
  719. {
  720. value: materialSteelPallet,
  721. name: '钢托盘(' + materialSteelPallet + ')',
  722. itemStyle: {
  723. color: {
  724. type: 'linear',
  725. x: 0, y: 0, x2: 1, y2: 1,
  726. colorStops: [
  727. { offset: 0, color: '#00d4ff' },
  728. { offset: 1, color: '#0084ff' }
  729. ]
  730. }
  731. }
  732. },
  733. {
  734. value: materialFramePallet,
  735. name: '围框托盘(' + materialFramePallet + ')',
  736. itemStyle: {
  737. color: {
  738. type: 'linear',
  739. x: 0, y: 0, x2: 1, y2: 1,
  740. colorStops: [
  741. { offset: 0, color: '#7b68ee' },
  742. { offset: 1, color: '#9370db' }
  743. ]
  744. }
  745. }
  746. },
  747. {
  748. value: materialFlatPallet,
  749. name: '平托盘(' + materialFlatPallet + ')',
  750. itemStyle: {
  751. color: {
  752. type: 'linear',
  753. x: 0, y: 0, x2: 1, y2: 1,
  754. colorStops: [
  755. { offset: 0, color: '#00ff88' },
  756. { offset: 1, color: '#00cc70' }
  757. ]
  758. }
  759. }
  760. },
  761. {
  762. value: availableSlots,
  763. name: '可用库位(' + availableSlots + ')',
  764. itemStyle: {
  765. color: {
  766. type: 'linear',
  767. x: 0, y: 0, x2: 1, y2: 1,
  768. colorStops: [
  769. { offset: 0, color: '#c5d9ed' },
  770. { offset: 1, color: '#f4edb9' }
  771. ]
  772. }
  773. }
  774. }
  775. ]
  776. }
  777. ]
  778. }
  779. this.charts.storage.setOption(option)
  780. },
  781. /**
  782. * 初始化原材料库存趋势图使用真实数据
  783. */
  784. initFinishedGoodsTrendChart() {
  785. const chartDom = document.getElementById('finishedGoodsTrendChart')
  786. if (!chartDom) return
  787. this.charts.finishedGoods = echarts.init(chartDom)
  788. // 使用真实数据或生成空数据
  789. const days = []
  790. const values = []
  791. if (this.rawMaterialTrend && this.rawMaterialTrend.length > 0) {
  792. // 使用从后端获取的真实数据
  793. this.rawMaterialTrend.forEach(item => {
  794. days.push(`${item.dayNum}`)
  795. values.push(item.quantity || 0)
  796. })
  797. } else {
  798. // 如果没有数据,生成当月空数据占位
  799. const today = new Date()
  800. const currentMonth = today.getMonth()
  801. const daysInMonth = new Date(today.getFullYear(), currentMonth + 1, 0).getDate()
  802. for (let i = 1; i <= daysInMonth; i++) {
  803. days.push(`${i}`)
  804. values.push(0)
  805. }
  806. }
  807. const option = {
  808. color: ['#00d4ff'],
  809. tooltip: {
  810. trigger: 'axis',
  811. backgroundColor: 'rgba(20, 40, 70, 0.95)',
  812. borderColor: '#00d4ff',
  813. borderWidth: 1,
  814. textStyle: {
  815. color: '#fff'
  816. },
  817. axisPointer: {
  818. type: 'cross',
  819. label: {
  820. backgroundColor: '#00d4ff'
  821. }
  822. }
  823. },
  824. grid: {
  825. left: '3%',
  826. right: '4%',
  827. bottom: '3%',
  828. top: '10%',
  829. containLabel: true
  830. },
  831. xAxis: {
  832. type: 'category',
  833. boundaryGap: false,
  834. data: days,
  835. axisLine: {
  836. lineStyle: {
  837. color: '#3a7fb0'
  838. }
  839. },
  840. axisLabel: {
  841. color: '#8ab8d6',
  842. fontSize: 10
  843. }
  844. },
  845. yAxis: {
  846. type: 'value',
  847. splitLine: {
  848. lineStyle: {
  849. color: '#3a7fb0',
  850. type: 'dashed'
  851. }
  852. },
  853. axisLine: {
  854. lineStyle: {
  855. color: '#3a7fb0'
  856. }
  857. },
  858. axisLabel: {
  859. color: '#8ab8d6',
  860. fontSize: 10
  861. }
  862. },
  863. series: [
  864. {
  865. name: '库存数量',
  866. type: 'line',
  867. smooth: true,
  868. symbol: 'circle',
  869. symbolSize: 6,
  870. lineStyle: {
  871. color: '#00d4ff',
  872. width: 2
  873. },
  874. itemStyle: {
  875. color: '#00d4ff',
  876. borderColor: '#fff',
  877. borderWidth: 2
  878. },
  879. areaStyle: {
  880. color: {
  881. type: 'linear',
  882. x: 0, y: 0, x2: 0, y2: 1,
  883. colorStops: [
  884. { offset: 0, color: 'rgba(0, 212, 255, 0.3)' },
  885. { offset: 1, color: 'rgba(0, 212, 255, 0.05)' }
  886. ]
  887. }
  888. },
  889. data: values
  890. }
  891. ]
  892. }
  893. this.charts.finishedGoods.setOption(option)
  894. },
  895. /**
  896. * 初始化规格料库存趋势图使用真实数据
  897. */
  898. initRawMaterialTrendChart() {
  899. const chartDom = document.getElementById('rawMaterialTrendChart')
  900. if (!chartDom) return
  901. this.charts.rawMaterial = echarts.init(chartDom)
  902. // 使用真实数据或生成空数据
  903. const days = []
  904. const values = []
  905. if (this.specifiedMaterialTrend && this.specifiedMaterialTrend.length > 0) {
  906. // 使用从后端获取的真实数据
  907. this.specifiedMaterialTrend.forEach(item => {
  908. days.push(`${item.dayNum}`)
  909. values.push(item.quantity || 0)
  910. })
  911. } else {
  912. // 如果没有数据,生成当月空数据占位
  913. const today = new Date()
  914. const currentMonth = today.getMonth()
  915. const daysInMonth = new Date(today.getFullYear(), currentMonth + 1, 0).getDate()
  916. for (let i = 1; i <= daysInMonth; i++) {
  917. days.push(`${i}`)
  918. values.push(0)
  919. }
  920. }
  921. const option = {
  922. color: ['#00ff88'],
  923. tooltip: {
  924. trigger: 'axis',
  925. backgroundColor: 'rgba(20, 40, 70, 0.95)',
  926. borderColor: '#00ff88',
  927. borderWidth: 1,
  928. textStyle: {
  929. color: '#fff'
  930. },
  931. axisPointer: {
  932. type: 'cross',
  933. label: {
  934. backgroundColor: '#00ff88'
  935. }
  936. }
  937. },
  938. grid: {
  939. left: '3%',
  940. right: '4%',
  941. bottom: '3%',
  942. top: '10%',
  943. containLabel: true
  944. },
  945. xAxis: {
  946. type: 'category',
  947. boundaryGap: false,
  948. data: days,
  949. axisLine: {
  950. lineStyle: {
  951. color: '#3a7fb0'
  952. }
  953. },
  954. axisLabel: {
  955. color: '#8ab8d6',
  956. fontSize: 10
  957. }
  958. },
  959. yAxis: {
  960. type: 'value',
  961. splitLine: {
  962. lineStyle: {
  963. color: '#3a7fb0',
  964. type: 'dashed'
  965. }
  966. },
  967. axisLine: {
  968. lineStyle: {
  969. color: '#3a7fb0'
  970. }
  971. },
  972. axisLabel: {
  973. color: '#8ab8d6',
  974. fontSize: 10
  975. }
  976. },
  977. series: [
  978. {
  979. name: '库存数量',
  980. type: 'line',
  981. smooth: true,
  982. symbol: 'circle',
  983. symbolSize: 6,
  984. lineStyle: {
  985. color: '#00ff88',
  986. width: 2
  987. },
  988. itemStyle: {
  989. color: '#00ff88',
  990. borderColor: '#fff',
  991. borderWidth: 2
  992. },
  993. areaStyle: {
  994. color: {
  995. type: 'linear',
  996. x: 0, y: 0, x2: 0, y2: 1,
  997. colorStops: [
  998. { offset: 0, color: 'rgba(0, 255, 136, 0.3)' },
  999. { offset: 1, color: 'rgba(0, 255, 136, 0.05)' }
  1000. ]
  1001. }
  1002. },
  1003. data: values
  1004. }
  1005. ]
  1006. }
  1007. this.charts.rawMaterial.setOption(option)
  1008. },
  1009. /**
  1010. * 初始化产成品库存趋势图使用真实数据
  1011. */
  1012. initStagnantChart() {
  1013. const chartDom = document.getElementById('stagnantChart')
  1014. if (!chartDom) return
  1015. this.charts.stagnant = echarts.init(chartDom)
  1016. // 使用真实数据或生成空数据
  1017. const days = []
  1018. const values = []
  1019. if (this.finishedGoodsTrend && this.finishedGoodsTrend.length > 0) {
  1020. // 使用从后端获取的真实数据
  1021. this.finishedGoodsTrend.forEach(item => {
  1022. days.push(`${item.dayNum}`)
  1023. values.push(item.quantity || 0)
  1024. })
  1025. } else {
  1026. // 如果没有数据,生成当月空数据占位
  1027. const today = new Date()
  1028. const currentMonth = today.getMonth()
  1029. const daysInMonth = new Date(today.getFullYear(), currentMonth + 1, 0).getDate()
  1030. for (let i = 1; i <= daysInMonth; i++) {
  1031. days.push(`${i}`)
  1032. values.push(0)
  1033. }
  1034. }
  1035. const option = {
  1036. color: ['#9370db'],
  1037. tooltip: {
  1038. trigger: 'axis',
  1039. backgroundColor: 'rgba(20, 40, 70, 0.95)',
  1040. borderColor: '#9370db',
  1041. borderWidth: 1,
  1042. textStyle: {
  1043. color: '#fff'
  1044. },
  1045. axisPointer: {
  1046. type: 'cross',
  1047. label: {
  1048. backgroundColor: '#9370db'
  1049. }
  1050. }
  1051. },
  1052. grid: {
  1053. left: '3%',
  1054. right: '4%',
  1055. bottom: '3%',
  1056. top: '10%',
  1057. containLabel: true
  1058. },
  1059. xAxis: {
  1060. type: 'category',
  1061. boundaryGap: false,
  1062. data: days,
  1063. axisLine: {
  1064. lineStyle: {
  1065. color: '#3a7fb0'
  1066. }
  1067. },
  1068. axisLabel: {
  1069. color: '#8ab8d6',
  1070. fontSize: 10
  1071. }
  1072. },
  1073. yAxis: {
  1074. type: 'value',
  1075. splitLine: {
  1076. lineStyle: {
  1077. color: '#3a7fb0',
  1078. type: 'dashed'
  1079. }
  1080. },
  1081. axisLine: {
  1082. lineStyle: {
  1083. color: '#3a7fb0'
  1084. }
  1085. },
  1086. axisLabel: {
  1087. color: '#8ab8d6',
  1088. fontSize: 10
  1089. }
  1090. },
  1091. series: [
  1092. {
  1093. name: '库存数量',
  1094. type: 'line',
  1095. smooth: true,
  1096. symbol: 'circle',
  1097. symbolSize: 6,
  1098. lineStyle: {
  1099. color: '#9370db',
  1100. width: 2
  1101. },
  1102. itemStyle: {
  1103. color: '#9370db',
  1104. borderColor: '#fff',
  1105. borderWidth: 2
  1106. },
  1107. areaStyle: {
  1108. color: {
  1109. type: 'linear',
  1110. x: 0, y: 0, x2: 0, y2: 1,
  1111. colorStops: [
  1112. { offset: 0, color: 'rgba(147, 112, 219, 0.3)' },
  1113. { offset: 1, color: 'rgba(147, 112, 219, 0.05)' }
  1114. ]
  1115. }
  1116. },
  1117. data: values
  1118. }
  1119. ]
  1120. }
  1121. this.charts.stagnant.setOption(option)
  1122. },
  1123. /**
  1124. * 初始化领料申请单环形图
  1125. */
  1126. initMaterialRequestChart() {
  1127. const chartDom = document.getElementById('materialRequestChart')
  1128. if (!chartDom) return
  1129. this.charts.materialRequest = echarts.init(chartDom)
  1130. const option = {
  1131. color: ['#00ff88', '#00d4ff', '#ffaa00'],
  1132. tooltip: {
  1133. trigger: 'item',
  1134. backgroundColor: 'rgba(20, 40, 70, 0.95)',
  1135. borderColor: '#00d4ff',
  1136. borderWidth: 1,
  1137. textStyle: {
  1138. color: '#fff'
  1139. }
  1140. },
  1141. legend: {
  1142. orient: 'vertical',
  1143. left: '3%',
  1144. top: 'center',
  1145. textStyle: {
  1146. color: '#8ab8d6',
  1147. fontSize: 12
  1148. },
  1149. itemWidth: 12,
  1150. itemHeight: 12,
  1151. itemGap: 10
  1152. },
  1153. series: [
  1154. {
  1155. name: '申请单状态',
  1156. type: 'pie',
  1157. radius: ['36%', '52%'],
  1158. center: ['52%', '50%'],
  1159. avoidLabelOverlap: false,
  1160. itemStyle: {
  1161. borderRadius: 6,
  1162. borderColor: '#143050',
  1163. borderWidth: 2
  1164. },
  1165. label: {
  1166. show: false
  1167. },
  1168. emphasis: {
  1169. label: {
  1170. show: true,
  1171. fontSize: 14,
  1172. fontWeight: 'bold'
  1173. }
  1174. },
  1175. data: [
  1176. {
  1177. value: this.materialRequestData.completed,
  1178. name: '已完成',
  1179. itemStyle: { color: '#00ff88' }
  1180. },
  1181. {
  1182. value: this.materialRequestData.processing,
  1183. name: '进行中',
  1184. itemStyle: { color: '#00d4ff' }
  1185. },
  1186. {
  1187. value: this.materialRequestData.pending,
  1188. name: '待处理',
  1189. itemStyle: { color: '#ffaa00' }
  1190. }
  1191. ]
  1192. }
  1193. ]
  1194. }
  1195. this.charts.materialRequest.setOption(option)
  1196. },
  1197. /**
  1198. * 初始化发货环形图
  1199. */
  1200. initShipmentChart() {
  1201. const chartDom = document.getElementById('shipmentChart')
  1202. if (!chartDom) return
  1203. this.charts.shipment = echarts.init(chartDom)
  1204. const option = {
  1205. color: ['#00ff88', '#00d4ff', '#ffaa00'],
  1206. tooltip: {
  1207. trigger: 'item',
  1208. backgroundColor: 'rgba(20, 40, 70, 0.95)',
  1209. borderColor: '#00d4ff',
  1210. borderWidth: 1,
  1211. textStyle: {
  1212. color: '#fff'
  1213. }
  1214. },
  1215. legend: {
  1216. orient: 'vertical',
  1217. left: '3%',
  1218. top: 'center',
  1219. textStyle: {
  1220. color: '#8ab8d6',
  1221. fontSize: 12
  1222. },
  1223. itemWidth: 12,
  1224. itemHeight: 12,
  1225. itemGap: 10
  1226. },
  1227. series: [
  1228. {
  1229. name: '发货状态',
  1230. type: 'pie',
  1231. radius: ['36%', '52%'],
  1232. center: ['52%', '50%'],
  1233. avoidLabelOverlap: false,
  1234. itemStyle: {
  1235. borderRadius: 6,
  1236. borderColor: '#143050',
  1237. borderWidth: 2
  1238. },
  1239. label: {
  1240. show: false
  1241. },
  1242. emphasis: {
  1243. label: {
  1244. show: true,
  1245. fontSize: 14,
  1246. fontWeight: 'bold'
  1247. }
  1248. },
  1249. data: [
  1250. {
  1251. value: this.shipmentData.completed,
  1252. name: '已发货',
  1253. itemStyle: { color: '#00ff88' }
  1254. },
  1255. {
  1256. value: this.shipmentData.processing,
  1257. name: '拣选中',
  1258. itemStyle: { color: '#00d4ff' }
  1259. },
  1260. {
  1261. value: this.shipmentData.pending,
  1262. name: '待拣选',
  1263. itemStyle: { color: '#ffaa00' }
  1264. }
  1265. ]
  1266. }
  1267. ]
  1268. }
  1269. this.charts.shipment.setOption(option)
  1270. }
  1271. }
  1272. }
  1273. </script>
  1274. <style scoped lang="scss">
  1275. .warehouse-3d-screen {
  1276. width: 100vw;
  1277. height: 100vh;
  1278. background: linear-gradient(135deg, #1a2f4a 0%, #2a4060 50%, #1f3a56 100%);
  1279. overflow: hidden;
  1280. position: relative;
  1281. font-family: 'Microsoft YaHei', Arial, sans-serif;
  1282. }
  1283. /* ========== 装饰背景 ========== */
  1284. .bg-decoration {
  1285. position: absolute;
  1286. width: 100%;
  1287. height: 100%;
  1288. pointer-events: none;
  1289. z-index: 0;
  1290. .grid-bg {
  1291. position: absolute;
  1292. width: 100%;
  1293. height: 100%;
  1294. background-image:
  1295. linear-gradient(rgba(0, 212, 255, 0.03) 1px, transparent 1px),
  1296. linear-gradient(90deg, rgba(0, 212, 255, 0.03) 1px, transparent 1px);
  1297. background-size: 50px 50px;
  1298. }
  1299. .decoration-line {
  1300. position: absolute;
  1301. height: 2px;
  1302. background: linear-gradient(90deg, transparent, #00d4ff, transparent);
  1303. opacity: 0.3;
  1304. animation: lineMove 8s linear infinite;
  1305. &.line-1 {
  1306. width: 40%;
  1307. top: 20%;
  1308. left: -40%;
  1309. }
  1310. &.line-2 {
  1311. width: 30%;
  1312. top: 50%;
  1313. right: -30%;
  1314. animation-delay: 2s;
  1315. }
  1316. &.line-3 {
  1317. width: 50%;
  1318. top: 80%;
  1319. left: -50%;
  1320. animation-delay: 4s;
  1321. }
  1322. }
  1323. .decoration-circle {
  1324. position: absolute;
  1325. border: 2px solid rgba(0, 212, 255, 0.2);
  1326. border-radius: 50%;
  1327. animation: circleScale 6s ease-in-out infinite;
  1328. &.circle-1 {
  1329. width: 300px;
  1330. height: 300px;
  1331. top: 10%;
  1332. right: 5%;
  1333. }
  1334. &.circle-2 {
  1335. width: 200px;
  1336. height: 200px;
  1337. bottom: 15%;
  1338. left: 10%;
  1339. animation-delay: 3s;
  1340. }
  1341. }
  1342. }
  1343. @keyframes lineMove {
  1344. 0% { transform: translateX(0); }
  1345. 100% { transform: translateX(200%); }
  1346. }
  1347. @keyframes circleScale {
  1348. 0%, 100% { transform: scale(1); opacity: 0.2; }
  1349. 50% { transform: scale(1.2); opacity: 0.4; }
  1350. }
  1351. /* ========== 顶部标题栏 ========== */
  1352. .screen-header {
  1353. position: relative;
  1354. z-index: 10;
  1355. height: 80px;
  1356. display: flex;
  1357. align-items: center;
  1358. justify-content: space-between;
  1359. padding: 0 40px;
  1360. background: linear-gradient(180deg, rgba(20, 50, 80, 0.9) 0%, rgba(15, 40, 65, 0.8) 100%);
  1361. border-bottom: 2px solid rgba(0, 212, 255, 0.4);
  1362. box-shadow: 0 2px 20px rgba(0, 212, 255, 0.3);
  1363. .header-logo {
  1364. flex-shrink: 0;
  1365. width: 250px;
  1366. height: 50px;
  1367. display: flex;
  1368. align-items: center;
  1369. .logo-img {
  1370. max-width: 100%;
  1371. max-height: 100%;
  1372. object-fit: contain;
  1373. filter: drop-shadow(0 0 10px rgba(0, 212, 255, 0.5));
  1374. }
  1375. }
  1376. .header-decoration {
  1377. width: 100px;
  1378. height: 2px;
  1379. background: linear-gradient(90deg, transparent, #00d4ff);
  1380. &.right {
  1381. background: linear-gradient(90deg, #00d4ff, transparent);
  1382. }
  1383. }
  1384. .header-center {
  1385. flex: 1;
  1386. text-align: center;
  1387. position: relative;
  1388. .title-glow {
  1389. position: absolute;
  1390. top: 50%;
  1391. left: 50%;
  1392. transform: translate(-50%, -50%);
  1393. width: 500px;
  1394. height: 60px;
  1395. background: radial-gradient(ellipse, rgba(0, 212, 255, 0.2), transparent);
  1396. filter: blur(20px);
  1397. }
  1398. .screen-title {
  1399. font-size: 28px;
  1400. font-weight: bold;
  1401. color: #fff;
  1402. margin: 0;
  1403. text-shadow:
  1404. 0 0 10px rgba(0, 212, 255, 0.8),
  1405. 0 0 20px rgba(0, 212, 255, 0.5),
  1406. 0 0 30px rgba(0, 212, 255, 0.3);
  1407. letter-spacing: 2px;
  1408. position: relative;
  1409. }
  1410. .title-subtitle {
  1411. font-size: 12px;
  1412. color: #8ab8d6;
  1413. margin-top: 4px;
  1414. letter-spacing: 1px;
  1415. text-transform: uppercase;
  1416. }
  1417. }
  1418. .header-time {
  1419. flex-shrink: 0;
  1420. display: flex;
  1421. align-items: center;
  1422. padding: 8px 16px;
  1423. background: rgba(0, 212, 255, 0.1);
  1424. border: 1px solid rgba(0, 212, 255, 0.3);
  1425. border-radius: 6px;
  1426. .time-icon {
  1427. width: 6px;
  1428. height: 6px;
  1429. background: #00ff88;
  1430. border-radius: 50%;
  1431. margin-right: 10px;
  1432. animation: blink 2s ease-in-out infinite;
  1433. }
  1434. .time-text {
  1435. font-size: 14px;
  1436. color: #8ab8d6;
  1437. font-weight: 500;
  1438. letter-spacing: 1px;
  1439. }
  1440. }
  1441. }
  1442. @keyframes blink {
  1443. 0%, 100% { opacity: 1; box-shadow: 0 0 5px #00ff88; }
  1444. 50% { opacity: 0.3; box-shadow: none; }
  1445. }
  1446. /* ========== 主内容区 ========== */
  1447. .screen-content {
  1448. position: relative;
  1449. z-index: 1;
  1450. padding: 15px;
  1451. height: calc(100vh - 140px); /* 减去顶部80px和底部装饰效果40px */
  1452. overflow-y: auto;
  1453. display: flex;
  1454. flex-direction: column;
  1455. gap: 12px;
  1456. margin-top: 16px;
  1457. &::-webkit-scrollbar {
  1458. width: 6px;
  1459. }
  1460. &::-webkit-scrollbar-track {
  1461. background: rgba(30, 60, 100, 0.5);
  1462. }
  1463. &::-webkit-scrollbar-thumb {
  1464. background: rgba(0, 212, 255, 0.5);
  1465. border-radius: 3px;
  1466. &:hover {
  1467. background: rgba(0, 212, 255, 0.7);
  1468. }
  1469. }
  1470. }
  1471. .content-row {
  1472. display: flex;
  1473. gap: 12px;
  1474. &.row-1 {
  1475. height: 400px;
  1476. }
  1477. &.row-2 {
  1478. height: 300px;
  1479. }
  1480. &.row-3 {
  1481. height: 330px;
  1482. }
  1483. }
  1484. /* ========== 卡片面板通用样式 ========== */
  1485. .panel-card {
  1486. flex: 1;
  1487. background: rgba(20, 50, 80, 0.6);
  1488. border: 1px solid rgba(0, 212, 255, 0.4);
  1489. border-radius: 12px;
  1490. backdrop-filter: blur(10px);
  1491. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
  1492. display: flex;
  1493. flex-direction: column;
  1494. overflow: hidden;
  1495. transition: all 0.3s ease;
  1496. &:hover {
  1497. border-color: rgba(0, 212, 255, 0.6);
  1498. box-shadow: 0 6px 30px rgba(0, 212, 255, 0.3);
  1499. transform: translateY(-2px);
  1500. }
  1501. .card-header {
  1502. padding: 10px 14px;
  1503. background: linear-gradient(90deg, rgba(0, 212, 255, 0.25) 0%, rgba(0, 212, 255, 0.1) 100%);
  1504. border-bottom: 1px solid rgba(0, 212, 255, 0.4);
  1505. display: flex;
  1506. align-items: center;
  1507. gap: 10px;
  1508. .header-icon {
  1509. width: 4px;
  1510. height: 20px;
  1511. background: linear-gradient(180deg, #00d4ff, #0084ff);
  1512. border-radius: 2px;
  1513. box-shadow: 0 0 10px rgba(0, 212, 255, 0.6);
  1514. }
  1515. .header-title {
  1516. font-size: 16px;
  1517. font-weight: bold;
  1518. color: #fff;
  1519. letter-spacing: 1px;
  1520. }
  1521. .header-subtitle {
  1522. font-size: 11px;
  1523. color: #8ab8d6;
  1524. margin-left: auto;
  1525. text-transform: uppercase;
  1526. letter-spacing: 1px;
  1527. }
  1528. }
  1529. .card-body {
  1530. flex: 1;
  1531. padding: 12px;
  1532. overflow: hidden;
  1533. }
  1534. }
  1535. /* ========== 任务统计卡片 ========== */
  1536. .task-summary {
  1537. .task-stats {
  1538. display: flex;
  1539. gap: 12px;
  1540. margin-bottom: 12px;
  1541. .stat-item {
  1542. flex: 1;
  1543. display: flex;
  1544. align-items: center;
  1545. gap: 10px;
  1546. padding: 10px 12px;
  1547. background: linear-gradient(135deg, rgba(0, 212, 255, 0.1) 0%, rgba(0, 132, 255, 0.1) 100%);
  1548. border: 1px solid rgba(0, 212, 255, 0.3);
  1549. border-radius: 8px;
  1550. .stat-icon {
  1551. width: 40px;
  1552. height: 40px;
  1553. display: flex;
  1554. align-items: center;
  1555. justify-content: center;
  1556. background: linear-gradient(135deg, #00d4ff, #0084ff);
  1557. border-radius: 8px;
  1558. font-size: 20px;
  1559. color: #fff;
  1560. box-shadow: 0 4px 15px rgba(0, 212, 255, 0.4);
  1561. }
  1562. .stat-content {
  1563. flex: 1;
  1564. .stat-label {
  1565. font-size: 11px;
  1566. color: #8ab8d6;
  1567. margin-bottom: 4px;
  1568. }
  1569. .stat-value {
  1570. font-size: 22px;
  1571. font-weight: bold;
  1572. color: #00d4ff;
  1573. line-height: 1;
  1574. text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
  1575. }
  1576. .stat-unit {
  1577. font-size: 11px;
  1578. color: #8ab8d6;
  1579. margin-top: 3px;
  1580. }
  1581. }
  1582. }
  1583. .stat-divider {
  1584. width: 2px;
  1585. background: linear-gradient(180deg, transparent, rgba(0, 212, 255, 0.5), transparent);
  1586. }
  1587. }
  1588. .task-breakdown {
  1589. display: flex;
  1590. gap: 10px;
  1591. margin-bottom: 10px;
  1592. .breakdown-item {
  1593. flex: 1;
  1594. padding: 10px 12px;
  1595. background: rgba(25, 50, 85, 0.7);
  1596. border: 1px solid rgba(0, 212, 255, 0.2);
  1597. border-radius: 6px;
  1598. display: flex;
  1599. flex-direction: column;
  1600. align-items: center;
  1601. gap: 4px;
  1602. .item-icon {
  1603. font-size: 24px;
  1604. margin-bottom: 2px;
  1605. }
  1606. .item-label {
  1607. font-size: 11px;
  1608. color: #8ab8d6;
  1609. }
  1610. .item-value {
  1611. font-size: 20px;
  1612. font-weight: bold;
  1613. color: #fff;
  1614. }
  1615. .item-percent {
  1616. font-size: 11px;
  1617. color: #00ff88;
  1618. }
  1619. &.outbound {
  1620. border-top: 2px solid #00d4ff;
  1621. }
  1622. &.inbound {
  1623. border-top: 2px solid #00ff88;
  1624. }
  1625. }
  1626. }
  1627. /* 托盘数量统计样式 */
  1628. .pallet-statistics {
  1629. display: flex;
  1630. flex-direction: column;
  1631. gap: 6px;
  1632. /* 空托盘总数区域 */
  1633. .pallet-total-section {
  1634. .pallet-total-item {
  1635. padding: 1px 16px;
  1636. background: linear-gradient(135deg, rgba(192, 244, 181, 0.15) 0%, rgba(198, 230, 177, 0.15) 100%);
  1637. border: 2px solid rgba(255, 215, 0, 0.4);
  1638. border-radius: 8px;
  1639. display: flex;
  1640. align-items: center;
  1641. gap: 12px;
  1642. transition: all 0.3s ease;
  1643. box-shadow: 0 2px 8px rgba(255, 215, 0, 0.15);
  1644. &:hover {
  1645. transform: translateY(-2px);
  1646. box-shadow: 0 4px 12px rgba(255, 215, 0, 0.25);
  1647. border-color: rgba(255, 215, 0, 0.6);
  1648. }
  1649. .total-icon {
  1650. font-size: 20px;
  1651. opacity: 0.95;
  1652. }
  1653. .total-info {
  1654. flex: 1;
  1655. display: flex;
  1656. justify-content: space-between;
  1657. align-items: center;
  1658. .total-label {
  1659. font-size: 13px;
  1660. font-weight: 600;
  1661. color: #bded8e;
  1662. letter-spacing: 1px;
  1663. }
  1664. .total-value {
  1665. font-size: 20px;
  1666. font-weight: bold;
  1667. color: #bded8e;
  1668. text-shadow: 0 0 10px rgba(255, 215, 0, 0.3);
  1669. }
  1670. }
  1671. }
  1672. }
  1673. /* 分类明细区域 */
  1674. .pallet-detail-section {
  1675. margin-left: 5px;
  1676. margin-right: 5px;
  1677. .detail-title {
  1678. font-size: 11px;
  1679. color: #8ab8d6;
  1680. margin-bottom: 8px;
  1681. padding-left: 4px;
  1682. letter-spacing: 0.5px;
  1683. opacity: 0.9;
  1684. }
  1685. .detail-items {
  1686. display: flex;
  1687. gap: 8px;
  1688. }
  1689. }
  1690. .pallet-item {
  1691. flex: 1;
  1692. min-width: 100px;
  1693. padding: 4px 12px;
  1694. background: rgba(25, 50, 85, 0.5);
  1695. border: 1px solid rgba(0, 212, 255, 0.15);
  1696. border-radius: 6px;
  1697. display: flex;
  1698. align-items: center;
  1699. gap: 8px;
  1700. transition: all 0.3s ease;
  1701. &:hover {
  1702. background: rgba(25, 50, 85, 0.8);
  1703. border-color: rgba(0, 212, 255, 0.4);
  1704. transform: translateY(-2px);
  1705. }
  1706. .pallet-icon {
  1707. font-size: 22px;
  1708. opacity: 0.9;
  1709. }
  1710. .pallet-info {
  1711. flex: 1;
  1712. display: flex;
  1713. flex-direction: column;
  1714. gap: 4px;
  1715. .pallet-label {
  1716. font-size: 11px;
  1717. color: #8ab8d6;
  1718. letter-spacing: 0.3px;
  1719. }
  1720. .pallet-value {
  1721. font-size: 17px;
  1722. font-weight: bold;
  1723. color: #fff;
  1724. }
  1725. }
  1726. /* 不同托盘类型的边框颜色 */
  1727. &.flat {
  1728. border-left: 2px solid #00ff88;
  1729. .pallet-value {
  1730. color: #00ff88;
  1731. }
  1732. }
  1733. &.guard {
  1734. border-left: 2px solid #7b68ee;
  1735. .pallet-value {
  1736. color: #9370db;
  1737. }
  1738. }
  1739. &.steel {
  1740. border-left: 2px solid #00d4ff;
  1741. .pallet-value {
  1742. color: #00d4ff;
  1743. }
  1744. }
  1745. &.other {
  1746. border-left: 2px solid #b3f38d;
  1747. .pallet-value {
  1748. color: #b0ef77;
  1749. }
  1750. }
  1751. }
  1752. }
  1753. }
  1754. /* ========== 库位利用率卡片 ========== */
  1755. .storage-utilization {
  1756. .utilization-summary {
  1757. display: flex;
  1758. justify-content: space-around;
  1759. margin-bottom: 16px;
  1760. padding: 12px;
  1761. background: rgba(25, 50, 85, 0.7);
  1762. border-radius: 8px;
  1763. .summary-item {
  1764. display: flex;
  1765. flex-direction: column;
  1766. align-items: center;
  1767. gap: 6px;
  1768. .summary-label {
  1769. font-size: 12px;
  1770. color: #8ab8d6;
  1771. }
  1772. .summary-value {
  1773. font-size: 22px;
  1774. font-weight: bold;
  1775. color: #fff;
  1776. &.used {
  1777. color: #00d4ff;
  1778. }
  1779. &.rate {
  1780. color: #00ff88;
  1781. }
  1782. }
  1783. }
  1784. }
  1785. .chart-container {
  1786. height: calc(100% - 70px);
  1787. }
  1788. }
  1789. /* ========== 设备状态卡片 ========== */
  1790. .device-status {
  1791. .device-group {
  1792. margin-bottom: 6px;
  1793. &:last-child {
  1794. margin-bottom: 0;
  1795. }
  1796. .group-title {
  1797. font-size: 13px;
  1798. color: #8ab8d6;
  1799. margin-bottom: 2px;
  1800. padding-bottom: 4px;
  1801. border-bottom: 1px solid rgba(0, 212, 255, 0.2);
  1802. }
  1803. .device-list {
  1804. display: flex;
  1805. flex-direction: column;
  1806. gap: 6px;
  1807. &.agv-grid {
  1808. display: grid;
  1809. grid-template-columns: 1fr 1fr;
  1810. gap: 6px;
  1811. }
  1812. }
  1813. .device-item {
  1814. padding: 8px 10px;
  1815. background: rgba(25, 50, 85, 0.7);
  1816. border: 1px solid rgba(0, 212, 255, 0.2);
  1817. border-left: 3px solid #00d4ff;
  1818. border-radius: 6px;
  1819. &.working {
  1820. border-left-color: #00ff88;
  1821. .status-dot {
  1822. background: #00ff88;
  1823. box-shadow: 0 0 10px #00ff88;
  1824. }
  1825. }
  1826. &.idle {
  1827. border-left-color: #ffaa00;
  1828. .status-dot {
  1829. background: #ffaa00;
  1830. box-shadow: 0 0 10px #ffaa00;
  1831. }
  1832. }
  1833. &.charging {
  1834. border-left-color: #00d4ff;
  1835. .status-dot {
  1836. background: #00d4ff;
  1837. box-shadow: 0 0 10px #00d4ff;
  1838. }
  1839. }
  1840. .device-header {
  1841. display: flex;
  1842. justify-content: space-between;
  1843. align-items: center;
  1844. margin-bottom: 6px;
  1845. }
  1846. .device-name {
  1847. font-size: 14px;
  1848. color: #fff;
  1849. font-weight: bold;
  1850. }
  1851. .device-status-badge {
  1852. display: flex;
  1853. align-items: center;
  1854. gap: 6px;
  1855. .status-dot {
  1856. width: 8px;
  1857. height: 8px;
  1858. border-radius: 50%;
  1859. animation: pulse 2s ease-in-out infinite;
  1860. }
  1861. .status-text {
  1862. font-size: 12px;
  1863. color: #8ab8d6;
  1864. }
  1865. }
  1866. .device-metrics {
  1867. display: flex;
  1868. justify-content: space-between;
  1869. .metric {
  1870. font-size: 11px;
  1871. color: #8ab8d6;
  1872. }
  1873. }
  1874. }
  1875. .device-item-compact {
  1876. padding: 8px;
  1877. background: rgba(25, 50, 85, 0.7);
  1878. border: 1px solid rgba(0, 212, 255, 0.2);
  1879. border-radius: 6px;
  1880. &.working {
  1881. border-left: 3px solid #00ff88;
  1882. .status-dot {
  1883. background: #00ff88;
  1884. box-shadow: 0 0 8px #00ff88;
  1885. }
  1886. }
  1887. &.idle {
  1888. border-left: 3px solid #ffaa00;
  1889. .status-dot {
  1890. background: #ffaa00;
  1891. box-shadow: 0 0 8px #ffaa00;
  1892. }
  1893. }
  1894. &.charging {
  1895. border-left: 3px solid #00d4ff;
  1896. .status-dot {
  1897. background: #00d4ff;
  1898. box-shadow: 0 0 8px #00d4ff;
  1899. }
  1900. }
  1901. .compact-header {
  1902. display: flex;
  1903. justify-content: space-between;
  1904. align-items: center;
  1905. margin-bottom: 4px;
  1906. .compact-name {
  1907. font-size: 13px;
  1908. color: #fff;
  1909. font-weight: bold;
  1910. }
  1911. .compact-status {
  1912. display: flex;
  1913. align-items: center;
  1914. gap: 4px;
  1915. font-size: 11px;
  1916. color: #8ab8d6;
  1917. .status-dot {
  1918. width: 6px;
  1919. height: 6px;
  1920. border-radius: 50%;
  1921. animation: pulse 2s ease-in-out infinite;
  1922. }
  1923. }
  1924. }
  1925. .compact-info {
  1926. display: flex;
  1927. justify-content: space-between;
  1928. font-size: 11px;
  1929. color: #8ab8d6;
  1930. }
  1931. }
  1932. }
  1933. }
  1934. @keyframes pulse {
  1935. 0%, 100% { opacity: 1; }
  1936. 50% { opacity: 0.5; }
  1937. }
  1938. /* ========== 空状态提示 ========== */
  1939. .empty-state {
  1940. display: flex;
  1941. flex-direction: column;
  1942. align-items: center;
  1943. justify-content: center;
  1944. padding: 20px;
  1945. color: rgba(255, 255, 255, 0.4);
  1946. font-size: 13px;
  1947. grid-column: 1 / -1; /* 占据整个网格 */
  1948. i {
  1949. font-size: 28px;
  1950. margin-bottom: 8px;
  1951. opacity: 0.6;
  1952. }
  1953. span {
  1954. opacity: 0.8;
  1955. }
  1956. }
  1957. /* ========== 库存趋势卡片 ========== */
  1958. .inventory-trend {
  1959. .chart-container {
  1960. width: 100%;
  1961. height: 100%;
  1962. }
  1963. }
  1964. /* ========== 呆滞分析卡片 ========== */
  1965. .stagnant-analysis {
  1966. .chart-container {
  1967. width: 100%;
  1968. height: 100%;
  1969. }
  1970. }
  1971. /* ========== 当日作业统计卡片 ========== */
  1972. .daily-operation {
  1973. .operation-visual {
  1974. display: flex;
  1975. gap: 8px;
  1976. height: calc(100% - 10px);
  1977. .visual-left {
  1978. width: 280px;
  1979. height: 250px;
  1980. flex-shrink: 0;
  1981. display: flex;
  1982. align-items: center;
  1983. justify-content: center;
  1984. padding: 15px;
  1985. .mini-chart {
  1986. width: 100%;
  1987. height: 100%;
  1988. }
  1989. }
  1990. .visual-right {
  1991. flex: 1;
  1992. display: flex;
  1993. flex-direction: column;
  1994. justify-content: space-between;
  1995. .operation-stat {
  1996. flex: 1;
  1997. display: flex;
  1998. flex-direction: column;
  1999. justify-content: space-around;
  2000. .stat-row {
  2001. display: flex;
  2002. justify-content: space-between;
  2003. align-items: center;
  2004. padding: 4px 8px;
  2005. background: rgba(25, 50, 85, 0.7);
  2006. border-radius: 6px;
  2007. border-left: 3px solid transparent;
  2008. .stat-label {
  2009. font-size: 12px;
  2010. color: #8ab8d6;
  2011. }
  2012. .stat-number {
  2013. font-size: 16px;
  2014. font-weight: bold;
  2015. color: #fff;
  2016. &.total {
  2017. color: #00d4ff;
  2018. }
  2019. &.completed {
  2020. color: #00ff88;
  2021. }
  2022. &.processing {
  2023. color: #ffaa00;
  2024. }
  2025. &.pending {
  2026. color: #9370db;
  2027. }
  2028. }
  2029. &:nth-child(1) { border-left-color: #00d4ff; }
  2030. &:nth-child(2) { border-left-color: #00ff88; }
  2031. &:nth-child(3) { border-left-color: #ffaa00; }
  2032. &:nth-child(4) { border-left-color: #9370db; }
  2033. }
  2034. }
  2035. .completion-rate {
  2036. margin-top: 8px;
  2037. padding: 8px;
  2038. background: rgba(0, 212, 255, 0.1);
  2039. border-radius: 8px;
  2040. .rate-label {
  2041. font-size: 10px;
  2042. color: #8ab8d6;
  2043. margin-bottom: 3px;
  2044. }
  2045. .rate-value {
  2046. font-size: 20px;
  2047. font-weight: bold;
  2048. color: #00ff88;
  2049. margin-bottom: 5px;
  2050. text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
  2051. }
  2052. .rate-bar {
  2053. height: 6px;
  2054. background: rgba(25, 50, 85, 0.9);
  2055. border-radius: 3px;
  2056. overflow: hidden;
  2057. .rate-fill {
  2058. height: 100%;
  2059. background: linear-gradient(90deg, #00ff88, #00d4ff);
  2060. border-radius: 4px;
  2061. transition: width 0.5s ease;
  2062. box-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
  2063. }
  2064. }
  2065. }
  2066. }
  2067. }
  2068. }
  2069. /* ========== 底部装饰效果 ========== */
  2070. .bottom-decoration-bar {
  2071. position: fixed;
  2072. bottom: 0;
  2073. left: 0;
  2074. width: 100%;
  2075. height: 40px;
  2076. z-index: 100;
  2077. pointer-events: none;
  2078. }
  2079. /* 浮动数据点 */
  2080. .floating-data-points {
  2081. position: absolute;
  2082. bottom: 0;
  2083. left: 0;
  2084. width: 100%;
  2085. height: 40px;
  2086. }
  2087. .data-point {
  2088. position: absolute;
  2089. bottom: 8px;
  2090. &.point-1 {
  2091. left: 15%;
  2092. animation: float-up 3s ease-in-out infinite;
  2093. }
  2094. &.point-2 {
  2095. left: 30%;
  2096. animation: float-up 3.5s ease-in-out infinite 0.5s;
  2097. }
  2098. &.point-3 {
  2099. left: 50%;
  2100. animation: float-up 3.2s ease-in-out infinite 1s;
  2101. }
  2102. &.point-4 {
  2103. left: 70%;
  2104. animation: float-up 3.8s ease-in-out infinite 1.5s;
  2105. }
  2106. &.point-5 {
  2107. left: 85%;
  2108. animation: float-up 3.3s ease-in-out infinite 2s;
  2109. }
  2110. }
  2111. .point-ring {
  2112. width: 14px;
  2113. height: 14px;
  2114. border: 2px solid rgba(0, 212, 255, 0.6);
  2115. border-radius: 50%;
  2116. animation: ring-expand 2s ease-out infinite;
  2117. }
  2118. .point-core {
  2119. position: absolute;
  2120. top: 50%;
  2121. left: 50%;
  2122. transform: translate(-50%, -50%);
  2123. width: 6px;
  2124. height: 6px;
  2125. background: #00d4ff;
  2126. border-radius: 50%;
  2127. box-shadow:
  2128. 0 0 8px #00d4ff,
  2129. 0 0 12px #00d4ff;
  2130. }
  2131. /* 动画定义 */
  2132. @keyframes glow-slide {
  2133. 0% {
  2134. transform: translateX(-100%);
  2135. opacity: 0;
  2136. }
  2137. 20% {
  2138. opacity: 1;
  2139. }
  2140. 80% {
  2141. opacity: 1;
  2142. }
  2143. 100% {
  2144. transform: translateX(100%);
  2145. opacity: 0;
  2146. }
  2147. }
  2148. @keyframes float-up {
  2149. 0%, 100% {
  2150. transform: translateY(0);
  2151. opacity: 0.6;
  2152. }
  2153. 50% {
  2154. transform: translateY(-15px);
  2155. opacity: 1;
  2156. }
  2157. }
  2158. @keyframes ring-expand {
  2159. 0% {
  2160. transform: scale(0.6);
  2161. opacity: 0.8;
  2162. }
  2163. 100% {
  2164. transform: scale(2.5);
  2165. opacity: 0;
  2166. }
  2167. }
  2168. /* ========== 响应式设计 ========== */
  2169. @media screen and (max-width: 1920px) {
  2170. .screen-header {
  2171. .screen-title {
  2172. font-size: 26px;
  2173. letter-spacing: 1.5px;
  2174. }
  2175. .title-subtitle {
  2176. font-size: 11px;
  2177. }
  2178. }
  2179. }
  2180. @media screen and (max-width: 1600px) {
  2181. .screen-header {
  2182. height: 100px;
  2183. padding: 0 30px;
  2184. .screen-title {
  2185. font-size: 24px;
  2186. letter-spacing: 1px;
  2187. }
  2188. .title-subtitle {
  2189. font-size: 11px;
  2190. letter-spacing: 0.5px;
  2191. }
  2192. }
  2193. .content-row {
  2194. &.row-1 { height: 340px; }
  2195. &.row-2 { height: 280px; }
  2196. &.row-3 { height: 310px; }
  2197. }
  2198. }
  2199. </style>