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.

662 lines
16 KiB

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