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

<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>