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.

689 lines
17 KiB

11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
11 months ago
​
​
​
​
​
​
​
​
11 months ago
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
11 months ago
11 months ago
11 months ago
​
​
​
​
​
​
11 months ago
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
11 months ago
​
​
​
​
​
​
​
​
11 months ago
​
​
​
11 months ago
11 months ago
​
11 months ago
​
11 months ago
11 months ago
​
​
​
11 months ago
11 months ago
​
​
​
11 months ago
11 months ago
​
​
​
11 months ago
​
​
11 months ago
11 months ago
​
​
11 months ago
​
​
11 months ago
​
​
​
​
11 months ago
​
​
11 months ago
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
11 months ago
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
11 months ago
11 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/ccl.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 }}</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 }}</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. }
  262. },
  263. /**
  264. * 断开WebSocket连接
  265. */
  266. disconnectWebSocket() {
  267. if (this.wsSubscription) {
  268. WebSocketClient.unsubscribe(this.wsSubscription)
  269. this.wsSubscription = null
  270. }
  271. if (this.wsConnected) {
  272. WebSocketClient.disconnect()
  273. this.wsConnected = false
  274. console.log('[机械臂拣选看板] WebSocket已断开')
  275. }
  276. },
  277. /**
  278. * 降级到轮询模式
  279. */
  280. fallbackToPolling() {
  281. this.getDataList()
  282. }
  283. }
  284. }
  285. </script>
  286. <style scoped lang="scss">
  287. /* ===== 整体容器 ===== */
  288. .picking-board-screen {
  289. width: 100vw;
  290. height: 100vh;
  291. background: linear-gradient(135deg, #5f8cc3 0%, #749cc8 100%);
  292. position: relative;
  293. overflow: hidden;
  294. font-family: 'Microsoft YaHei', 'PingFang SC', Arial, sans-serif;
  295. }
  296. /* ===== 装饰背景 ===== */
  297. .bg-decoration {
  298. display: none;
  299. }
  300. /* ===== 顶部标题区 ===== */
  301. .screen-header {
  302. position: relative;
  303. height: 60px;
  304. display: flex;
  305. align-items: center;
  306. justify-content: center;
  307. padding: 0 40px;
  308. z-index: 10;
  309. border-bottom: 2px solid rgba(23, 179, 163, 0.4);
  310. background: linear-gradient(180deg, rgba(23, 179, 163, 0.08) 0%, transparent 100%);
  311. }
  312. /* CCL Logo */
  313. .header-logo {
  314. position: absolute;
  315. left: 20px;
  316. top: 50%;
  317. transform: translateY(-50%);
  318. z-index: 20;
  319. }
  320. .logo-img {
  321. height: 40px;
  322. width: auto;
  323. filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
  324. transition: all 0.3s ease;
  325. &:hover {
  326. filter: drop-shadow(0 4px 12px rgba(23, 179, 163, 0.5));
  327. transform: scale(1.05);
  328. }
  329. }
  330. .header-decoration {
  331. display: none;
  332. }
  333. .header-center {
  334. position: relative;
  335. text-align: center;
  336. }
  337. .title-glow {
  338. display: none;
  339. }
  340. .screen-title {
  341. position: relative;
  342. font-size: 28px;
  343. font-weight: bold;
  344. color: #ffffff;
  345. margin: 0;
  346. letter-spacing: 3px;
  347. text-shadow: 0 0 20px rgba(23, 179, 163, 0.5);
  348. }
  349. .title-subtitle {
  350. font-size: 10px;
  351. color: rgba(255, 255, 255, 0.8);
  352. letter-spacing: 2px;
  353. margin-top: 3px;
  354. font-family: Arial, sans-serif;
  355. text-transform: uppercase;
  356. }
  357. .header-time {
  358. position: absolute;
  359. right: 10px;
  360. top: 50%;
  361. transform: translateY(-50%);
  362. display: flex;
  363. align-items: center;
  364. gap: 12px;
  365. background: rgba(23, 179, 163, 0.15);
  366. padding: 12px 20px;
  367. border-radius: 8px;
  368. border: 1px solid rgba(23, 179, 163, 0.4);
  369. backdrop-filter: blur(10px);
  370. }
  371. .time-icon {
  372. font-size: 14px;
  373. color: #17B3A3;
  374. }
  375. .time-text {
  376. font-size: 16px;
  377. color: #ffffff;
  378. font-family: 'Consolas', 'Courier New', monospace;
  379. font-weight: 500;
  380. letter-spacing: 1px;
  381. }
  382. /* ===== 主内容区 ===== */
  383. .screen-content {
  384. position: relative;
  385. z-index: 1;
  386. padding: 10px 5px;
  387. height: calc(100vh - 60px);
  388. overflow-y: auto;
  389. display: flex;
  390. gap: 30px;
  391. &::-webkit-scrollbar {
  392. width: 6px;
  393. }
  394. &::-webkit-scrollbar-track {
  395. background: rgba(23, 179, 163, 0.1);
  396. }
  397. &::-webkit-scrollbar-thumb {
  398. background: rgba(23, 179, 163, 0.5);
  399. border-radius: 3px;
  400. &:hover {
  401. background: rgba(23, 179, 163, 0.7);
  402. }
  403. }
  404. }
  405. /* ===== 拣选面板 ===== */
  406. .picking-panel {
  407. margin-left: 5px;
  408. flex: 1;
  409. background: rgba(70, 90, 120, 0.9);
  410. backdrop-filter: blur(10px);
  411. border: 1px solid rgba(23, 179, 163, 0.5);
  412. border-radius: 12px;
  413. box-shadow:
  414. 0 8px 32px rgba(0, 0, 0, 0.4),
  415. inset 0 1px 0 rgba(255, 255, 255, 0.1);
  416. overflow: hidden;
  417. transition: all 0.3s ease;
  418. display: flex;
  419. flex-direction: column;
  420. &:hover {
  421. border-color: rgba(23, 179, 163, 0.5);
  422. box-shadow:
  423. 0 12px 48px rgba(0, 0, 0, 0.5),
  424. 0 0 30px rgba(23, 179, 163, 0.2);
  425. transform: translateY(-2px);
  426. }
  427. }
  428. /* ===== 面板标题栏 ===== */
  429. .panel-title-bar {
  430. background: linear-gradient(135deg, rgba(23, 179, 163, 0.3) 0%, rgba(23, 179, 163, 0.15) 100%);
  431. border-bottom: 1px solid rgba(23, 179, 163, 0.3);
  432. padding: 10px 16px;
  433. display: flex;
  434. justify-content: space-between;
  435. align-items: center;
  436. }
  437. .title-left {
  438. display: flex;
  439. align-items: center;
  440. gap: 15px;
  441. flex: 1;
  442. }
  443. .title-icon {
  444. font-size: 16px;
  445. color: #64D8CB;
  446. }
  447. .title-text {
  448. display: flex;
  449. align-items: center;
  450. gap: 8px;
  451. flex-wrap: wrap;
  452. }
  453. .title-main {
  454. font-size: 18px;
  455. font-weight: bold;
  456. color: #ffffff;
  457. text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
  458. }
  459. /* ===== 数据表格 ===== */
  460. .panel-table {
  461. padding: 5px 5px;
  462. flex: 1;
  463. overflow-y: auto;
  464. &::-webkit-scrollbar {
  465. width: 4px;
  466. }
  467. &::-webkit-scrollbar-track {
  468. background: rgba(23, 179, 163, 0.05);
  469. }
  470. &::-webkit-scrollbar-thumb {
  471. background: rgba(23, 179, 163, 0.3);
  472. border-radius: 2px;
  473. &:hover {
  474. background: rgba(23, 179, 163, 0.5);
  475. }
  476. }
  477. }
  478. .data-table {
  479. width: 100%;
  480. border-collapse: separate;
  481. border-spacing: 0;
  482. thead {
  483. tr {
  484. background: linear-gradient(135deg, rgba(23, 179, 163, 0.25) 0%, rgba(23, 179, 163, 0.15) 100%);
  485. }
  486. th {
  487. padding: 8px 2px;
  488. color: #fcfdfd;
  489. font-size: 10px;
  490. font-weight: bold;
  491. text-align: center;
  492. border-bottom: 2px solid rgba(23, 179, 163, 0.4);
  493. text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
  494. white-space: nowrap;
  495. &:first-child {
  496. border-top-left-radius: 8px;
  497. }
  498. &:last-child {
  499. border-top-right-radius: 8px;
  500. }
  501. }
  502. }
  503. tbody {
  504. tr {
  505. background: rgba(60, 80, 105, 0.6);
  506. transition: all 0.3s ease;
  507. &:nth-child(even) {
  508. background: rgba(70, 90, 115, 0.7);
  509. }
  510. &:hover {
  511. background: rgba(23, 179, 163, 0.15);
  512. box-shadow: 0 4px 12px rgba(23, 179, 163, 0.2);
  513. }
  514. &:last-child {
  515. td:first-child {
  516. border-bottom-left-radius: 8px;
  517. }
  518. td:last-child {
  519. border-bottom-right-radius: 8px;
  520. }
  521. }
  522. }
  523. td {
  524. padding: 7px 2px;
  525. color: rgba(255, 255, 255, 0.9);
  526. font-size: 12px;
  527. border-bottom: 1px solid rgba(23, 179, 163, 0.15);
  528. &.text-center {
  529. text-align: center;
  530. }
  531. &.text-left {
  532. text-align: left;
  533. }
  534. &.text-right {
  535. text-align: right;
  536. }
  537. }
  538. }
  539. }
  540. /* ===== 状态徽章 ===== */
  541. .status-badge {
  542. display: inline-block;
  543. padding: 3px 8px;
  544. border-radius: 12px;
  545. font-size: 10px;
  546. font-weight: bold;
  547. text-align: center;
  548. min-width: 60px;
  549. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  550. &.status-success {
  551. background: linear-gradient(135deg, #10b981, #34d399);
  552. color: #ffffff;
  553. box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
  554. }
  555. &.status-warning {
  556. background: linear-gradient(135deg, #f59e0b, #fbbf24);
  557. color: #ffffff;
  558. box-shadow: 0 0 15px rgba(245, 158, 11, 0.5);
  559. }
  560. &.status-pending {
  561. background: linear-gradient(135deg, #6b7280, #9ca3af);
  562. color: #ffffff;
  563. box-shadow: 0 0 15px rgba(107, 114, 128, 0.5);
  564. }
  565. }
  566. /* ===== 底部装饰 ===== */
  567. .screen-footer {
  568. display: none;
  569. }
  570. /* ===== 响应式适配 ===== */
  571. @media screen and (max-width: 1600px) {
  572. .screen-title {
  573. font-size: 26px;
  574. letter-spacing: 3px;
  575. }
  576. .title-main {
  577. font-size: 18px;
  578. }
  579. }
  580. @media screen and (min-width: 2560px) {
  581. .screen-title {
  582. font-size: 32px;
  583. }
  584. .panel-title-bar {
  585. padding: 12px 20px;
  586. }
  587. .title-main {
  588. font-size: 18px;
  589. }
  590. .data-table {
  591. thead th {
  592. font-size: 12px;
  593. padding: 9px 5px;
  594. }
  595. tbody td {
  596. font-size: 14px;
  597. padding: 8px 12px;
  598. }
  599. }
  600. }
  601. </style>