You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2031 lines
52 KiB

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