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.

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