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.

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