11 changed files with 1909 additions and 695 deletions
-
4src/api/haianWarehouse/autoJ1SortingBoard.js
-
4src/api/haianWarehouse/autoJ2SortingBoard.js
-
4src/api/haianWarehouse/autoSortingBoard.js
-
4src/api/haianWarehouse/errorAreaBoard.js
-
4src/api/haianWarehouse/palletOutboundBoard.js
-
7src/router/index.js
-
342src/views/modules/haianWarehouse/dashboard/autoJ1SortingBoard54.vue
-
342src/views/modules/haianWarehouse/dashboard/autoJ2SortingBoard54.vue
-
702src/views/modules/haianWarehouse/dashboard/autoSortingBoard54.vue
-
597src/views/modules/haianWarehouse/dashboard/errorAreaBoard54.vue
-
594src/views/modules/haianWarehouse/dashboard/palletOutboundBoard54.vue
@ -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) |
|||
@ -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) |
|||
@ -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) |
|||
@ -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) |
|||
@ -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) |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
|
|||
@ -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> |
|||
|
|||
@ -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> |
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue