|
|
<template> <div class="haian-sorting-board"> <!-- 海安独立大屏:固定分拣位,工单来自标签预留,完成状态来自WMS;不展示原/目标托盘 - rqrq --> <header class="board-header"> <img src="~@/assets/img/cclbai.png" alt="CCL" class="board-logo"> <div class="board-title"> <h1>{{ boardTitle }} <!-- 连接灯与业务状态分开:绿灯已连接,红灯断线闪烁,黄灯建连中,灰灯停用 - rqrq --> <span :class="['ws-status-dot', wsState]" :title="connectionText" role="status" :aria-label="connectionText"></span> </h1><!-- <span>分拣位 {{ sortingStation }} · 工厂 {{ site || '未设置' }}</span>--> </div> <time>{{ currentTime }}</time> </header>
<section class="board-summary"> <div class="summary-counts"> <span>标签 <strong>{{ visibleRows.length }}</strong></span> <span>未完成 <strong class="pending-text">{{ pendingCount }}</strong></span> <span>已完成 <strong class="completed-text">{{ completedCount }}</strong></span> </div><!-- <div class="summary-refresh">--><!-- <span class="connection-text">{{ connectionText }}</span>--> <!-- 没有可展示任务时隐藏业务提示,避免将分拣位未返回误显示成异常 - rqrq --><!-- <span v-if="visibleRows.length > 0" role="status">{{ displayMessage }}</span>--><!-- <el-button size="small" :loading="queryLoading" :disabled="queryLoading || !siteReady || !pushEnabled" @click="fetchData(true)">刷新</el-button>--><!-- </div>--> </section>
<!-- 固定高度容器配合粘性表头;数据返回后按实际溢出滚动,鼠标进入暂停便于核对 - rqrq --> <div ref="tableViewport" class="table-viewport" @mouseenter="scrollPaused = true" @mouseleave="scrollPaused = false"> <table class="board-table"> <colgroup> <col style="width: 4%"> <col style="width: 13%"> <col style="width: 12%"> <col style="width: 29%"> <col style="width: 19%"> <col style="width: 8%"> <col style="width: 15%"> </colgroup> <thead> <!-- 六屏仅展示标签物料编码,移除产品编码列后将空间分配给物料名称 - rqrq --> <tr><th>序号</th><th>工单号</th><th>物料编码</th><th>物料名称</th><th>RFID / 标签号</th><th>状态</th><th>说明</th></tr> </thead> <tbody> <tr v-for="(item, index) in visibleRows" :key="item.rfidBarcode"> <td>{{ index + 1 }}</td> <td :title="item.orderNo">{{ item.orderNo || '-' }}</td> <!-- partNo来自标签表handling_unit.part_no;名称及悬浮提示最多取前100位,空值显示横线 - rqrq --> <td :title="item.partNo">{{ item.partNo || '-' }}</td> <td class="description" :title="(item.partDesc || '').slice(0, 100)">{{ (item.partDesc || '').slice(0, 100) || '-' }}</td> <td class="barcode" :title="item.rfidBarcode">{{ item.rfidBarcode }}</td> <td><span :class="['status-badge', item.status === '已完成' ? 'completed' : 'pending']">{{ item.status }}</span></td> <td class="row-message" :title="item.message">{{ item.message || '-' }}</td> </tr> <!-- 未返回任务时表体留白,不展示空数据或异常提示;连接状态由标题指示灯表示 - rqrq --> </tbody> </table> </div>
<footer class="board-footer"><!-- <span>{{ pushEnabled ? 'WebSocket 实时推送 · 每轮间隔 5 秒' : '海安看板未启用' }}</span>--> <span>数据更新时间:{{ updatedTime }}</span> </footer> </div></template>
<script>import dayjs from 'dayjs'import HaianBoardSocket from '@/utils/haianBoardSocket'import { getSortingBoardConfig } from '@/api/haianWarehouse/sortingBoardConfig'import { getAutoJ2SortingBoard } from '@/api/haianWarehouse/autoJ2SortingBoard'
// 自动J2H独立页面,布局、字段和请求逻辑在本文件维护;不包装人工或另一自动看板 - rqrq
export default { name: 'HaianAutoJ2SortingBoard54', data () { return { // 本页面固定J2H,不接收其他位置参数,防止自动看板串屏 - rqrq
sortingStation: 'J2H', boardTitle: '自动分拣 2', site: '', siteReady: false, rows: [], available: false, message: '正在获取分拣数据…', updatedAt: 0, lastReceivedAt: 0, now: Date.now(), queryLoading: false, configLoading: false, pushEnabled: false, wsState: 'disabled', dataVersion: 0, refreshTimer: null, clockTimer: null, scrollTimer: null, scrollPaused: false, scrollResumeAt: 0, requestVersion: 0 } }, computed: { // 时钟与数据刷新独立,超过20秒未收到有效响应即标记过期,避免断网仍显示正常任务 - rqrq
currentTime () { return dayjs(this.now).format('YYYY-MM-DD HH:mm:ss') }, updatedTime () { return this.updatedAt ? dayjs(this.updatedAt).format('YYYY-MM-DD HH:mm:ss') : '-' }, stale () { return this.lastReceivedAt > 0 && this.now - this.lastReceivedAt > 20000 }, visibleRows () { return this.available && !this.stale ? this.rows : [] }, pendingCount () { return this.visibleRows.filter(item => item.status === '未完成').length }, completedCount () { return this.visibleRows.filter(item => item.status === '已完成').length }, // 绿灯只代表STOMP连接正常,WCS/WMS查询异常通过旁边业务提示显示,不混淆连接与任务状态 - rqrq
connectionText () { return { connected: '连接正常', disconnected: '连接断开,等待重连', connecting: '正在连接', disabled: '推送已停用' }[this.wsState] }, displayMessage () { return this.stale ? '数据更新超时,正在重试…' : this.message } }, watch: { // 地址参数变化时清空旧数据并递增请求版本,迟到响应不能覆盖新请求 - rqrq
'$route.fullPath' () { this.initializeBoard() } }, mounted () { // 海安专用屏先初始化固定工厂54再查询,直接访问无需登录或补充地址参数 - rqrq
this.initializeBoard() this.clockTimer = setInterval(() => { this.now = Date.now() }, 1000) // 每30秒只复查配置,关闭时不查业务数据;配置重启生效后页面自动建立或关闭连接 - rqrq
this.refreshTimer = setInterval(() => { this.checkBoardConfig() }, 30000) this.scrollTimer = setInterval(this.scrollTable, 60) }, beforeDestroy () { // 页面离开时废弃在途响应并清理全部定时器,防止卸载后持续请求和修改界面 - rqrq
this.requestVersion++ this.disconnectWebSocket() clearInterval(this.refreshTimer) clearInterval(this.clockTimer) clearInterval(this.scrollTimer) }, methods: { // 专用屏独立使用工厂54,不读取或修改登录工厂;兼容site=54地址,错误工厂参数仍拒绝查询 - rqrq
initializeBoard () { this.requestVersion++ this.disconnectWebSocket() this.pushEnabled = false this.wsState = 'disabled' this.configLoading = false this.dataVersion = 0 this.queryLoading = false this.rows = [] this.available = false this.updatedAt = 0 this.lastReceivedAt = 0 this.siteReady = false this.site = '54' const querySite = this.$route.query.site if (querySite !== undefined && (typeof querySite !== 'string' || querySite.trim() !== '54')) { this.message = '海安看板的工厂参数必须为 site=54' return } this.siteReady = true this.message = '正在获取分拣数据…' this.$nextTick(() => { if (this.$refs.tableViewport) this.$refs.tableViewport.scrollTop = 0 this.checkBoardConfig() }) }, // 只探测配置,不查询业务库;后端严格true才允许连接,配置失败清空旧屏并等待下轮重试 - rqrq
async checkBoardConfig () { if (!this.siteReady || this.configLoading) return const version = this.requestVersion this.configLoading = true try { // 显式传本屏工厂54读取开关,避免未登录或登录其他工厂时查错配置 - rqrq
const { data } = await getSortingBoardConfig({ site: this.site }) if (version !== this.requestVersion) return if (!data || data.code !== 0 || !data.row || data.row.site !== this.site) throw new Error('配置查询失败') this.pushEnabled = data.row.enabled === true if (!this.pushEnabled) { this.disconnectWebSocket() this.wsState = 'disabled' this.available = false this.rows = [] this.updatedAt = 0 this.lastReceivedAt = 0 this.message = '海安看板已停用' return } if (!this._boardSocket) this.initWebSocket() } catch (error) { if (version !== this.requestVersion) return this.pushEnabled = false this.disconnectWebSocket() this.wsState = 'disconnected' this.available = false this.rows = [] this.lastReceivedAt = 0 this.message = '看板配置连接异常,正在重试…' } finally { if (version === this.requestVersion) this.configLoading = false } }, // 每屏独立订阅显式工厂与位置,重连由纯传输工具处理;自动两屏业务消费逻辑各自在本文件维护 - rqrq
initWebSocket () { if (!this.siteReady || !this.pushEnabled || this._boardSocket) return const version = this.requestVersion const apiServer = process.env.NODE_ENV !== 'production' && process.env.OPEN_PROXY ? '/proxyApi/' : window.SITE_CONFIG.baseUrl const url = apiServer.replace(/\/+$/, '') + '/ws/dashboard' const topic = '/topic/dashboard/haian/sorting/' + this.site + '/' + this.sortingStation this._boardSocket = new HaianBoardSocket(url, topic, data => { if (version !== this.requestVersion || !this.pushEnabled) return this.dataVersion++ this.applyBoard(data) }, state => { if (version !== this.requestVersion || !this.pushEnabled) return // 连接代次变化使之前发出的HTTP失效,避免断线后迟到响应重新展示旧屏 - rqrq
this.dataVersion++ this.wsState = state if (state === 'connected') { this.lastReceivedAt = Date.now() this.message = '连接正常,等待分拣数据…' } else { this.available = false this.rows = [] this.updatedAt = 0 this.lastReceivedAt = 0 this.message = state === 'connecting' ? '正在连接看板服务…' : '连接已断开,正在自动重连…' } }) this._boardSocket.start() }, // 停用和卸载彻底清理连接及重试;先清空句柄,旧回调受requestVersion保护 - rqrq
disconnectWebSocket () { this.dataVersion++ const socket = this._boardSocket this._boardSocket = null if (socket) socket.stop() }, // HTTP手动刷新与推送按相同结构消费,完整核对site/位置;异常与空任务分开显示 - rqrq
applyBoard (data) { if (!this.pushEnabled) return // 响应必须属于本屏固定工厂及分拣位,登录工厂变化不影响独立屏 - rqrq
if (!data || data.code !== 0 || !data.row || data.row.site !== this.site || data.row.sortingStation !== this.sortingStation) { this.available = false this.rows = [] this.message = '看板数据校验失败,请检查工厂和分拣位' throw new Error(this.message) } const board = data.row this.lastReceivedAt = Date.now() this.available = board.available === true this.rows = this.available && Array.isArray(board.rows) ? board.rows : [] this.updatedAt = Number(board.updatedAt) || 0 this.message = board.message || (this.rows.length ? '数据正常' : '当前分拣位暂无任务') }, // 仅手动刷新使用独立HTTP接口,后端也检查开关;在途HTTP不能覆盖期间收到的新推送,finally恢复按钮 - rqrq
async fetchData (manual = false) { if (!this.siteReady || !this.pushEnabled || this.queryLoading) return const version = this.requestVersion const dataVersion = this.dataVersion const station = this.sortingStation // 与配置查询及WebSocket主题使用同一本屏工厂,刷新不依赖登录态 - rqrq
const site = this.site this.queryLoading = true try { const { data } = await getAutoJ2SortingBoard({ site, sortingStation: station }) if (version !== this.requestVersion || !this.pushEnabled || dataVersion !== this.dataVersion) return if (!data || data.code !== 0 || !data.row) { this.rows = [] this.available = false this.message = (data && data.msg) || '看板查询失败' if (manual) this.$alert(this.message, '查询失败', { confirmButtonText: '确定' }).catch(() => {}) return } this.applyBoard(data) if (manual && !this.available) this.$alert(this.message, '数据不可用', { confirmButtonText: '确定' }).catch(() => {}) } catch (error) { if (version !== this.requestVersion || !this.pushEnabled || dataVersion !== this.dataVersion) return this.rows = [] this.available = false this.message = '手动刷新失败,等待推送恢复…' if (manual) this.$message.error('查询海安分拣看板失败,请检查网络或服务') } finally { if (version === this.requestVersion) this.queryLoading = false } }, // 无论首次数据何时到达都按实际滚动高度启动;底部停留2秒后回到顶部,少量数据不滚动 - rqrq
scrollTable () { const viewport = this.$refs.tableViewport if (!viewport || this.scrollPaused || !this.visibleRows.length || Date.now() < this.scrollResumeAt) return if (viewport.scrollHeight <= viewport.clientHeight) return if (viewport.scrollTop + viewport.clientHeight >= viewport.scrollHeight - 1) { viewport.scrollTop = 0 this.scrollResumeAt = Date.now() + 2000 } else { viewport.scrollTop += 1 } } }}</script>
<style scoped>/* 沿用老分拣看板蓝色渐变、CCL白色Logo、青绿色表头和连接灯;海安布局独立维护,长文本可换行 - rqrq */.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; }.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); }.board-logo { width: auto; height: 40px; filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3)); }.board-title { flex: 1; text-align: center; }.board-title h1 { margin: 0 0 8px; font-size: 32px; letter-spacing: 3px; text-shadow: 0 0 20px rgba(23, 179, 163, 0.5); }.board-title span { color: #d2e1ee; font-size: 16px; }.board-header time { font-size: 18px; font-variant-numeric: tabular-nums; }.board-summary { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; padding: 18px 0; }.summary-counts, .summary-refresh { display: flex; align-items: center; gap: 20px; }.summary-counts strong { font-size: 26px; margin-left: 8px; }.pending-text { color: #ffd17d; }.completed-text { color: #80e5bc; }.ws-status-dot { display: inline-block; width: 12px; height: 12px; margin-left: 12px; border-radius: 50%; vertical-align: middle; background: #94a3b8; }.ws-status-dot.connected { background: #22c55e; box-shadow: 0 0 10px rgba(34, 197, 94, 0.8); }.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; }.ws-status-dot.connecting { background: #f59e0b; box-shadow: 0 0 10px rgba(245, 158, 11, 0.7); }@keyframes ws-status-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0.2; } }.summary-refresh { font-size: 14px; gap: 10px; }.table-viewport { flex: 1; min-height: 0; overflow-y: auto; background: rgba(70, 90, 120, 0.9); border: 1px solid #637e9e; border-radius: 6px; }.board-table { width: 100%; table-layout: fixed; border-spacing: 0; }.board-table th { position: sticky; top: 0; z-index: 1; background: #1b6676; color: #fff; font-size: 18px; padding: 16px 8px; }.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; }.board-table tbody tr:nth-child(even) { background: #344b66; }.board-table .description { text-align: left; }.board-table .barcode { font-family: Consolas, 'Microsoft YaHei', monospace; }.board-table .row-message { color: #ffd17d; font-size: 14px; }.status-badge { display: inline-block; padding: 5px 10px; border-radius: 4px; white-space: nowrap; font-size: 16px; }.status-badge.pending { background: #745827; color: #fff0cf; }.status-badge.completed { background: #21684e; color: #d4ffeb; }.board-table .empty-message { height: 220px; font-size: 24px; color: #d2e1ee; }.board-table .empty-message.is-error { color: #ffd17d; }.board-footer { display: flex; justify-content: space-between; padding-top: 12px; color: #d2e1ee; font-size: 14px; }@media screen and (max-width: 1280px) { .haian-sorting-board { padding: 12px; } .board-title h1 { font-size: 26px; } .board-header time { font-size: 14px; } .board-table th, .board-table td { font-size: 14px; padding: 12px 5px; } .status-badge { padding: 4px; font-size: 13px; }}</style>
|