Browse Source

海安分拣看板

dev
常熟吴彦祖 1 day ago
parent
commit
3ac410a1c9
  1. 4
      src/api/haianWarehouse/autoJ1SortingBoard.js
  2. 4
      src/api/haianWarehouse/autoJ2SortingBoard.js
  3. 4
      src/api/haianWarehouse/manualSortingBoard.js
  4. 4
      src/api/haianWarehouse/sortingBoardConfig.js
  5. 6
      src/router/index.js
  6. 119
      src/utils/haianBoardSocket.js
  7. 353
      src/views/modules/haianWarehouse/dashboard/autoJ1SortingBoard54.vue
  8. 353
      src/views/modules/haianWarehouse/dashboard/autoJ2SortingBoard54.vue
  9. 182
      src/views/modules/haianWarehouse/dashboard/sortingBoard54.vue

4
src/api/haianWarehouse/autoJ1SortingBoard.js

@ -0,0 +1,4 @@
import { createAPI } from '@/utils/httpRequest.js'
// 自动J1H独立看板查询,显式传site及sortingStation;失败交由对应页面展示 - rqrq
export const getAutoJ1SortingBoard = data => createAPI('/api/dashboard/haian/sorting/getAutoJ1SortingBoard', 'post', data)

4
src/api/haianWarehouse/autoJ2SortingBoard.js

@ -0,0 +1,4 @@
import { createAPI } from '@/utils/httpRequest.js'
// 自动J2H独立看板查询,显式传site及sortingStation;失败交由对应页面展示 - rqrq
export const getAutoJ2SortingBoard = data => createAPI('/api/dashboard/haian/sorting/getAutoJ2SortingBoard', 'post', data)

4
src/api/haianWarehouse/manualSortingBoard.js

@ -0,0 +1,4 @@
import { createAPI } from '@/utils/httpRequest.js'
// 人工R1H-R4H共用看板查询,显式传site及sortingStation;失败交由对应页面展示 - rqrq
export const getManualSortingBoard = data => createAPI('/api/dashboard/haian/sorting/getManualSortingBoard', 'post', data)

4
src/api/haianWarehouse/sortingBoardConfig.js

@ -0,0 +1,4 @@
import { createAPI } from '@/utils/httpRequest.js'
// 海安看板启动前读取独立开关,显式传site;本接口不触发WCS/WMS业务查询 - rqrq
export const getSortingBoardConfig = data => createAPI('/api/dashboard/haian/sorting/getSortingBoardConfig', 'post', data)

6
src/router/index.js

@ -21,9 +21,9 @@ const _import = require('./import-' + process.env.NODE_ENV)
const globalRoutes = [
{ path: '/404', component: _import('common/404'), name: '404', meta: { title: '404未找到' } },
{ path: '/login', component: _import('common/login'), name: 'login', meta: { title: '登录' } },
// 海安六个看板独立入口共用海安展示页,固定分拣位防串屏;工厂由页面初始化并显式传给后端 - rqrq
{ path: '/dashboard-haian-sorting-j1h', component: _import('modules/haianWarehouse/dashboard/sortingBoard54'), name: 'haian-sorting-j1h', props: { sortingStation: 'J1H', boardTitle: '自动分拣 1' }, meta: { title: '海安自动分拣1' } },
{ path: '/dashboard-haian-sorting-j2h', component: _import('modules/haianWarehouse/dashboard/sortingBoard54'), name: 'haian-sorting-j2h', props: { sortingStation: 'J2H', boardTitle: '自动分拣 2' }, meta: { title: '海安自动分拣2' } },
// 海安人工四屏共用页面,自动J1H/J2H各自独立页面,固定分拣位防串屏;工厂由页面初始化并显式传给后端 - rqrq
{ path: '/dashboard-haian-sorting-j1h', component: _import('modules/haianWarehouse/dashboard/autoJ1SortingBoard54'), name: 'haian-sorting-j1h', meta: { title: '海安自动分拣1' } },
{ path: '/dashboard-haian-sorting-j2h', component: _import('modules/haianWarehouse/dashboard/autoJ2SortingBoard54'), name: 'haian-sorting-j2h', meta: { title: '海安自动分拣2' } },
{ path: '/dashboard-haian-sorting-r1h', component: _import('modules/haianWarehouse/dashboard/sortingBoard54'), name: 'haian-sorting-r1h', props: { sortingStation: 'R1H', boardTitle: '人工分拣 1' }, meta: { title: '海安人工分拣1' } },
{ path: '/dashboard-haian-sorting-r2h', component: _import('modules/haianWarehouse/dashboard/sortingBoard54'), name: 'haian-sorting-r2h', props: { sortingStation: 'R2H', boardTitle: '人工分拣 2' }, meta: { title: '海安人工分拣2' } },
{ path: '/dashboard-haian-sorting-r3h', component: _import('modules/haianWarehouse/dashboard/sortingBoard54'), name: 'haian-sorting-r3h', props: { sortingStation: 'R3H', boardTitle: '人工分拣 3' }, meta: { title: '海安人工分拣3' } },

119
src/utils/haianBoardSocket.js

@ -0,0 +1,119 @@
import SockJS from 'sockjs-client'
import Stomp from 'stompjs'
// 海安专用传输实例:仅管理连接/心跳/订阅,不包含看板字段与业务;每页独立实例防止互相断连 - rqrq
export default class HaianBoardSocket {
constructor (url, topic, onMessage, onState) {
this.url = url
this.topic = topic
this.onMessage = onMessage
this.onState = onState
this.client = null
this.socket = null
this.retryTimer = null
this.connectTimer = null
this.healthTimer = null
this.attempts = 0
this.generation = 0
this.stopped = true
}
// 主动开启仅执行一次;页面必须先取得enabled=true,工具本身不查询开关或数据库 - rqrq
start () {
if (!this.stopped) return
this.stopped = false
this.connect()
}
// 使用旧看板同一SockJS/STOMP端点,每次连接成功重新订阅;代次校验隔离旧连接迟到回调 - rqrq
connect () {
if (this.stopped) return
this.release()
const generation = this.generation
const current = () => !this.stopped && generation === this.generation
this.onState('connecting')
try {
const socket = new SockJS(this.url)
this.socket = socket
const client = Stomp.over(socket)
this.client = client
client.debug = null
client.heartbeat.outgoing = 10000
client.heartbeat.incoming = 10000
// 建连超时防止网络黑洞永久停在connecting;STOMP握手成功才显示绿灯 - rqrq
this.connectTimer = setTimeout(() => { if (current()) this.fail() }, 20000)
client.connect({}, () => {
if (!current()) return
clearTimeout(this.connectTimer)
this.connectTimer = null
try {
client.subscribe(this.topic, message => {
if (!current()) return
try {
this.onMessage(JSON.parse(message.body))
} catch (error) {
// 非法消息触发断连重订阅,避免绿灯掩盖持续无法消费的数据 - rqrq
this.fail()
}
})
this.attempts = 0
this.onState('connected')
// STOMP负责心跳超时关闭;额外检测浏览器离线和SockJS状态,最长5秒反映显式断网 - rqrq
this.healthTimer = setInterval(() => {
if (current() && (window.navigator.onLine === false || socket.readyState !== 1)) this.fail()
}, 5000)
} catch (error) {
if (current()) this.fail()
}
}, () => { if (current()) this.fail() })
} catch (error) {
if (current()) this.fail()
}
}
// 断线立即红灯,10/20/30…60秒无限重试;同一代异常只安排一次,恢复后重新从10秒计时 - rqrq
fail () {
if (this.stopped || this.retryTimer) return
this.release()
this.onState('disconnected')
const delay = Math.min(++this.attempts * 10000, 60000)
this.retryTimer = setTimeout(() => {
this.retryTimer = null
this.connect()
}, delay)
}
// 完整释放STOMP自身心跳及页面健康检测,关闭旧连接前先失效回调,防止关闭又触发重连 - rqrq
release () {
this.generation++
clearTimeout(this.connectTimer)
clearInterval(this.healthTimer)
this.connectTimer = null
this.healthTimer = null
const client = this.client
const socket = this.socket
this.client = null
this.socket = null
try {
if (client) client.disconnect(() => {})
} catch (error) {
// 连接尚未打开时DISCONNECT可能失败,仍清理SockJS;该失败不恢复业务连接 - rqrq
} finally {
// stompjs 2.3.3在发送DISCONNECT抛错时不会进入自身清理,补充释放内部心跳定时器 - rqrq
if (client) client._cleanUp()
}
if (socket) {
socket.onclose = null
socket.onerror = null
try { socket.close() } catch (error) { /* 已关闭连接无需重复处理 - rqrq */ }
}
}
// 页面销毁、切换工厂或配置关闭时取消全部重试,手动停用后绝不自动重连 - rqrq
stop () {
this.stopped = true
clearTimeout(this.retryTimer)
this.retryTimer = null
this.release()
}
}

353
src/views/modules/haianWarehouse/dashboard/autoJ1SortingBoard54.vue

@ -0,0 +1,353 @@
<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>
<span 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: 12%">
<col style="width: 17%">
<col style="width: 19%">
<col style="width: 8%">
<col style="width: 15%">
</colgroup>
<thead>
<tr><th>序号</th><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>
<td :title="item.orderPartNo">{{ item.orderPartNo || '-' }}</td>
<td :title="item.partNo">{{ item.partNo || '-' }}</td>
<td class="description" :title="item.partDesc">{{ item.partDesc || '-' }}</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>
<tr v-if="visibleRows.length === 0">
<td colspan="8" class="empty-message" :class="{ 'is-error': !available || stale }">{{ displayMessage }}</td>
</tr>
</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 { getAutoJ1SortingBoard } from '@/api/haianWarehouse/autoJ1SortingBoard'
// J1H - rqrq
export default {
name: 'HaianAutoJ1SortingBoard54',
data () {
return {
// J1H - rqrq
sortingStation: 'J1H',
boardTitle: '自动分拣 1',
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 },
// 绿STOMPWCS/WMS - rqrq
connectionText () { return { connected: '连接正常', disconnected: '连接断开,等待重连', connecting: '正在连接', disabled: '推送已停用' }[this.wsState] },
displayMessage () { return this.stale ? '数据更新超时,正在重试…' : this.message }
},
watch: {
// - rqrq
'$route.fullPath' () { this.initializeBoard() },
// - rqrq
'$store.state.user.site' () { this.initializeBoard() }
},
mounted () {
// site使 - 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: {
// URLsite=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 = ''
const storeSite = String(this.$store.state.user.site || '').trim()
const querySite = this.$route.query.site
if (querySite !== undefined && (typeof querySite !== 'string' || querySite.trim() !== '54')) {
this.message = '海安看板的工厂参数必须为 site=54'
return
}
const site = querySite === undefined ? storeSite : querySite.trim()
if (site !== '54' || (storeSite && storeSite !== '54')) {
this.message = '请使用海安工厂打开看板;独立屏地址需带 ?site=54'
return
}
this.$store.commit('user/updateSite', site)
this.site = site
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 {
const { data } = await getSortingBoardConfig({ site: this.$store.state.user.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()
},
// HTTPsite/ - rqrq
applyBoard (data) {
if (!this.pushEnabled) return
const site = String(this.$store.state.user.site || '').trim()
if (!data || data.code !== 0 || !data.row || site !== this.site || data.row.site !== 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 ? '数据正常' : '当前分拣位暂无任务')
},
// 使HTTPHTTPfinally - 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
const site = String(this.$store.state.user.site || '').trim()
if (site !== this.site) {
this.initializeBoard()
return
}
this.queryLoading = true
try {
const { data } = await getAutoJ1SortingBoard({ site: this.$store.state.user.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>

353
src/views/modules/haianWarehouse/dashboard/autoJ2SortingBoard54.vue

@ -0,0 +1,353 @@
<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>
<span 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: 12%">
<col style="width: 17%">
<col style="width: 19%">
<col style="width: 8%">
<col style="width: 15%">
</colgroup>
<thead>
<tr><th>序号</th><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>
<td :title="item.orderPartNo">{{ item.orderPartNo || '-' }}</td>
<td :title="item.partNo">{{ item.partNo || '-' }}</td>
<td class="description" :title="item.partDesc">{{ item.partDesc || '-' }}</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>
<tr v-if="visibleRows.length === 0">
<td colspan="8" class="empty-message" :class="{ 'is-error': !available || stale }">{{ displayMessage }}</td>
</tr>
</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 },
// 绿STOMPWCS/WMS - rqrq
connectionText () { return { connected: '连接正常', disconnected: '连接断开,等待重连', connecting: '正在连接', disabled: '推送已停用' }[this.wsState] },
displayMessage () { return this.stale ? '数据更新超时,正在重试…' : this.message }
},
watch: {
// - rqrq
'$route.fullPath' () { this.initializeBoard() },
// - rqrq
'$store.state.user.site' () { this.initializeBoard() }
},
mounted () {
// site使 - 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: {
// URLsite=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 = ''
const storeSite = String(this.$store.state.user.site || '').trim()
const querySite = this.$route.query.site
if (querySite !== undefined && (typeof querySite !== 'string' || querySite.trim() !== '54')) {
this.message = '海安看板的工厂参数必须为 site=54'
return
}
const site = querySite === undefined ? storeSite : querySite.trim()
if (site !== '54' || (storeSite && storeSite !== '54')) {
this.message = '请使用海安工厂打开看板;独立屏地址需带 ?site=54'
return
}
this.$store.commit('user/updateSite', site)
this.site = site
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 {
const { data } = await getSortingBoardConfig({ site: this.$store.state.user.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()
},
// HTTPsite/ - rqrq
applyBoard (data) {
if (!this.pushEnabled) return
const site = String(this.$store.state.user.site || '').trim()
if (!data || data.code !== 0 || !data.row || site !== this.site || data.row.site !== 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 ? '数据正常' : '当前分拣位暂无任务')
},
// 使HTTPHTTPfinally - 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
const site = String(this.$store.state.user.site || '').trim()
if (site !== this.site) {
this.initializeBoard()
return
}
this.queryLoading = true
try {
const { data } = await getAutoJ2SortingBoard({ site: this.$store.state.user.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>

182
src/views/modules/haianWarehouse/dashboard/sortingBoard54.vue

@ -4,7 +4,10 @@
<header class="board-header">
<img src="~@/assets/img/cclbai.png" alt="CCL" class="board-logo">
<div class="board-title">
<h1>海安 · {{ boardTitle }}</h1>
<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>
@ -17,9 +20,9 @@
<span>已完成 <strong class="completed-text">{{ completedCount }}</strong></span>
</div>
<div class="summary-refresh">
<span :class="['connection-dot', { healthy: available && !stale }]" aria-hidden="true"></span>
<span class="connection-text">{{ connectionText }}</span>
<span role="status">{{ displayMessage }}</span>
<el-button size="small" :loading="queryLoading" :disabled="queryLoading || !siteReady" @click="fetchData(true)">刷新</el-button>
<el-button size="small" :loading="queryLoading" :disabled="queryLoading || !siteReady || !pushEnabled" @click="fetchData(true)">刷新</el-button>
</div>
</section>
@ -58,7 +61,7 @@
</div>
<footer class="board-footer">
<span> 5 秒自动刷新</span>
<span>{{ pushEnabled ? 'WebSocket 实时推送 · 每轮间隔 5 秒' : '海安看板未启用' }}</span>
<span>数据更新时间{{ updatedTime }}</span>
</footer>
</div>
@ -66,9 +69,11 @@
<script>
import dayjs from 'dayjs'
import { getSortingBoard } from '@/api/haianWarehouse/sortingBoard'
import HaianBoardSocket from '@/utils/haianBoardSocket'
import { getSortingBoardConfig } from '@/api/haianWarehouse/sortingBoardConfig'
import { getManualSortingBoard } from '@/api/haianWarehouse/manualSortingBoard'
// 使props - rqrq
// R1H-R4Hprops - rqrq
export default {
name: 'HaianSortingBoard54',
props: {
@ -86,6 +91,10 @@ export default {
lastReceivedAt: 0,
now: Date.now(),
queryLoading: false,
configLoading: false,
pushEnabled: false,
wsState: 'disabled',
dataVersion: 0,
refreshTimer: null,
clockTimer: null,
scrollTimer: null,
@ -102,22 +111,28 @@ export default {
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 },
// 绿STOMPWCS/WMS - rqrq
connectionText () { return { connected: '连接正常', disconnected: '连接断开,等待重连', connecting: '正在连接', disabled: '推送已停用' }[this.wsState] },
displayMessage () { return this.stale ? '数据更新超时,正在重试…' : this.message }
},
watch: {
// - rqrq
'$route.fullPath' () { this.initializeBoard() }
// - rqrq
'$route.fullPath' () { this.initializeBoard() },
// - rqrq
'$store.state.user.site' () { this.initializeBoard() }
},
mounted () {
// site使 - rqrq
this.initializeBoard()
this.clockTimer = setInterval(() => { this.now = Date.now() }, 1000)
this.refreshTimer = setInterval(() => { this.fetchData(false) }, 5000)
// 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)
@ -126,6 +141,11 @@ export default {
// URLsite=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
@ -133,6 +153,11 @@ export default {
this.lastReceivedAt = 0
this.siteReady = false
this.site = ''
// R1H-R4H - rqrq
if (!['R1H', 'R2H', 'R3H', 'R4H'].includes(this.sortingStation)) {
this.message = '人工看板仅支持 R1H–R4H 分拣位'
return
}
const storeSite = String(this.$store.state.user.site || '').trim()
const querySite = this.$route.query.site
if (querySite !== undefined && (typeof querySite !== 'string' || querySite.trim() !== '54')) {
@ -150,26 +175,111 @@ export default {
this.message = '正在获取分拣数据…'
this.$nextTick(() => {
if (this.$refs.tableViewport) this.$refs.tableViewport.scrollTop = 0
this.fetchData(false)
this.checkBoardConfig()
})
},
// loadingsitefinally - rqrq
// true - rqrq
async checkBoardConfig () {
if (!this.siteReady || this.configLoading) return
const version = this.requestVersion
this.configLoading = true
try {
const { data } = await getSortingBoardConfig({ site: this.$store.state.user.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()
},
// HTTPsite/ - rqrq
applyBoard (data) {
if (!this.pushEnabled) return
const site = String(this.$store.state.user.site || '').trim()
if (!data || data.code !== 0 || !data.row || site !== this.site || data.row.site !== 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 ? '数据正常' : '当前分拣位暂无任务')
},
// 使HTTPHTTPfinally - rqrq
async fetchData (manual = false) {
if (!this.siteReady || this.queryLoading) return
if (!this.siteReady || !this.pushEnabled || this.queryLoading) return
const version = this.requestVersion
const dataVersion = this.dataVersion
const station = this.sortingStation
const site = String(this.$store.state.user.site || '').trim()
if (site !== this.site) {
this.available = false
this.rows = []
this.message = '当前工厂已变化,请重新打开海安看板'
this.initializeBoard()
return
}
this.queryLoading = true
try {
const { data } = await getSortingBoard({ site: this.$store.state.user.site, sortingStation: station })
if (version !== this.requestVersion) return
this.lastReceivedAt = Date.now()
const { data } = await getManualSortingBoard({ site: this.$store.state.user.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
@ -177,23 +287,14 @@ export default {
if (manual) this.$alert(this.message, '查询失败', { confirmButtonText: '确定' }).catch(() => {})
return
}
const board = data.row
if (board.site !== site || board.sortingStation !== station) {
throw new Error('返回工厂或分拣位与当前看板不一致')
}
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 ? '数据正常' : '当前分拣位暂无任务')
this.applyBoard(data)
if (manual && !this.available) this.$alert(this.message, '数据不可用', { confirmButtonText: '确定' }).catch(() => {})
} catch (error) {
if (version !== this.requestVersion) return
if (version !== this.requestVersion || !this.pushEnabled || dataVersion !== this.dataVersion) return
this.rows = []
this.available = false
this.message = '分拣数据连接异常,正在重试…'
// 5 - rqrq
if (manual || !this.lastReceivedAt) this.$message.error('查询海安分拣看板失败,请检查网络或服务')
this.lastReceivedAt = Date.now()
this.message = '手动刷新失败,等待推送恢复…'
if (manual) this.$message.error('查询海安分拣看板失败,请检查网络或服务')
} finally {
if (version === this.requestVersion) this.queryLoading = false
}
@ -215,12 +316,12 @@ export default {
</script>
<style scoped>
/* 海安大屏沿用项目蓝底白字视觉,固定视口和表头;长文本可换行,避免小屏横向滚动 - rqrq */
.haian-sorting-board { height: 100vh; width: 100%; box-sizing: border-box; padding: 18px 24px 12px; background: #344f70; 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; }
.board-logo { width: 108px; height: auto; }
/* 沿用老分拣看板蓝色渐变、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; }
.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; }
@ -228,10 +329,13 @@ export default {
.summary-counts strong { font-size: 26px; margin-left: 8px; }
.pending-text { color: #ffd17d; }
.completed-text { color: #80e5bc; }
.connection-dot { width: 10px; height: 10px; flex-shrink: 0; border-radius: 50%; background: #ffad99; }
.connection-dot.healthy { background: #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: #2b405a; border: 1px solid #637e9e; border-radius: 6px; }
.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; }

Loading…
Cancel
Save