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.

340 lines
18 KiB

2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
2 days ago
  1. <template>
  2. <div class="haian-sorting-board">
  3. <!-- 海安独立大屏固定分拣位工单来自标签预留完成状态来自WMS不展示原/目标托盘 - rqrq -->
  4. <header class="board-header">
  5. <img src="~@/assets/img/cclbai.png" alt="CCL" class="board-logo">
  6. <div class="board-title">
  7. <h1>{{ boardTitle }}
  8. <!-- 连接灯与业务状态分开绿灯已连接红灯断线闪烁黄灯建连中灰灯停用 - rqrq -->
  9. <span :class="['ws-status-dot', wsState]" :title="connectionText" role="status" :aria-label="connectionText"></span>
  10. </h1>
  11. <!-- <span>分拣位 {{ sortingStation }} · 工厂 {{ site || '未设置' }}</span>-->
  12. </div>
  13. <time>{{ currentTime }}</time>
  14. </header>
  15. <section class="board-summary">
  16. <div class="summary-counts">
  17. <span>标签 <strong>{{ visibleRows.length }}</strong></span>
  18. <span>未完成 <strong class="pending-text">{{ pendingCount }}</strong></span>
  19. <span>已完成 <strong class="completed-text">{{ completedCount }}</strong></span>
  20. </div>
  21. <!-- <div class="summary-refresh">-->
  22. <!-- <span class="connection-text">{{ connectionText }}</span>-->
  23. <!-- 没有可展示任务时隐藏业务提示避免将分拣位未返回误显示成异常 - rqrq -->
  24. <!-- <span v-if="visibleRows.length > 0" role="status">{{ displayMessage }}</span>-->
  25. <!-- <el-button size="small" :loading="queryLoading" :disabled="queryLoading || !siteReady || !pushEnabled" @click="fetchData(true)">刷新</el-button>-->
  26. <!-- </div>-->
  27. </section>
  28. <!-- 固定高度容器配合粘性表头数据返回后按实际溢出滚动鼠标进入暂停便于核对 - rqrq -->
  29. <div ref="tableViewport" class="table-viewport" @mouseenter="scrollPaused = true" @mouseleave="scrollPaused = false">
  30. <table class="board-table">
  31. <colgroup>
  32. <col style="width: 4%">
  33. <col style="width: 13%">
  34. <col style="width: 12%">
  35. <col style="width: 29%">
  36. <col style="width: 19%">
  37. <col style="width: 8%">
  38. <col style="width: 15%">
  39. </colgroup>
  40. <thead>
  41. <!-- 六屏仅展示标签物料编码移除产品编码列后将空间分配给物料名称 - rqrq -->
  42. <tr><th>序号</th><th>工单号</th><th>物料编码</th><th>物料名称</th><th>RFID / 标签号</th><th>状态</th><th>说明</th></tr>
  43. </thead>
  44. <tbody>
  45. <tr v-for="(item, index) in visibleRows" :key="item.rfidBarcode">
  46. <td>{{ index + 1 }}</td>
  47. <td :title="item.orderNo">{{ item.orderNo || '-' }}</td>
  48. <!-- partNo来自标签表handling_unit.part_no名称及悬浮提示最多取前100位空值显示横线 - rqrq -->
  49. <td :title="item.partNo">{{ item.partNo || '-' }}</td>
  50. <td class="description" :title="(item.partDesc || '').slice(0, 100)">{{ (item.partDesc || '').slice(0, 100) || '-' }}</td>
  51. <td class="barcode" :title="item.rfidBarcode">{{ item.rfidBarcode }}</td>
  52. <td><span :class="['status-badge', item.status === '已完成' ? 'completed' : 'pending']">{{ item.status }}</span></td>
  53. <td class="row-message" :title="item.message">{{ item.message || '-' }}</td>
  54. </tr>
  55. <!-- 未返回任务时表体留白不展示空数据或异常提示连接状态由标题指示灯表示 - rqrq -->
  56. </tbody>
  57. </table>
  58. </div>
  59. <footer class="board-footer">
  60. <!-- <span>{{ pushEnabled ? 'WebSocket 实时推送 · 每轮间隔 5 秒' : '海安看板未启用' }}</span>-->
  61. <span>数据更新时间{{ updatedTime }}</span>
  62. </footer>
  63. </div>
  64. </template>
  65. <script>
  66. import dayjs from 'dayjs'
  67. import HaianBoardSocket from '@/utils/haianBoardSocket'
  68. import { getSortingBoardConfig } from '@/api/haianWarehouse/sortingBoardConfig'
  69. import { getAutoJ2SortingBoard } from '@/api/haianWarehouse/autoJ2SortingBoard'
  70. // 自动J2H独立页面,布局、字段和请求逻辑在本文件维护;不包装人工或另一自动看板 - rqrq
  71. export default {
  72. name: 'HaianAutoJ2SortingBoard54',
  73. data () {
  74. return {
  75. // 本页面固定J2H,不接收其他位置参数,防止自动看板串屏 - rqrq
  76. sortingStation: 'J2H',
  77. boardTitle: '自动分拣 2',
  78. site: '',
  79. siteReady: false,
  80. rows: [],
  81. available: false,
  82. message: '正在获取分拣数据…',
  83. updatedAt: 0,
  84. lastReceivedAt: 0,
  85. now: Date.now(),
  86. queryLoading: false,
  87. configLoading: false,
  88. pushEnabled: false,
  89. wsState: 'disabled',
  90. dataVersion: 0,
  91. refreshTimer: null,
  92. clockTimer: null,
  93. scrollTimer: null,
  94. scrollPaused: false,
  95. scrollResumeAt: 0,
  96. requestVersion: 0
  97. }
  98. },
  99. computed: {
  100. // 时钟与数据刷新独立,超过20秒未收到有效响应即标记过期,避免断网仍显示正常任务 - rqrq
  101. currentTime () { return dayjs(this.now).format('YYYY-MM-DD HH:mm:ss') },
  102. updatedTime () { return this.updatedAt ? dayjs(this.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-' },
  103. stale () { return this.lastReceivedAt > 0 && this.now - this.lastReceivedAt > 20000 },
  104. visibleRows () { return this.available && !this.stale ? this.rows : [] },
  105. pendingCount () { return this.visibleRows.filter(item => item.status === '未完成').length },
  106. completedCount () { return this.visibleRows.filter(item => item.status === '已完成').length },
  107. // 绿灯只代表STOMP连接正常,WCS/WMS查询异常通过旁边业务提示显示,不混淆连接与任务状态 - rqrq
  108. connectionText () { return { connected: '连接正常', disconnected: '连接断开,等待重连', connecting: '正在连接', disabled: '推送已停用' }[this.wsState] },
  109. displayMessage () { return this.stale ? '数据更新超时,正在重试…' : this.message }
  110. },
  111. watch: {
  112. // 地址参数变化时清空旧数据并递增请求版本,迟到响应不能覆盖新请求 - rqrq
  113. '$route.fullPath' () { this.initializeBoard() }
  114. },
  115. mounted () {
  116. // 海安专用屏先初始化固定工厂54再查询,直接访问无需登录或补充地址参数 - rqrq
  117. this.initializeBoard()
  118. this.clockTimer = setInterval(() => { this.now = Date.now() }, 1000)
  119. // 每30秒只复查配置,关闭时不查业务数据;配置重启生效后页面自动建立或关闭连接 - rqrq
  120. this.refreshTimer = setInterval(() => { this.checkBoardConfig() }, 30000)
  121. this.scrollTimer = setInterval(this.scrollTable, 60)
  122. },
  123. beforeDestroy () {
  124. // 页面离开时废弃在途响应并清理全部定时器,防止卸载后持续请求和修改界面 - rqrq
  125. this.requestVersion++
  126. this.disconnectWebSocket()
  127. clearInterval(this.refreshTimer)
  128. clearInterval(this.clockTimer)
  129. clearInterval(this.scrollTimer)
  130. },
  131. methods: {
  132. // 专用屏独立使用工厂54,不读取或修改登录工厂;兼容site=54地址,错误工厂参数仍拒绝查询 - rqrq
  133. initializeBoard () {
  134. this.requestVersion++
  135. this.disconnectWebSocket()
  136. this.pushEnabled = false
  137. this.wsState = 'disabled'
  138. this.configLoading = false
  139. this.dataVersion = 0
  140. this.queryLoading = false
  141. this.rows = []
  142. this.available = false
  143. this.updatedAt = 0
  144. this.lastReceivedAt = 0
  145. this.siteReady = false
  146. this.site = '54'
  147. const querySite = this.$route.query.site
  148. if (querySite !== undefined && (typeof querySite !== 'string' || querySite.trim() !== '54')) {
  149. this.message = '海安看板的工厂参数必须为 site=54'
  150. return
  151. }
  152. this.siteReady = true
  153. this.message = '正在获取分拣数据…'
  154. this.$nextTick(() => {
  155. if (this.$refs.tableViewport) this.$refs.tableViewport.scrollTop = 0
  156. this.checkBoardConfig()
  157. })
  158. },
  159. // 只探测配置,不查询业务库;后端严格true才允许连接,配置失败清空旧屏并等待下轮重试 - rqrq
  160. async checkBoardConfig () {
  161. if (!this.siteReady || this.configLoading) return
  162. const version = this.requestVersion
  163. this.configLoading = true
  164. try {
  165. // 显式传本屏工厂54读取开关,避免未登录或登录其他工厂时查错配置 - rqrq
  166. const { data } = await getSortingBoardConfig({ site: this.site })
  167. if (version !== this.requestVersion) return
  168. if (!data || data.code !== 0 || !data.row || data.row.site !== this.site) throw new Error('配置查询失败')
  169. this.pushEnabled = data.row.enabled === true
  170. if (!this.pushEnabled) {
  171. this.disconnectWebSocket()
  172. this.wsState = 'disabled'
  173. this.available = false
  174. this.rows = []
  175. this.updatedAt = 0
  176. this.lastReceivedAt = 0
  177. this.message = '海安看板已停用'
  178. return
  179. }
  180. if (!this._boardSocket) this.initWebSocket()
  181. } catch (error) {
  182. if (version !== this.requestVersion) return
  183. this.pushEnabled = false
  184. this.disconnectWebSocket()
  185. this.wsState = 'disconnected'
  186. this.available = false
  187. this.rows = []
  188. this.lastReceivedAt = 0
  189. this.message = '看板配置连接异常,正在重试…'
  190. } finally {
  191. if (version === this.requestVersion) this.configLoading = false
  192. }
  193. },
  194. // 每屏独立订阅显式工厂与位置,重连由纯传输工具处理;自动两屏业务消费逻辑各自在本文件维护 - rqrq
  195. initWebSocket () {
  196. if (!this.siteReady || !this.pushEnabled || this._boardSocket) return
  197. const version = this.requestVersion
  198. const apiServer = process.env.NODE_ENV !== 'production' && process.env.OPEN_PROXY ? '/proxyApi/' : window.SITE_CONFIG.baseUrl
  199. const url = apiServer.replace(/\/+$/, '') + '/ws/dashboard'
  200. const topic = '/topic/dashboard/haian/sorting/' + this.site + '/' + this.sortingStation
  201. this._boardSocket = new HaianBoardSocket(url, topic, data => {
  202. if (version !== this.requestVersion || !this.pushEnabled) return
  203. this.dataVersion++
  204. this.applyBoard(data)
  205. }, state => {
  206. if (version !== this.requestVersion || !this.pushEnabled) return
  207. // 连接代次变化使之前发出的HTTP失效,避免断线后迟到响应重新展示旧屏 - rqrq
  208. this.dataVersion++
  209. this.wsState = state
  210. if (state === 'connected') {
  211. this.lastReceivedAt = Date.now()
  212. this.message = '连接正常,等待分拣数据…'
  213. } else {
  214. this.available = false
  215. this.rows = []
  216. this.updatedAt = 0
  217. this.lastReceivedAt = 0
  218. this.message = state === 'connecting' ? '正在连接看板服务…' : '连接已断开,正在自动重连…'
  219. }
  220. })
  221. this._boardSocket.start()
  222. },
  223. // 停用和卸载彻底清理连接及重试;先清空句柄,旧回调受requestVersion保护 - rqrq
  224. disconnectWebSocket () {
  225. this.dataVersion++
  226. const socket = this._boardSocket
  227. this._boardSocket = null
  228. if (socket) socket.stop()
  229. },
  230. // HTTP手动刷新与推送按相同结构消费,完整核对site/位置;异常与空任务分开显示 - rqrq
  231. applyBoard (data) {
  232. if (!this.pushEnabled) return
  233. // 响应必须属于本屏固定工厂及分拣位,登录工厂变化不影响独立屏 - rqrq
  234. if (!data || data.code !== 0 || !data.row || data.row.site !== this.site || data.row.sortingStation !== this.sortingStation) {
  235. this.available = false
  236. this.rows = []
  237. this.message = '看板数据校验失败,请检查工厂和分拣位'
  238. throw new Error(this.message)
  239. }
  240. const board = data.row
  241. this.lastReceivedAt = Date.now()
  242. this.available = board.available === true
  243. this.rows = this.available && Array.isArray(board.rows) ? board.rows : []
  244. this.updatedAt = Number(board.updatedAt) || 0
  245. this.message = board.message || (this.rows.length ? '数据正常' : '当前分拣位暂无任务')
  246. },
  247. // 仅手动刷新使用独立HTTP接口,后端也检查开关;在途HTTP不能覆盖期间收到的新推送,finally恢复按钮 - rqrq
  248. async fetchData (manual = false) {
  249. if (!this.siteReady || !this.pushEnabled || this.queryLoading) return
  250. const version = this.requestVersion
  251. const dataVersion = this.dataVersion
  252. const station = this.sortingStation
  253. // 与配置查询及WebSocket主题使用同一本屏工厂,刷新不依赖登录态 - rqrq
  254. const site = this.site
  255. this.queryLoading = true
  256. try {
  257. const { data } = await getAutoJ2SortingBoard({ site, sortingStation: station })
  258. if (version !== this.requestVersion || !this.pushEnabled || dataVersion !== this.dataVersion) return
  259. if (!data || data.code !== 0 || !data.row) {
  260. this.rows = []
  261. this.available = false
  262. this.message = (data && data.msg) || '看板查询失败'
  263. if (manual) this.$alert(this.message, '查询失败', { confirmButtonText: '确定' }).catch(() => {})
  264. return
  265. }
  266. this.applyBoard(data)
  267. if (manual && !this.available) this.$alert(this.message, '数据不可用', { confirmButtonText: '确定' }).catch(() => {})
  268. } catch (error) {
  269. if (version !== this.requestVersion || !this.pushEnabled || dataVersion !== this.dataVersion) return
  270. this.rows = []
  271. this.available = false
  272. this.message = '手动刷新失败,等待推送恢复…'
  273. if (manual) this.$message.error('查询海安分拣看板失败,请检查网络或服务')
  274. } finally {
  275. if (version === this.requestVersion) this.queryLoading = false
  276. }
  277. },
  278. // 无论首次数据何时到达都按实际滚动高度启动;底部停留2秒后回到顶部,少量数据不滚动 - rqrq
  279. scrollTable () {
  280. const viewport = this.$refs.tableViewport
  281. if (!viewport || this.scrollPaused || !this.visibleRows.length || Date.now() < this.scrollResumeAt) return
  282. if (viewport.scrollHeight <= viewport.clientHeight) return
  283. if (viewport.scrollTop + viewport.clientHeight >= viewport.scrollHeight - 1) {
  284. viewport.scrollTop = 0
  285. this.scrollResumeAt = Date.now() + 2000
  286. } else {
  287. viewport.scrollTop += 1
  288. }
  289. }
  290. }
  291. }
  292. </script>
  293. <style scoped>
  294. /* 沿用老分拣看板蓝色渐变、CCL白色Logo、青绿色表头和连接灯;海安布局独立维护,长文本可换行 - rqrq */
  295. .haian-sorting-board { height: 100vh; width: 100%; box-sizing: border-box; padding: 18px 24px 12px; background: linear-gradient(135deg, #5f8cc3 0%, #749cc8 100%); color: #f4f7fb; font-family: 'Microsoft YaHei', sans-serif; display: flex; flex-direction: column; overflow: hidden; }
  296. .board-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; min-height: 82px; border-bottom: 2px solid rgba(23, 179, 163, 0.4); background: linear-gradient(180deg, rgba(23, 179, 163, 0.08), transparent); }
  297. .board-logo { width: auto; height: 40px; filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3)); }
  298. .board-title { flex: 1; text-align: center; }
  299. .board-title h1 { margin: 0 0 8px; font-size: 32px; letter-spacing: 3px; text-shadow: 0 0 20px rgba(23, 179, 163, 0.5); }
  300. .board-title span { color: #d2e1ee; font-size: 16px; }
  301. .board-header time { font-size: 18px; font-variant-numeric: tabular-nums; }
  302. .board-summary { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; padding: 18px 0; }
  303. .summary-counts, .summary-refresh { display: flex; align-items: center; gap: 20px; }
  304. .summary-counts strong { font-size: 26px; margin-left: 8px; }
  305. .pending-text { color: #ffd17d; }
  306. .completed-text { color: #80e5bc; }
  307. .ws-status-dot { display: inline-block; width: 12px; height: 12px; margin-left: 12px; border-radius: 50%; vertical-align: middle; background: #94a3b8; }
  308. .ws-status-dot.connected { background: #22c55e; box-shadow: 0 0 10px rgba(34, 197, 94, 0.8); }
  309. .ws-status-dot.disconnected { background: #ef4444; box-shadow: 0 0 12px rgba(239, 68, 68, 0.85); animation: ws-status-blink 1s steps(1, end) infinite; }
  310. .ws-status-dot.connecting { background: #f59e0b; box-shadow: 0 0 10px rgba(245, 158, 11, 0.7); }
  311. @keyframes ws-status-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0.2; } }
  312. .summary-refresh { font-size: 14px; gap: 10px; }
  313. .table-viewport { flex: 1; min-height: 0; overflow-y: auto; background: rgba(70, 90, 120, 0.9); border: 1px solid #637e9e; border-radius: 6px; }
  314. .board-table { width: 100%; table-layout: fixed; border-spacing: 0; }
  315. .board-table th { position: sticky; top: 0; z-index: 1; background: #1b6676; color: #fff; font-size: 18px; padding: 16px 8px; }
  316. .board-table td { padding: 18px 8px; font-size: 18px; text-align: center; border-bottom: 1px solid #526b87; overflow-wrap: break-word; word-wrap: break-word; }
  317. .board-table tbody tr:nth-child(even) { background: #344b66; }
  318. .board-table .description { text-align: left; }
  319. .board-table .barcode { font-family: Consolas, 'Microsoft YaHei', monospace; }
  320. .board-table .row-message { color: #ffd17d; font-size: 14px; }
  321. .status-badge { display: inline-block; padding: 5px 10px; border-radius: 4px; white-space: nowrap; font-size: 16px; }
  322. .status-badge.pending { background: #745827; color: #fff0cf; }
  323. .status-badge.completed { background: #21684e; color: #d4ffeb; }
  324. .board-table .empty-message { height: 220px; font-size: 24px; color: #d2e1ee; }
  325. .board-table .empty-message.is-error { color: #ffd17d; }
  326. .board-footer { display: flex; justify-content: space-between; padding-top: 12px; color: #d2e1ee; font-size: 14px; }
  327. @media screen and (max-width: 1280px) {
  328. .haian-sorting-board { padding: 12px; }
  329. .board-title h1 { font-size: 26px; }
  330. .board-header time { font-size: 14px; }
  331. .board-table th, .board-table td { font-size: 14px; padding: 12px 5px; }
  332. .status-badge { padding: 4px; font-size: 13px; }
  333. }
  334. </style>