Browse Source

3个看板

master
常熟吴彦祖 2 weeks ago
parent
commit
b4e8861de3
  1. 4
      src/api/haianWarehouse/autoJ1SortingBoard.js
  2. 4
      src/api/haianWarehouse/autoJ2SortingBoard.js
  3. 4
      src/api/haianWarehouse/autoSortingBoard.js
  4. 4
      src/api/haianWarehouse/errorAreaBoard.js
  5. 4
      src/api/haianWarehouse/palletOutboundBoard.js
  6. 7
      src/router/index.js
  7. 342
      src/views/modules/haianWarehouse/dashboard/autoJ1SortingBoard54.vue
  8. 342
      src/views/modules/haianWarehouse/dashboard/autoJ2SortingBoard54.vue
  9. 702
      src/views/modules/haianWarehouse/dashboard/autoSortingBoard54.vue
  10. 597
      src/views/modules/haianWarehouse/dashboard/errorAreaBoard54.vue
  11. 594
      src/views/modules/haianWarehouse/dashboard/palletOutboundBoard54.vue

4
src/api/haianWarehouse/autoJ1SortingBoard.js

@ -1,4 +0,0 @@
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

@ -1,4 +0,0 @@
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/autoSortingBoard.js

@ -0,0 +1,4 @@
import { createAPI } from '@/utils/httpRequest.js'
// 海安自动分拣合并看板,一次取位置1和位置2;site 由页面固定传 54 - rqrq
export const getAutoSortingBoard = data => createAPI('/api/dashboard/haian/sorting/getAutoSortingBoard', 'post', data)

4
src/api/haianWarehouse/errorAreaBoard.js

@ -0,0 +1,4 @@
import { createAPI } from '@/utils/httpRequest.js'
// 海安异常区域看板。页面每 5 秒查询,site 固定传 54 - rqrq
export const getErrorAreaBoard = data => createAPI('/api/dashboard/haian/error-area/getErrorAreaBoard', 'post', data)

4
src/api/haianWarehouse/palletOutboundBoard.js

@ -0,0 +1,4 @@
import { createAPI } from '@/utils/httpRequest.js'
// 海安出库托盘看板。页面每 5 秒查询,site 固定传 54 - rqrq
export const getPalletOutboundBoard = data => createAPI('/api/dashboard/haian/outbound/getPalletOutboundBoard', 'post', data)

7
src/router/index.js

@ -21,9 +21,10 @@ 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: '登录' } },
// 海安六屏保留原URL,人工传工作位ST-MP-05至08,自动页固定ST-RP-03/04;请求、响应校验和订阅均按工作位防串屏 - 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' } },
// 自动分拣合并为一屏;人工四屏仍按工作位 ST-MP-05 至 ST-MP-08 分开,避免串屏 - rqrq
{ path: '/dashboard-haian-sorting-auto', component: _import('modules/haianWarehouse/dashboard/autoSortingBoard54'), name: 'haian-sorting-auto', meta: { title: '海安自动分拣' } },
{ path: '/dashboard-haian-pallet-outbound', component: _import('modules/haianWarehouse/dashboard/palletOutboundBoard54'), name: 'haian-pallet-outbound', meta: { title: '海安出库托盘' } },
{ path: '/dashboard-haian-pallet-error', component: _import('modules/haianWarehouse/dashboard/errorAreaBoard54'), name: 'haian-pallet-error', meta: { title: '海安异常区域' } },
{ path: '/dashboard-haian-sorting-r1h', component: _import('modules/haianWarehouse/dashboard/sortingBoard54'), name: 'haian-sorting-r1h', props: { sortingStation: 'ST-MP-05', boardTitle: '人工分拣 1' }, meta: { title: '海安人工分拣1' } },
{ path: '/dashboard-haian-sorting-r2h', component: _import('modules/haianWarehouse/dashboard/sortingBoard54'), name: 'haian-sorting-r2h', props: { sortingStation: 'ST-MP-06', boardTitle: '人工分拣 2' }, meta: { title: '海安人工分拣2' } },
{ path: '/dashboard-haian-sorting-r3h', component: _import('modules/haianWarehouse/dashboard/sortingBoard54'), name: 'haian-sorting-r3h', props: { sortingStation: 'ST-MP-07', boardTitle: '人工分拣 3' }, meta: { title: '海安人工分拣3' } },

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

@ -1,342 +0,0 @@
<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 { getAutoJ1SortingBoard } from '@/api/haianWarehouse/autoJ1SortingBoard'
// 自动J1H独立页面,布局、字段和请求逻辑在本文件维护;不包装人工或另一自动看板 - rqrq
export default {
name: 'HaianAutoJ1SortingBoard54',
data () {
return {
// 本屏固定实际工作位ST-RP-03,查询、响应校验和订阅共用该身份;后端映射对应被分拣位ST-RP-01 - rqrq
sortingStation: 'ST-RP-03',
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 },
// 绿灯只代表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 getAutoJ1SortingBoard({ 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; }
/* 表头表体字号加2px,行高锁在原18px字号的行盒,换行后每一行高度也不跟着涨 - rqrq */
.board-table th { position: sticky; top: 0; z-index: 1; background: #1b6676; color: #fff; font-size: 20px; line-height: 22px; padding: 16px 8px; }
.board-table td { padding: 18px 8px; font-size: 20px; line-height: 22px; 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: 16px; }
/* 状态字号加2px,行盒保持原高度,避免状态块把表格行撑高 - rqrq */
.status-badge { display: inline-block; padding: 5px 10px; border-radius: 4px; white-space: nowrap; font-size: 18px; line-height: 19px; }
.status-badge.pending { background: #745827; color: #fff0cf; }
.status-badge.completed { background: #21684e; color: #d4ffeb; }
.board-table .empty-message { height: 220px; font-size: 26px; 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: 16px; line-height: 17px; padding: 12px 5px; }
.status-badge { padding: 4px; font-size: 15px; line-height: 16px; }
}
</style>

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

@ -1,342 +0,0 @@
<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 {
// 本屏固定实际工作位ST-RP-04,查询、响应校验和订阅共用该身份;后端映射对应被分拣位ST-RP-02 - rqrq
sortingStation: 'ST-RP-04',
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; }
/* 表头表体字号加2px,行高锁在原18px字号的行盒,换行后每一行高度也不跟着涨 - rqrq */
.board-table th { position: sticky; top: 0; z-index: 1; background: #1b6676; color: #fff; font-size: 20px; line-height: 22px; padding: 16px 8px; }
.board-table td { padding: 18px 8px; font-size: 20px; line-height: 22px; 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: 16px; }
/* 状态字号加2px,行盒保持原高度,避免状态块把表格行撑高 - rqrq */
.status-badge { display: inline-block; padding: 5px 10px; border-radius: 4px; white-space: nowrap; font-size: 18px; line-height: 19px; }
.status-badge.pending { background: #745827; color: #fff0cf; }
.status-badge.completed { background: #21684e; color: #d4ffeb; }
.board-table .empty-message { height: 220px; font-size: 26px; 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: 16px; line-height: 17px; padding: 12px 5px; }
.status-badge { padding: 4px; font-size: 15px; line-height: 16px; }
}
</style>

702
src/views/modules/haianWarehouse/dashboard/autoSortingBoard54.vue

@ -0,0 +1,702 @@
<template>
<div class="picking-board-screen">
<!-- 装饰背景 -->
<div class="bg-decoration">
<div class="decoration-line line-1"></div>
<div class="decoration-line line-2"></div>
<div class="decoration-line line-3"></div>
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
</div>
<!-- 顶部标题栏 -->
<div class="screen-header">
<!-- CCL Logo -->
<div class="header-logo">
<img src="~@/assets/img/cclbai.png" alt="CCL Logo" class="logo-img">
</div>
<div class="header-decoration left"></div>
<div class="header-center">
<div class="title-glow"></div>
<h1 class="screen-title">海安自动分拣
<span class="ws-status-dot" :class="connectionClass" :title="connectionText"></span>
</h1>
</div>
<div class="header-decoration right"></div>
<div class="header-time">
<div class="time-icon"></div>
<div class="time-text">{{ currentTime }}</div>
</div>
</div>
<!-- 主内容区 -->
<div class="screen-content">
<!-- 周转箱拣选面板 -->
<div class="picking-panel">
<!-- 面板标题 -->
<!-- 数据表格 -->
<div class="panel-table">
<table class="data-table">
<thead>
<tr>
<th style="width: 90px;">拣选口</th>
<th style="width: 150px;">拣选托盘码</th>
<th style="width: 220px;">拣选物料名称</th>
<th style="width: 160px;">RFID</th>
<th style="width: 150px;">存放托盘码</th>
<th style="width: 110px;">状态</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, idx) in boardList" :key="idx + '-' + item.pickPort + '-' + item.rfidBarcode">
<td class="text-center">{{ formatPickPort(item.pickPort) }}</td>
<td class="text-center">{{ item.pickingPalletId || '-' }}</td>
<td class="text-center">{{ item.partDesc || '-' }}</td>
<td class="text-center">{{ item.rfidBarcode }}</td>
<td class="text-center">{{ item.storagePalletId || '-' }}</td>
<td class="text-center">
<span :class="['status-badge', getStatusClass(item.status)]">{{ item.status }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 底部装饰 -->
<div class="screen-footer">
<div class="footer-line"></div>
</div>
</div>
</template>
<script>
import HaianBoardSocket from '@/utils/haianBoardSocket'
import { getSortingBoardConfig } from '@/api/haianWarehouse/sortingBoardConfig'
// 外观沿用周转箱看板。数据改走海安看板 WebSocket,主题一次带上位置1和位置2 - rqrq
export default {
name: 'HaianAutoSortingBoard54',
data() {
return {
currentTime: '',
timeInterval: null,
serverTimeOffset: 0,
refreshCheckInterval: null,
configTimer: null,
site: '54',
pushEnabled: false,
configLoading: false,
requestVersion: 0,
// connecting 黄、connected 绿、disconnected 红,只表示 STOMP 连接,不表示立库有没有任务 - rqrq
connectionState: 'connecting',
// 位置1和位置2合在一张表,按拣选口排列 - rqrq
boardList: []
}
},
computed: {
connectionClass () {
return {
'is-connecting': this.connectionState === 'connecting',
'is-connected': this.connectionState === 'connected',
'is-disconnected': this.connectionState === 'disconnected' || this.connectionState === 'disabled'
}
},
connectionText () {
return { connecting: '正在连接', connected: '连接正常', disconnected: '连接断开,等待重连', disabled: '推送已停用' }[this.connectionState]
}
},
mounted() {
this.currentTime = '等待服务器时间同步...'
this.timeInterval = setInterval(() => {
this.updateTime()
}, 1000)
this.startRefreshCheck()
// 先读推送开关,打开后订阅合并主题;数据由后台每轮推送,页面不轮询接口 - rqrq
this.checkBoardConfig()
this.configTimer = setInterval(() => {
this.checkBoardConfig()
}, 30000)
},
beforeDestroy() {
this.requestVersion++
this.disconnectWebSocket()
if (this.timeInterval) {
clearInterval(this.timeInterval)
}
if (this.refreshCheckInterval) {
clearInterval(this.refreshCheckInterval)
}
if (this.configTimer) {
clearInterval(this.configTimer)
}
},
methods: {
/**
* 启动定时刷新检查
* 每分钟检查一次,如果到达凌晨5点则自动刷新页面
*/
startRefreshCheck() {
this.refreshCheckInterval = setInterval(() => {
this.checkAndRefreshPage()
}, 60000)
this.checkAndRefreshPage()
},
/**
* 检查当前时间,如果是凌晨5点则刷新页面
*/
checkAndRefreshPage() {
const now = new Date()
const hours = now.getHours()
const minutes = now.getMinutes()
// 判断是否为凌晨5:00-5:10之间
if (hours === 5 && minutes === 10) {
location.reload()
}
},
// 只读推送开关。关闭时断开并清空表格,不改成立库轮询 - rqrq
async checkBoardConfig () {
if (this.configLoading) return
const version = this.requestVersion
this.configLoading = true
try {
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.connectionState = 'disabled'
this.boardList = []
return
}
if (!this._boardSocket) this.initWebSocket()
} catch (error) {
if (version !== this.requestVersion) return
this.pushEnabled = false
this.disconnectWebSocket()
this.connectionState = 'disconnected'
this.boardList = []
} finally {
if (version === this.requestVersion) this.configLoading = false
}
},
// 订阅海安合并主题。连接灯只反映 STOMP,两组清单由推送消息覆盖 - rqrq
initWebSocket () {
if (!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 + '/AUTO'
this._boardSocket = new HaianBoardSocket(url, topic, data => {
if (version !== this.requestVersion || !this.pushEnabled) return
this.applyBoard(data)
}, state => {
if (version !== this.requestVersion || !this.pushEnabled) return
this.connectionState = state
if (state !== 'connected') {
this.boardList = []
}
})
this._boardSocket.start()
},
disconnectWebSocket () {
const socket = this._boardSocket
this._boardSocket = null
if (socket) socket.stop()
},
// 两组推送合成一张表。先按拣选口,同一口里未完成在前,已完成留在后面 - rqrq
applyBoard (data) {
if (!data || data.code !== 0 || !data.row || data.row.site !== this.site) {
this.boardList = []
throw new Error('自动分拣看板数据校验失败')
}
const board = data.row
if (board.available !== true) {
this.boardList = []
return
}
const rows = []
if (Array.isArray(board.position1Rows)) rows.push.apply(rows, board.position1Rows)
if (Array.isArray(board.position2Rows)) rows.push.apply(rows, board.position2Rows)
rows.sort((left, right) => {
const portDiff = this.pickPortOrder(left.pickPort) - this.pickPortOrder(right.pickPort)
if (portDiff !== 0) return portDiff
return (left.status === '未完成' ? 0 : 1) - (right.status === '未完成' ? 0 : 1)
})
this.boardList = rows
},
// 拣选口固定显示位置1、位置2,兼容推送里仍是 1、2 的旧值 - rqrq
formatPickPort (pickPort) {
const order = this.pickPortOrder(pickPort)
return order === 1 || order === 2 ? '位置' + order : (pickPort || '-')
},
pickPortOrder (pickPort) {
const text = String(pickPort || '')
if (text === '1' || text === '位置1') return 1
if (text === '2' || text === '位置2') return 2
return 9
},
/**
* 更新服务器时间偏移量
*/
updateServerTimeOffset(serverTimeString) {
try {
const timeStr = serverTimeString.split(' ')[0] + ' ' + serverTimeString.split(' ')[1]
const serverTime = new Date(timeStr).getTime()
const localTime = new Date().getTime()
if (!isNaN(serverTime)) {
this.serverTimeOffset = serverTime - localTime
}
} catch (error) {
console.warn('解析服务器时间失败:', error)
}
},
/**
* 更新当前时间(使用服务器时间偏移量)
*/
updateTime() {
const now = new Date(new Date().getTime() + this.serverTimeOffset)
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const weekDay = weekDays[now.getDay()]
this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${weekDay}`
},
/**
* 根据状态获取样式类名
*/
getStatusClass(status) {
return status === '已完成' ? 'status-success' : 'status-pending'
}
}
}
</script>
<style scoped lang="scss">
/* ===== 整体容器 ===== */
.picking-board-screen {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #5f8cc3 0%, #749cc8 100%);
position: relative;
overflow: hidden;
font-family: 'Microsoft YaHei', 'PingFang SC', Arial, sans-serif;
}
/* ===== 装饰背景 ===== */
.bg-decoration {
display: none;
}
/* ===== 顶部标题区 ===== */
.screen-header {
position: relative;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 40px;
z-index: 10;
border-bottom: 2px solid rgba(23, 179, 163, 0.4);
background: linear-gradient(180deg, rgba(23, 179, 163, 0.08) 0%, transparent 100%);
}
/* CCL Logo */
.header-logo {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
z-index: 20;
}
.logo-img {
height: 40px;
width: auto;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
transition: all 0.3s ease;
&:hover {
filter: drop-shadow(0 4px 12px rgba(23, 179, 163, 0.5));
transform: scale(1.05);
}
}
.header-decoration {
display: none;
}
.header-center {
position: relative;
text-align: center;
}
.title-glow {
display: none;
}
.screen-title {
position: relative;
font-size: 34px;
font-weight: bold;
color: #ffffff;
margin: 0;
letter-spacing: 3px;
text-shadow: 0 0 20px rgba(23, 179, 163, 0.5);
}
.ws-status-dot {
display: inline-block;
width: 12px;
height: 12px;
margin-left: 12px;
border-radius: 50%;
vertical-align: middle;
background: #22c55e;
box-shadow: 0 0 10px rgba(34, 197, 94, 0.8);
&.is-connecting {
background: #f59e0b;
box-shadow: 0 0 12px rgba(245, 158, 11, 0.85);
}
&.is-disconnected {
background: #ef4444;
box-shadow: 0 0 12px rgba(239, 68, 68, 0.85);
animation: ws-status-blink 1s steps(1, end) infinite;
}
}
@keyframes ws-status-blink {
0%, 50% {
opacity: 1;
}
50.01%, 100% {
opacity: 0.2;
}
}
.title-subtitle {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
letter-spacing: 2px;
margin-top: 3px;
font-family: Arial, sans-serif;
text-transform: uppercase;
}
.header-time {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
border-radius: 8px;
backdrop-filter: blur(10px);
}
.time-icon {
font-size: 14px;
color: #17B3A3;
}
.time-text {
font-size: 16px;
color: #ffffff;
font-family: 'Consolas', 'Courier New', monospace;
font-weight: 500;
letter-spacing: 1px;
}
/* ===== 主内容区 ===== */
.screen-content {
position: relative;
z-index: 1;
padding: 8px 15px;
height: calc(100vh - 60px);
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(23, 179, 163, 0.1);
}
&::-webkit-scrollbar-thumb {
background: rgba(23, 179, 163, 0.5);
border-radius: 3px;
&:hover {
background: rgba(23, 179, 163, 0.7);
}
}
}
/* ===== 拣选面板 ===== */
.picking-panel {
width: 100%;
height: calc(100vh - 80px);
background: rgba(70, 90, 120, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(23, 179, 163, 0.5);
border-radius: 12px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
overflow: hidden;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
&:hover {
border-color: rgba(23, 179, 163, 0.5);
box-shadow:
0 12px 48px rgba(0, 0, 0, 0.5),
0 0 30px rgba(23, 179, 163, 0.2);
transform: translateY(-2px);
}
}
/* ===== 面板标题栏 ===== */
.panel-title-bar {
background: linear-gradient(135deg, rgba(23, 179, 163, 0.3) 0%, rgba(23, 179, 163, 0.15) 100%);
border-bottom: 1px solid rgba(23, 179, 163, 0.3);
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-left {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.title-icon {
font-size: 16px;
color: #64D8CB;
}
.title-text {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.title-main {
font-size: 24px;
font-weight: bold;
color: #ffffff;
text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
}
/* ===== 数据表格 ===== */
.panel-table {
padding: 10px;
flex: 1;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(23, 179, 163, 0.05);
}
&::-webkit-scrollbar-thumb {
background: rgba(23, 179, 163, 0.3);
border-radius: 3px;
&:hover {
background: rgba(23, 179, 163, 0.5);
}
}
}
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
thead {
tr {
background: linear-gradient(135deg, rgba(23, 179, 163, 0.25) 0%, rgba(23, 179, 163, 0.15) 100%);
}
th {
padding: 5px 6px;
color: #fcfdfd;
font-size: 16px;
font-weight: bold;
text-align: center;
border-bottom: 2px solid rgba(23, 179, 163, 0.4);
text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
white-space: nowrap;
&:first-child {
border-top-left-radius: 8px;
}
&:last-child {
border-top-right-radius: 8px;
}
}
}
tbody {
tr {
background: rgba(60, 80, 105, 0.6);
transition: all 0.3s ease;
&:nth-child(even) {
background: rgba(70, 90, 115, 0.7);
}
&:hover {
background: rgba(23, 179, 163, 0.15);
box-shadow: 0 4px 12px rgba(23, 179, 163, 0.2);
}
&:last-child {
td:first-child {
border-bottom-left-radius: 8px;
}
td:last-child {
border-bottom-right-radius: 8px;
}
}
}
td {
padding: 4px 6px;
color: rgba(255, 255, 255, 0.9);
font-size: 15px;
border-bottom: 1px solid rgba(23, 179, 163, 0.15);
&.text-center {
text-align: center;
}
&.text-left {
text-align: left;
}
&.text-right {
text-align: right;
}
}
}
}
/* ===== 状态徽章 ===== */
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: bold;
text-align: center;
min-width: 80px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
&.status-success {
background: linear-gradient(135deg, #10b981, #34d399);
color: #ffffff;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
}
&.status-warning {
background: linear-gradient(135deg, #10b981, #34d399);
color: #ffffff;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
}
&.status-pending {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
color: #ffffff;
box-shadow: 0 0 15px rgba(245, 158, 11, 0.5);
}
}
/* ===== 底部装饰 ===== */
.screen-footer {
display: none;
}
/* ===== 响应式适配 ===== */
@media screen and (max-width: 1600px) {
.screen-title {
font-size: 30px;
letter-spacing: 3px;
}
.title-main {
font-size: 22px;
}
}
@media screen and (min-width: 2560px) {
.screen-title {
font-size: 38px;
}
.panel-title-bar {
padding: 16px 24px;
}
.title-main {
font-size: 28px;
}
.data-table {
thead th {
font-size: 20px;
padding: 6px 12px;
}
tbody td {
font-size: 20px;
padding: 4px 12px;
}
}
.status-badge {
font-size: 18px;
padding: 4px 10px;
min-width: 100px;
}
}
</style>

597
src/views/modules/haianWarehouse/dashboard/errorAreaBoard54.vue

@ -0,0 +1,597 @@
<template>
<div class="picking-board-screen">
<!-- 装饰背景 -->
<div class="bg-decoration">
<div class="decoration-line line-1"></div>
<div class="decoration-line line-2"></div>
<div class="decoration-line line-3"></div>
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
</div>
<!-- 顶部标题栏 -->
<div class="screen-header">
<!-- CCL Logo -->
<div class="header-logo">
<img src="~@/assets/img/cclbai.png" alt="CCL Logo" class="logo-img">
</div>
<div class="header-decoration left"></div>
<div class="header-center">
<div class="title-glow"></div>
<h1 class="screen-title">异常区域看板
<span class="ws-status-dot" :class="connectionClass" :title="connectionText"></span>
</h1>
</div>
<div class="header-decoration right"></div>
<div class="header-time">
<div class="time-icon"></div>
<div class="time-text">{{ currentTime }}</div>
</div>
</div>
<!-- 主内容区 -->
<div class="screen-content">
<!-- 周转箱拣选面板 -->
<div class="picking-panel">
<!-- 面板标题 -->
<!-- 数据表格 -->
<div class="panel-table">
<table class="data-table error-area-table">
<colgroup>
<col style="width: 20%">
<col style="width: 20%">
<col style="width: 60%">
</colgroup>
<thead>
<tr>
<th>位置</th>
<th>栈板</th>
<th>异常信息</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, idx) in boardList" :key="idx + '-' + item.palletId">
<td class="text-center">{{ item.locationCode || '-' }}</td>
<td class="text-center">{{ item.palletId || '-' }}</td>
<td class="text-left error-message">{{ item.errorMessage || '-' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 底部装饰 -->
<div class="screen-footer">
<div class="footer-line"></div>
</div>
</div>
</template>
<script>
import { getErrorAreaBoard } from '@/api/haianWarehouse/errorAreaBoard'
// 外观沿用海安看板。每 5 秒无锁查 site=54、区域 A09 的栈板 - rqrq
export default {
name: 'HaianErrorAreaBoard54',
data() {
return {
currentTime: '',
timeInterval: null,
serverTimeOffset: 0,
refreshCheckInterval: null,
pollTimer: null,
queryLoading: false,
// 第一次请求中为黄,查询成功为绿,失败为红。没有栈板也保持绿灯 - rqrq
connectionState: 'connecting',
boardList: []
}
},
computed: {
connectionClass () {
return {
'is-connecting': this.connectionState === 'connecting',
'is-connected': this.connectionState === 'connected',
'is-disconnected': this.connectionState === 'disconnected'
}
},
connectionText () {
return { connecting: '正在查询异常区域', connected: '数据正常', disconnected: '查询失败' }[this.connectionState]
}
},
mounted() {
this.currentTime = '等待服务器时间同步...'
this.timeInterval = setInterval(() => {
this.updateTime()
}, 1000)
this.startRefreshCheck()
// 每 5 秒查一次 A09。上一轮没回来就跳过,避免请求叠在一起 - rqrq
this.getDataList()
this.pollTimer = setInterval(() => {
this.getDataList()
}, 5000)
},
beforeDestroy() {
if (this.timeInterval) {
clearInterval(this.timeInterval)
}
if (this.refreshCheckInterval) {
clearInterval(this.refreshCheckInterval)
}
if (this.pollTimer) {
clearInterval(this.pollTimer)
}
},
methods: {
startRefreshCheck() {
this.refreshCheckInterval = setInterval(() => {
this.checkAndRefreshPage()
}, 60000)
this.checkAndRefreshPage()
},
checkAndRefreshPage() {
const now = new Date()
if (now.getHours() === 5 && now.getMinutes() === 10) {
location.reload()
}
},
// 查询成功亮绿灯,没有栈板也亮绿灯。查询失败亮红灯并清空表 - rqrq
getDataList() {
if (this.queryLoading) return
this.queryLoading = true
getErrorAreaBoard({ site: '54' }).then(({ data }) => {
if (data && data.code === 0 && data.row && data.row.site === '54' && data.row.available === true) {
this.connectionState = 'connected'
this.boardList = Array.isArray(data.row.rows) ? data.row.rows : []
return
}
this.connectionState = 'disconnected'
this.boardList = []
}).catch(() => {
this.connectionState = 'disconnected'
this.boardList = []
}).finally(() => {
this.queryLoading = false
})
},
updateTime() {
const now = new Date(new Date().getTime() + this.serverTimeOffset)
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const weekDay = weekDays[now.getDay()]
this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${weekDay}`
}
}
}
</script>
<style scoped lang="scss">
/* ===== 整体容器 ===== */
.picking-board-screen {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #5f8cc3 0%, #749cc8 100%);
position: relative;
overflow: hidden;
font-family: 'Microsoft YaHei', 'PingFang SC', Arial, sans-serif;
}
/* ===== 装饰背景 ===== */
.bg-decoration {
display: none;
}
/* ===== 顶部标题区 ===== */
.screen-header {
position: relative;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 40px;
z-index: 10;
border-bottom: 2px solid rgba(23, 179, 163, 0.4);
background: linear-gradient(180deg, rgba(23, 179, 163, 0.08) 0%, transparent 100%);
}
/* CCL Logo */
.header-logo {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
z-index: 20;
}
.logo-img {
height: 40px;
width: auto;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
transition: all 0.3s ease;
&:hover {
filter: drop-shadow(0 4px 12px rgba(23, 179, 163, 0.5));
transform: scale(1.05);
}
}
.header-decoration {
display: none;
}
.header-center {
position: relative;
text-align: center;
}
.title-glow {
display: none;
}
.screen-title {
position: relative;
font-size: 34px;
font-weight: bold;
color: #ffffff;
margin: 0;
letter-spacing: 3px;
text-shadow: 0 0 20px rgba(23, 179, 163, 0.5);
}
.ws-status-dot {
display: inline-block;
width: 12px;
height: 12px;
margin-left: 12px;
border-radius: 50%;
vertical-align: middle;
background: #22c55e;
box-shadow: 0 0 10px rgba(34, 197, 94, 0.8);
&.is-connecting {
background: #f59e0b;
box-shadow: 0 0 12px rgba(245, 158, 11, 0.85);
}
&.is-disconnected {
background: #ef4444;
box-shadow: 0 0 12px rgba(239, 68, 68, 0.85);
animation: ws-status-blink 1s steps(1, end) infinite;
}
}
@keyframes ws-status-blink {
0%, 50% {
opacity: 1;
}
50.01%, 100% {
opacity: 0.2;
}
}
.title-subtitle {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
letter-spacing: 2px;
margin-top: 3px;
font-family: Arial, sans-serif;
text-transform: uppercase;
}
.header-time {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
border-radius: 8px;
backdrop-filter: blur(10px);
}
.time-icon {
font-size: 14px;
color: #17B3A3;
}
.time-text {
font-size: 16px;
color: #ffffff;
font-family: 'Consolas', 'Courier New', monospace;
font-weight: 500;
letter-spacing: 1px;
}
/* ===== 主内容区 ===== */
.screen-content {
position: relative;
z-index: 1;
padding: 8px 15px;
height: calc(100vh - 60px);
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(23, 179, 163, 0.1);
}
&::-webkit-scrollbar-thumb {
background: rgba(23, 179, 163, 0.5);
border-radius: 3px;
&:hover {
background: rgba(23, 179, 163, 0.7);
}
}
}
/* ===== 拣选面板 ===== */
.picking-panel {
width: 100%;
height: calc(100vh - 80px);
background: rgba(70, 90, 120, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(23, 179, 163, 0.5);
border-radius: 12px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
overflow: hidden;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
&:hover {
border-color: rgba(23, 179, 163, 0.5);
box-shadow:
0 12px 48px rgba(0, 0, 0, 0.5),
0 0 30px rgba(23, 179, 163, 0.2);
transform: translateY(-2px);
}
}
/* ===== 面板标题栏 ===== */
.panel-title-bar {
background: linear-gradient(135deg, rgba(23, 179, 163, 0.3) 0%, rgba(23, 179, 163, 0.15) 100%);
border-bottom: 1px solid rgba(23, 179, 163, 0.3);
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-left {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.title-icon {
font-size: 16px;
color: #64D8CB;
}
.title-text {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.title-main {
font-size: 24px;
font-weight: bold;
color: #ffffff;
text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
}
/* ===== 数据表格 ===== */
.panel-table {
padding: 10px;
flex: 1;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(23, 179, 163, 0.05);
}
&::-webkit-scrollbar-thumb {
background: rgba(23, 179, 163, 0.3);
border-radius: 3px;
&:hover {
background: rgba(23, 179, 163, 0.5);
}
}
}
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
thead {
tr {
background: linear-gradient(135deg, rgba(23, 179, 163, 0.25) 0%, rgba(23, 179, 163, 0.15) 100%);
}
th {
padding: 5px 6px;
color: #fcfdfd;
font-size: 16px;
font-weight: bold;
text-align: center;
border-bottom: 2px solid rgba(23, 179, 163, 0.4);
text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
white-space: nowrap;
&:first-child {
border-top-left-radius: 8px;
}
&:last-child {
border-top-right-radius: 8px;
}
}
}
tbody {
tr {
background: rgba(60, 80, 105, 0.6);
transition: all 0.3s ease;
&:nth-child(even) {
background: rgba(70, 90, 115, 0.7);
}
&:hover {
background: rgba(23, 179, 163, 0.15);
box-shadow: 0 4px 12px rgba(23, 179, 163, 0.2);
}
&:last-child {
td:first-child {
border-bottom-left-radius: 8px;
}
td:last-child {
border-bottom-right-radius: 8px;
}
}
}
td {
padding: 4px 6px;
color: rgba(255, 255, 255, 0.9);
font-size: 15px;
border-bottom: 1px solid rgba(23, 179, 163, 0.15);
&.text-center {
text-align: center;
}
&.text-left {
text-align: left;
}
&.text-right {
text-align: right;
}
}
}
}
/* 异常信息列占 60%,长文本换行,不把位置和栈板挤没 - rqrq */
.error-area-table {
table-layout: fixed;
td.error-message {
word-break: break-all;
white-space: normal;
}
}
/* ===== 状态徽章 ===== */
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: bold;
text-align: center;
min-width: 80px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
&.status-success {
background: linear-gradient(135deg, #10b981, #34d399);
color: #ffffff;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
}
&.status-warning {
background: linear-gradient(135deg, #10b981, #34d399);
color: #ffffff;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
}
&.status-pending {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
color: #ffffff;
box-shadow: 0 0 15px rgba(245, 158, 11, 0.5);
}
}
/* ===== 底部装饰 ===== */
.screen-footer {
display: none;
}
/* ===== 响应式适配 ===== */
@media screen and (max-width: 1600px) {
.screen-title {
font-size: 30px;
letter-spacing: 3px;
}
.title-main {
font-size: 22px;
}
}
@media screen and (min-width: 2560px) {
.screen-title {
font-size: 38px;
}
.panel-title-bar {
padding: 16px 24px;
}
.title-main {
font-size: 28px;
}
.data-table {
thead th {
font-size: 20px;
padding: 6px 12px;
}
tbody td {
font-size: 20px;
padding: 4px 12px;
}
}
.status-badge {
font-size: 18px;
padding: 4px 10px;
min-width: 100px;
}
}
</style>

594
src/views/modules/haianWarehouse/dashboard/palletOutboundBoard54.vue

@ -0,0 +1,594 @@
<template>
<div class="picking-board-screen">
<!-- 装饰背景 -->
<div class="bg-decoration">
<div class="decoration-line line-1"></div>
<div class="decoration-line line-2"></div>
<div class="decoration-line line-3"></div>
<div class="decoration-circle circle-1"></div>
<div class="decoration-circle circle-2"></div>
</div>
<!-- 顶部标题栏 -->
<div class="screen-header">
<!-- CCL Logo -->
<div class="header-logo">
<img src="~@/assets/img/cclbai.png" alt="CCL Logo" class="logo-img">
</div>
<div class="header-decoration left"></div>
<div class="header-center">
<div class="title-glow"></div>
<h1 class="screen-title">出库托盘看板
<span class="ws-status-dot" :class="connectionClass" :title="connectionText"></span>
</h1>
</div>
<div class="header-decoration right"></div>
<div class="header-time">
<div class="time-icon"></div>
<div class="time-text">{{ currentTime }}</div>
</div>
</div>
<!-- 主内容区 -->
<div class="screen-content">
<!-- 周转箱拣选面板 -->
<div class="picking-panel">
<!-- 面板标题 -->
<!-- 数据表格 -->
<div class="panel-table">
<table class="data-table">
<thead>
<tr>
<th style="width: 180px;">托盘码</th>
<th style="width: 180px;">物料编码</th>
<th style="width: 120px;">数量</th>
<th style="width: 160px;">状态</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, idx) in boardList" :key="idx + '-' + item.palletId + '-' + item.partNo">
<td class="text-center">{{ item.palletId || '-' }}</td>
<td class="text-center">{{ item.partNo || '-' }}</td>
<td class="text-center">{{ formatQty(item.qty) }}</td>
<td class="text-center">{{ item.status || '-' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 底部装饰 -->
<div class="screen-footer">
<div class="footer-line"></div>
</div>
</div>
</template>
<script>
import { getPalletOutboundBoard } from '@/api/haianWarehouse/palletOutboundBoard'
// 外观沿用海安自动分拣看板。数据改为每 5 秒查出库托盘,灯只看立库是不是 200 - rqrq
export default {
name: 'HaianPalletOutboundBoard54',
data() {
return {
currentTime: '',
timeInterval: null,
serverTimeOffset: 0,
refreshCheckInterval: null,
pollTimer: null,
queryLoading: false,
// 第一次请求中为黄,立库 200 为绿,非 200 或调用失败为红 - rqrq
connectionState: 'connecting',
boardList: []
}
},
computed: {
connectionClass () {
return {
'is-connecting': this.connectionState === 'connecting',
'is-connected': this.connectionState === 'connected',
'is-disconnected': this.connectionState === 'disconnected'
}
},
connectionText () {
return { connecting: '正在查询出库托盘', connected: '立库返回正常', disconnected: '立库返回异常' }[this.connectionState]
}
},
mounted() {
this.currentTime = '等待服务器时间同步...'
this.timeInterval = setInterval(() => {
this.updateTime()
}, 1000)
this.startRefreshCheck()
// 每 5 秒查一次。上一轮没回来就跳过,避免请求叠在一起 - rqrq
this.getDataList()
this.pollTimer = setInterval(() => {
this.getDataList()
}, 5000)
},
beforeDestroy() {
if (this.timeInterval) {
clearInterval(this.timeInterval)
}
if (this.refreshCheckInterval) {
clearInterval(this.refreshCheckInterval)
}
if (this.pollTimer) {
clearInterval(this.pollTimer)
}
},
methods: {
startRefreshCheck() {
this.refreshCheckInterval = setInterval(() => {
this.checkAndRefreshPage()
}, 60000)
this.checkAndRefreshPage()
},
checkAndRefreshPage() {
const now = new Date()
if (now.getHours() === 5 && now.getMinutes() === 10) {
location.reload()
}
},
// 立库 200 亮绿灯,没有托盘也亮绿灯。非 200 或网络失败亮红灯并清空表 - rqrq
getDataList() {
if (this.queryLoading) return
this.queryLoading = true
getPalletOutboundBoard({ site: '54' }).then(({ data }) => {
if (data && data.code === 0 && data.row && data.row.site === '54' && data.row.available === true && Number(data.row.resCode) === 200) {
this.connectionState = 'connected'
this.boardList = Array.isArray(data.row.rows) ? data.row.rows : []
return
}
this.connectionState = 'disconnected'
this.boardList = []
}).catch(() => {
this.connectionState = 'disconnected'
this.boardList = []
}).finally(() => {
this.queryLoading = false
})
},
formatQty (qty) {
if (qty === null || qty === undefined || qty === '') {
return '-'
}
const num = Number(qty)
if (!Number.isFinite(num)) {
return '-'
}
return parseFloat(String(num))
},
updateTime() {
const now = new Date(new Date().getTime() + this.serverTimeOffset)
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const weekDay = weekDays[now.getDay()]
this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${weekDay}`
}
}
}
</script>
<style scoped lang="scss">
/* ===== 整体容器 ===== */
.picking-board-screen {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #5f8cc3 0%, #749cc8 100%);
position: relative;
overflow: hidden;
font-family: 'Microsoft YaHei', 'PingFang SC', Arial, sans-serif;
}
/* ===== 装饰背景 ===== */
.bg-decoration {
display: none;
}
/* ===== 顶部标题区 ===== */
.screen-header {
position: relative;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 40px;
z-index: 10;
border-bottom: 2px solid rgba(23, 179, 163, 0.4);
background: linear-gradient(180deg, rgba(23, 179, 163, 0.08) 0%, transparent 100%);
}
/* CCL Logo */
.header-logo {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
z-index: 20;
}
.logo-img {
height: 40px;
width: auto;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
transition: all 0.3s ease;
&:hover {
filter: drop-shadow(0 4px 12px rgba(23, 179, 163, 0.5));
transform: scale(1.05);
}
}
.header-decoration {
display: none;
}
.header-center {
position: relative;
text-align: center;
}
.title-glow {
display: none;
}
.screen-title {
position: relative;
font-size: 34px;
font-weight: bold;
color: #ffffff;
margin: 0;
letter-spacing: 3px;
text-shadow: 0 0 20px rgba(23, 179, 163, 0.5);
}
.ws-status-dot {
display: inline-block;
width: 12px;
height: 12px;
margin-left: 12px;
border-radius: 50%;
vertical-align: middle;
background: #22c55e;
box-shadow: 0 0 10px rgba(34, 197, 94, 0.8);
&.is-connecting {
background: #f59e0b;
box-shadow: 0 0 12px rgba(245, 158, 11, 0.85);
}
&.is-disconnected {
background: #ef4444;
box-shadow: 0 0 12px rgba(239, 68, 68, 0.85);
animation: ws-status-blink 1s steps(1, end) infinite;
}
}
@keyframes ws-status-blink {
0%, 50% {
opacity: 1;
}
50.01%, 100% {
opacity: 0.2;
}
}
.title-subtitle {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
letter-spacing: 2px;
margin-top: 3px;
font-family: Arial, sans-serif;
text-transform: uppercase;
}
.header-time {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
border-radius: 8px;
backdrop-filter: blur(10px);
}
.time-icon {
font-size: 14px;
color: #17B3A3;
}
.time-text {
font-size: 16px;
color: #ffffff;
font-family: 'Consolas', 'Courier New', monospace;
font-weight: 500;
letter-spacing: 1px;
}
/* ===== 主内容区 ===== */
.screen-content {
position: relative;
z-index: 1;
padding: 8px 15px;
height: calc(100vh - 60px);
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(23, 179, 163, 0.1);
}
&::-webkit-scrollbar-thumb {
background: rgba(23, 179, 163, 0.5);
border-radius: 3px;
&:hover {
background: rgba(23, 179, 163, 0.7);
}
}
}
/* ===== 拣选面板 ===== */
.picking-panel {
width: 100%;
height: calc(100vh - 80px);
background: rgba(70, 90, 120, 0.9);
backdrop-filter: blur(10px);
border: 1px solid rgba(23, 179, 163, 0.5);
border-radius: 12px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
overflow: hidden;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
&:hover {
border-color: rgba(23, 179, 163, 0.5);
box-shadow:
0 12px 48px rgba(0, 0, 0, 0.5),
0 0 30px rgba(23, 179, 163, 0.2);
transform: translateY(-2px);
}
}
/* ===== 面板标题栏 ===== */
.panel-title-bar {
background: linear-gradient(135deg, rgba(23, 179, 163, 0.3) 0%, rgba(23, 179, 163, 0.15) 100%);
border-bottom: 1px solid rgba(23, 179, 163, 0.3);
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-left {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.title-icon {
font-size: 16px;
color: #64D8CB;
}
.title-text {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.title-main {
font-size: 24px;
font-weight: bold;
color: #ffffff;
text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
}
/* ===== 数据表格 ===== */
.panel-table {
padding: 10px;
flex: 1;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(23, 179, 163, 0.05);
}
&::-webkit-scrollbar-thumb {
background: rgba(23, 179, 163, 0.3);
border-radius: 3px;
&:hover {
background: rgba(23, 179, 163, 0.5);
}
}
}
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
thead {
tr {
background: linear-gradient(135deg, rgba(23, 179, 163, 0.25) 0%, rgba(23, 179, 163, 0.15) 100%);
}
th {
padding: 5px 6px;
color: #fcfdfd;
font-size: 16px;
font-weight: bold;
text-align: center;
border-bottom: 2px solid rgba(23, 179, 163, 0.4);
text-shadow: 0 0 10px rgba(100, 216, 203, 0.5);
white-space: nowrap;
&:first-child {
border-top-left-radius: 8px;
}
&:last-child {
border-top-right-radius: 8px;
}
}
}
tbody {
tr {
background: rgba(60, 80, 105, 0.6);
transition: all 0.3s ease;
&:nth-child(even) {
background: rgba(70, 90, 115, 0.7);
}
&:hover {
background: rgba(23, 179, 163, 0.15);
box-shadow: 0 4px 12px rgba(23, 179, 163, 0.2);
}
&:last-child {
td:first-child {
border-bottom-left-radius: 8px;
}
td:last-child {
border-bottom-right-radius: 8px;
}
}
}
td {
padding: 4px 6px;
color: rgba(255, 255, 255, 0.9);
font-size: 15px;
border-bottom: 1px solid rgba(23, 179, 163, 0.15);
&.text-center {
text-align: center;
}
&.text-left {
text-align: left;
}
&.text-right {
text-align: right;
}
}
}
}
/* ===== 状态徽章 ===== */
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: bold;
text-align: center;
min-width: 80px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
&.status-success {
background: linear-gradient(135deg, #10b981, #34d399);
color: #ffffff;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
}
&.status-warning {
background: linear-gradient(135deg, #10b981, #34d399);
color: #ffffff;
box-shadow: 0 0 15px rgba(16, 185, 129, 0.5);
}
&.status-pending {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
color: #ffffff;
box-shadow: 0 0 15px rgba(245, 158, 11, 0.5);
}
}
/* ===== 底部装饰 ===== */
.screen-footer {
display: none;
}
/* ===== 响应式适配 ===== */
@media screen and (max-width: 1600px) {
.screen-title {
font-size: 30px;
letter-spacing: 3px;
}
.title-main {
font-size: 22px;
}
}
@media screen and (min-width: 2560px) {
.screen-title {
font-size: 38px;
}
.panel-title-bar {
padding: 16px 24px;
}
.title-main {
font-size: 28px;
}
.data-table {
thead th {
font-size: 20px;
padding: 6px 12px;
}
tbody td {
font-size: 20px;
padding: 4px 12px;
}
}
.status-badge {
font-size: 18px;
padding: 4px 10px;
min-width: 100px;
}
}
</style>
Loading…
Cancel
Save