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.

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