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.

712 lines
17 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
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="picking-board-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>
  11. <!-- 顶部标题栏 -->
  12. <div class="screen-header">
  13. <!-- CCL Logo -->
  14. <div class="header-logo">
  15. <img src="~@/assets/img/cclbai.png" alt="CCL Logo" class="logo-img">
  16. </div>
  17. <div class="header-decoration left"></div>
  18. <div class="header-center">
  19. <div class="title-glow"></div>
  20. <h1 class="screen-title">成品入库出库区实时看板</h1>
  21. <div class="title-subtitle">Finished Product Area Real-time Dashboard</div>
  22. </div>
  23. <div class="header-decoration right"></div>
  24. <div class="header-time">
  25. <div class="time-icon"></div>
  26. <div class="time-text">{{ currentTime }}</div>
  27. </div>
  28. </div>
  29. <!-- 主内容区 -->
  30. <div class="screen-content">
  31. <!-- 成品包装区面板 -->
  32. <div class="picking-panel">
  33. <!-- 面板标题 -->
  34. <div class="panel-title-bar">
  35. <div class="title-left">
  36. <div class="title-icon"></div>
  37. <div class="title-text">
  38. <span class="title-main">成品包装区</span>
  39. </div>
  40. </div>
  41. </div>
  42. <!-- 数据表格 -->
  43. <div class="panel-table">
  44. <table class="data-table">
  45. <thead>
  46. <tr>
  47. <th style="width: 60px;">No.</th>
  48. <th style="width: 100px;">存放位置</th>
  49. <th style="width: 110px;">托盘码</th>
  50. <th style="width: 100px;">拣选位置</th>
  51. <th style="width: 180px;">拣选物料名称</th>
  52. <th style="width: 100px;">拣选数量</th>
  53. <th style="width: 120px;">状态</th>
  54. </tr>
  55. </thead>
  56. <tbody>
  57. <tr v-for="(item, idx) in packagingList" :key="idx">
  58. <td class="text-center">{{ idx + 1 }}</td>
  59. <td class="text-center">{{ item.storageLocation }}</td>
  60. <td class="text-center">{{ item.palletCode }}</td>
  61. <td class="text-center">{{ item.pickingLocation }}</td>
  62. <td class="text-center">{{ item.materialName }}</td>
  63. <td class="text-center">{{ item.quantity }}</td>
  64. <td class="text-center">
  65. <span :class="['status-badge', getStatusClass(item.status)]">
  66. {{ item.status }}
  67. </span>
  68. </td>
  69. </tr>
  70. </tbody>
  71. </table>
  72. </div>
  73. </div>
  74. <!-- 成品入库区面板 -->
  75. <div class="picking-panel">
  76. <!-- 面板标题 -->
  77. <div class="panel-title-bar">
  78. <div class="title-left">
  79. <div class="title-icon"></div>
  80. <div class="title-text">
  81. <span class="title-main">成品入库区</span>
  82. </div>
  83. </div>
  84. </div>
  85. <!-- 数据表格 -->
  86. <div class="panel-table">
  87. <table class="data-table">
  88. <thead>
  89. <tr>
  90. <th style="width: 80px;">No.</th>
  91. <th style="width: 150px;">存放位置</th>
  92. <th style="width: 150px;">托盘码</th>
  93. <th style="width: 150px;">任务分类</th>
  94. <th style="width: 200px;">状态</th>
  95. </tr>
  96. </thead>
  97. <tbody>
  98. <tr v-for="(item, idx) in inboundList" :key="idx">
  99. <td class="text-center">{{ idx + 1 }}</td>
  100. <td class="text-center">{{ item.storageLocation }}</td>
  101. <td class="text-center">{{ item.palletCode }}</td>
  102. <td class="text-center">{{ item.taskType }}</td>
  103. <td class="text-center">
  104. <span :class="['status-badge', getStatusClass(item.status)]">
  105. {{ item.status }}
  106. </span>
  107. </td>
  108. </tr>
  109. </tbody>
  110. </table>
  111. </div>
  112. </div>
  113. </div>
  114. <!-- 底部装饰 -->
  115. <div class="screen-footer">
  116. <div class="footer-line"></div>
  117. </div>
  118. </div>
  119. </template>
  120. <script>
  121. import {finishedProductBoard} from '@/api/dashboard/dashboard.js'
  122. import WebSocketClient from '@/utils/websocket'
  123. export default {
  124. name: 'FinishedProductBoard',
  125. data() {
  126. return {
  127. currentTime: '',
  128. timeInterval: null,
  129. serverTimeOffset: 0, // 服务器时间偏移量(毫秒)
  130. // WebSocket相关
  131. useWebSocket: true, // 是否使用WebSocket(可切换为false降级到轮询)
  132. wsConnected: false, // WebSocket连接状态
  133. wsSubscription: null, // WebSocket订阅ID
  134. // 成品包装区数据
  135. packagingList: [],
  136. // 成品入库区数据
  137. inboundList: []
  138. }
  139. },
  140. mounted() {
  141. // 初始化时间显示
  142. this.currentTime = '等待服务器时间同步...'
  143. // 启动时钟定时器(每秒更新)
  144. this.timeInterval = setInterval(() => {
  145. this.updateTime()
  146. }, 1000)
  147. // 根据配置选择使用WebSocket或轮询
  148. if (this.useWebSocket) {
  149. this.initWebSocket()
  150. }
  151. },
  152. beforeDestroy() {
  153. // 清理时间更新定时器
  154. if (this.timeInterval) {
  155. clearInterval(this.timeInterval)
  156. }
  157. // 断开WebSocket连接
  158. this.disconnectWebSocket()
  159. },
  160. methods: {
  161. /**
  162. * 初始化WebSocket连接
  163. */
  164. initWebSocket() {
  165. var apiServer = (process.env.NODE_ENV !== 'production' && process.env.OPEN_PROXY ? '/proxyApi/' : window.SITE_CONFIG.baseUrl);
  166. // 连接WebSocket服务器
  167. const wsUrl = apiServer + '/ws/dashboard'
  168. WebSocketClient.connect(
  169. wsUrl,
  170. () => {
  171. this.wsConnected = true
  172. // 订阅成品入库出库区看板主题
  173. this.wsSubscription = WebSocketClient.subscribe(
  174. '/topic/dashboard/finished-product-board',
  175. this.handleWebSocketMessage
  176. )
  177. },
  178. (error) => {
  179. // 连接失败回调
  180. console.error('[成品入库出库区看板] WebSocket连接失败,降级到轮询模式', error)
  181. this.wsConnected = false
  182. this.fallbackToPolling()
  183. }
  184. )
  185. },
  186. /**
  187. * 处理WebSocket推送的消息
  188. *
  189. * @param {object} message WebSocket推送的消息
  190. */
  191. handleWebSocketMessage(message) {
  192. if (message && message.code === 0) {
  193. // 更新服务器时间偏移量
  194. if (message.serverTime) {
  195. this.updateServerTimeOffset(message.serverTime)
  196. }
  197. this.packagingList = message.data.packagingList || []
  198. this.inboundList = message.data.inboundList || []
  199. }
  200. },
  201. /**
  202. * 断开WebSocket连接
  203. */
  204. disconnectWebSocket() {
  205. if (this.wsSubscription) {
  206. WebSocketClient.unsubscribe(this.wsSubscription)
  207. this.wsSubscription = null
  208. }
  209. if (this.wsConnected) {
  210. WebSocketClient.disconnect()
  211. this.wsConnected = false
  212. console.log('[成品入库出库区看板] WebSocket已断开')
  213. }
  214. },
  215. /**
  216. * 降级到轮询模式
  217. */
  218. fallbackToPolling() {
  219. this.getDataList()
  220. },
  221. /**
  222. * 获取数据列表
  223. *
  224. * <p><b>功能说明</b>从后端API获取成品入库出库区看板实时数据</p>
  225. * <p><b>数据更新策略</b>覆盖而非追加避免内存累积</p>
  226. */
  227. getDataList() {
  228. finishedProductBoard({}).then(({data}) => {
  229. if (data && data.code === 200) {
  230. console.log('获取成品入库出库区数据成功:', data.data)
  231. // 处理返回的数据
  232. if (data.data) {
  233. // 成品包装区数据
  234. if (data.data.packagingList && data.data.packagingList.length > 0) {
  235. this.packagingList = data.data.packagingList
  236. console.log('成品包装区数据已更新,共', this.packagingList.length, '条')
  237. } else {
  238. console.log('暂无成品包装区数据')
  239. }
  240. // 成品入库区数据
  241. if (data.data.inboundList && data.data.inboundList.length > 0) {
  242. this.inboundList = data.data.inboundList
  243. console.log('成品入库区数据已更新,共', this.inboundList.length, '条')
  244. } else {
  245. console.log('暂无成品入库区数据')
  246. }
  247. }
  248. } else {
  249. console.error('获取成品入库出库区数据失败: 返回码不正确')
  250. }
  251. }).catch(error => {
  252. console.error('获取成品入库出库区数据失败:', error)
  253. })
  254. },
  255. /**
  256. * 更新服务器时间偏移量
  257. */
  258. updateServerTimeOffset(serverTimeString) {
  259. try {
  260. const timeStr = serverTimeString.split(' ')[0] + ' ' + serverTimeString.split(' ')[1]
  261. const serverTime = new Date(timeStr).getTime()
  262. const localTime = new Date().getTime()
  263. if (!isNaN(serverTime)) {
  264. this.serverTimeOffset = serverTime - localTime
  265. }
  266. } catch (error) {
  267. console.warn('解析服务器时间失败:', error)
  268. }
  269. },
  270. /**
  271. * 更新当前时间使用服务器时间偏移量
  272. */
  273. updateTime() {
  274. const now = new Date(new Date().getTime() + this.serverTimeOffset)
  275. const year = now.getFullYear()
  276. const month = String(now.getMonth() + 1).padStart(2, '0')
  277. const day = String(now.getDate()).padStart(2, '0')
  278. const hours = String(now.getHours()).padStart(2, '0')
  279. const minutes = String(now.getMinutes()).padStart(2, '0')
  280. const seconds = String(now.getSeconds()).padStart(2, '0')
  281. const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  282. const weekDay = weekDays[now.getDay()]
  283. this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${weekDay}`
  284. },
  285. /**
  286. * 根据状态获取样式类名
  287. */
  288. getStatusClass(status) {
  289. const statusMap = {
  290. '已到达': 'status-success',
  291. 'AGV运送中': 'status-warning',
  292. '已组盘': 'status-info',
  293. '等待': 'status-pending'
  294. }
  295. return statusMap[status] || 'status-pending'
  296. }
  297. }
  298. }
  299. </script>
  300. <style scoped lang="scss">
  301. /* ===== 整体容器 ===== */
  302. .picking-board-screen {
  303. width: 100vw;
  304. height: 100vh;
  305. background: linear-gradient(135deg, #5f8cc3 0%, #749cc8 100%);
  306. position: relative;
  307. overflow: hidden;
  308. font-family: 'Microsoft YaHei', 'PingFang SC', Arial, sans-serif;
  309. }
  310. /* ===== 装饰背景 ===== */
  311. .bg-decoration {
  312. display: none;
  313. }
  314. /* ===== 顶部标题区 ===== */
  315. .screen-header {
  316. position: relative;
  317. height: 60px;
  318. display: flex;
  319. align-items: center;
  320. justify-content: center;
  321. padding: 0 40px;
  322. z-index: 10;
  323. border-bottom: 2px solid rgba(23, 179, 163, 0.4);
  324. background: linear-gradient(180deg, rgba(23, 179, 163, 0.08) 0%, transparent 100%);
  325. }
  326. /* CCL Logo */
  327. .header-logo {
  328. position: absolute;
  329. left: 20px;
  330. top: 50%;
  331. transform: translateY(-50%);
  332. z-index: 20;
  333. }
  334. .logo-img {
  335. height: 40px;
  336. width: auto;
  337. filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
  338. transition: all 0.3s ease;
  339. &:hover {
  340. filter: drop-shadow(0 4px 12px rgba(23, 179, 163, 0.5));
  341. transform: scale(1.05);
  342. }
  343. }
  344. .header-decoration {
  345. display: none;
  346. }
  347. .header-center {
  348. position: relative;
  349. text-align: center;
  350. }
  351. .title-glow {
  352. display: none;
  353. }
  354. .screen-title {
  355. position: relative;
  356. font-size: 28px;
  357. font-weight: bold;
  358. color: #ffffff;
  359. margin: 0;
  360. letter-spacing: 3px;
  361. text-shadow: 0 0 20px rgba(23, 179, 163, 0.5);
  362. }
  363. .title-subtitle {
  364. font-size: 10px;
  365. color: rgba(255, 255, 255, 0.8);
  366. letter-spacing: 2px;
  367. margin-top: 3px;
  368. font-family: Arial, sans-serif;
  369. text-transform: uppercase;
  370. }
  371. .header-time {
  372. position: absolute;
  373. right: 10px;
  374. top: 50%;
  375. transform: translateY(-50%);
  376. display: flex;
  377. align-items: center;
  378. gap: 12px;
  379. background: rgba(23, 179, 163, 0.15);
  380. padding: 12px 20px;
  381. border-radius: 8px;
  382. border: 1px solid rgba(23, 179, 163, 0.4);
  383. backdrop-filter: blur(10px);
  384. }
  385. .time-icon {
  386. font-size: 14px;
  387. color: #17B3A3;
  388. }
  389. .time-text {
  390. font-size: 16px;
  391. color: #ffffff;
  392. font-family: 'Consolas', 'Courier New', monospace;
  393. font-weight: 500;
  394. letter-spacing: 1px;
  395. }
  396. /* ===== 主内容区 ===== */
  397. .screen-content {
  398. position: relative;
  399. z-index: 1;
  400. padding: 10px 5px;
  401. height: calc(100vh - 60px);
  402. overflow-y: auto;
  403. display: flex;
  404. gap: 30px;
  405. &::-webkit-scrollbar {
  406. width: 6px;
  407. }
  408. &::-webkit-scrollbar-track {
  409. background: rgba(23, 179, 163, 0.1);
  410. }
  411. &::-webkit-scrollbar-thumb {
  412. background: rgba(23, 179, 163, 0.5);
  413. border-radius: 3px;
  414. &:hover {
  415. background: rgba(23, 179, 163, 0.7);
  416. }
  417. }
  418. }
  419. /* ===== 拣选面板 ===== */
  420. .picking-panel {
  421. margin-left: 5px;
  422. flex: 1;
  423. background: rgba(70, 90, 120, 0.9);
  424. backdrop-filter: blur(10px);
  425. border: 1px solid rgba(23, 179, 163, 0.5);
  426. border-radius: 12px;
  427. box-shadow:
  428. 0 8px 32px rgba(0, 0, 0, 0.4),
  429. inset 0 1px 0 rgba(255, 255, 255, 0.1);
  430. overflow: hidden;
  431. transition: all 0.3s ease;
  432. display: flex;
  433. flex-direction: column;
  434. &:hover {
  435. border-color: rgba(23, 179, 163, 0.5);
  436. box-shadow:
  437. 0 12px 48px rgba(0, 0, 0, 0.5),
  438. 0 0 30px rgba(23, 179, 163, 0.2);
  439. transform: translateY(-2px);
  440. }
  441. }
  442. /* ===== 面板标题栏 ===== */
  443. .panel-title-bar {
  444. background: linear-gradient(135deg, rgba(23, 179, 163, 0.3) 0%, rgba(23, 179, 163, 0.15) 100%);
  445. border-bottom: 1px solid rgba(23, 179, 163, 0.3);
  446. padding: 10px 16px;
  447. display: flex;
  448. justify-content: space-between;
  449. align-items: center;
  450. }
  451. .title-left {
  452. display: flex;
  453. align-items: center;
  454. gap: 15px;
  455. flex: 1;
  456. }
  457. .title-icon {
  458. font-size: 16px;
  459. color: #64D8CB;
  460. }
  461. .title-text {
  462. display: flex;
  463. align-items: center;
  464. gap: 8px;
  465. flex-wrap: wrap;
  466. }
  467. .title-main {
  468. font-size: 18px;
  469. font-weight: bold;
  470. color: #ffffff;
  471. text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
  472. }
  473. /* ===== 数据表格 ===== */
  474. .panel-table {
  475. padding: 5px 5px;
  476. flex: 1;
  477. overflow-y: auto;
  478. &::-webkit-scrollbar {
  479. width: 4px;
  480. }
  481. &::-webkit-scrollbar-track {
  482. background: rgba(23, 179, 163, 0.05);
  483. }
  484. &::-webkit-scrollbar-thumb {
  485. background: rgba(23, 179, 163, 0.3);
  486. border-radius: 2px;
  487. &:hover {
  488. background: rgba(23, 179, 163, 0.5);
  489. }
  490. }
  491. }
  492. .data-table {
  493. width: 100%;
  494. border-collapse: separate;
  495. border-spacing: 0;
  496. thead {
  497. tr {
  498. background: linear-gradient(135deg, rgba(23, 179, 163, 0.25) 0%, rgba(23, 179, 163, 0.15) 100%);
  499. }
  500. th {
  501. padding: 8px 2px;
  502. color: #fcfdfd;
  503. font-size: 10px;
  504. font-weight: bold;
  505. text-align: center;
  506. border-bottom: 2px solid rgba(23, 179, 163, 0.4);
  507. text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
  508. white-space: nowrap;
  509. &:first-child {
  510. border-top-left-radius: 8px;
  511. }
  512. &:last-child {
  513. border-top-right-radius: 8px;
  514. }
  515. }
  516. }
  517. tbody {
  518. tr {
  519. background: rgba(60, 80, 105, 0.6);
  520. transition: all 0.3s ease;
  521. &:nth-child(even) {
  522. background: rgba(70, 90, 115, 0.7);
  523. }
  524. &:hover {
  525. background: rgba(23, 179, 163, 0.15);
  526. box-shadow: 0 4px 12px rgba(23, 179, 163, 0.2);
  527. }
  528. &:last-child {
  529. td:first-child {
  530. border-bottom-left-radius: 8px;
  531. }
  532. td:last-child {
  533. border-bottom-right-radius: 8px;
  534. }
  535. }
  536. }
  537. td {
  538. padding: 7px 2px;
  539. color: rgba(255, 255, 255, 0.9);
  540. font-size: 12px;
  541. border-bottom: 1px solid rgba(23, 179, 163, 0.15);
  542. &.text-center {
  543. text-align: center;
  544. }
  545. &.text-left {
  546. text-align: left;
  547. }
  548. &.text-right {
  549. text-align: right;
  550. }
  551. }
  552. }
  553. }
  554. /* ===== 状态徽章 ===== */
  555. .status-badge {
  556. display: inline-block;
  557. padding: 3px 8px;
  558. border-radius: 12px;
  559. font-size: 10px;
  560. font-weight: bold;
  561. text-align: center;
  562. min-width: 70px;
  563. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  564. &.status-success {
  565. background: linear-gradient(135deg, #10b981, #34d399);
  566. color: #ffffff;
  567. box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
  568. }
  569. &.status-warning {
  570. background: linear-gradient(135deg, #f59e0b, #fbbf24);
  571. color: #ffffff;
  572. box-shadow: 0 0 15px rgba(245, 158, 11, 0.5);
  573. }
  574. &.status-info {
  575. background: linear-gradient(135deg, #3b82f6, #60a5fa);
  576. color: #ffffff;
  577. box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
  578. }
  579. &.status-pending {
  580. background: linear-gradient(135deg, #6b7280, #9ca3af);
  581. color: #ffffff;
  582. box-shadow: 0 0 15px rgba(107, 114, 128, 0.5);
  583. }
  584. }
  585. /* ===== 底部装饰 ===== */
  586. .screen-footer {
  587. display: none;
  588. }
  589. /* ===== 响应式适配 ===== */
  590. @media screen and (max-width: 1600px) {
  591. .screen-title {
  592. font-size: 26px;
  593. letter-spacing: 3px;
  594. }
  595. .title-main {
  596. font-size: 18px;
  597. }
  598. }
  599. @media screen and (min-width: 2560px) {
  600. .screen-title {
  601. font-size: 32px;
  602. }
  603. .panel-title-bar {
  604. padding: 12px 20px;
  605. }
  606. .title-main {
  607. font-size: 18px;
  608. }
  609. .data-table {
  610. thead th {
  611. font-size: 12px;
  612. padding: 9px 5px;
  613. }
  614. tbody td {
  615. font-size: 14px;
  616. padding: 8px 12px;
  617. }
  618. }
  619. }
  620. </style>