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