feat: 新增产线看板功能并优化多端系统体验

1. 新增SSE看板广播机制,业务操作后主动推送刷新事件
2. 新增产线工序横道图与工位状态展示面板
3. 移除dashboard无用的three.js依赖
4. 重构WMS客户端布局与WMS前端资源哈希
5. 新增工位终端时钟图标与大屏自适应布局
6. 新增系统截图脚本与说明书生成工具
7. 修复多处代码细节与空值处理逻辑
This commit is contained in:
SunYF
2026-09-04 17:31:18 +08:00
parent b6799c9de4
commit 92f0439d89
64 changed files with 2263 additions and 633 deletions
@@ -32,6 +32,7 @@ export class DashboardDataService {
private controller: AbortController | null = null;
private timer: ReturnType<typeof setInterval> | null = null;
private pollingInFlight = false;
private changeListeners = new Set<ChangeListener>();
private modeListeners = new Set<ModeListener>();
@@ -121,13 +122,14 @@ export class DashboardDataService {
}
private async pollOnce() {
if (!this.running) return;
if (!this.running || this.pollingInFlight) return;
this.pollingInFlight = true;
try {
const res = await fetch(`${this.baseUrl}${dashboardEndpoints.snapshot}`, {
headers: { 'X-API-TOKEN': this.token },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const raw = (await res.json()) as Partial<DashboardData>;
const raw = unwrapPayload(await res.json()) as Partial<DashboardData>;
this.accept(raw, 'polling');
} catch (err) {
if (!this.running) return;
@@ -137,6 +139,8 @@ export class DashboardDataService {
`MES 不可用,已切换演示数据(${err instanceof Error ? err.message : '网络异常'}`,
);
}
} finally {
this.pollingInFlight = false;
}
}
@@ -162,12 +166,20 @@ export class DashboardDataService {
const blocks = buffer.split('\n\n');
buffer = blocks.pop() ?? '';
for (const block of blocks) {
const dataLine = block.split('\n').find((l) => l.startsWith('data:'));
const lines = block.split('\n');
const dataLine = lines.find((l) => l.startsWith('data:'));
if (!dataLine) continue;
const payload = dataLine.replace(/^data:\s*/, '').trim();
if (!payload) continue;
// 事件驱动:业务数据落库后后端广播 dashboard.updated(空载荷),
// 收到即拉一次最新快照,实时性优于轮询周期。
const evtLine = lines.find((l) => l.startsWith('event:'));
if (evtLine && evtLine.includes('dashboard.updated')) {
void this.pollOnce();
continue;
}
try {
const parsed = JSON.parse(payload) as Partial<DashboardData>;
const parsed = unwrapPayload(JSON.parse(payload)) as Partial<DashboardData>;
// 忽略 hello / 心跳等无数据载荷(无 production 且无 updatedAt
if (!parsed || typeof parsed !== 'object' || (!parsed.production && !parsed.updatedAt)) {
continue;
@@ -210,6 +222,15 @@ function isEmpty(d: DashboardData): boolean {
return !d.progress?.orderNo && (d.production?.outputToday ?? 0) === 0;
}
/** 后端响应统一为 {code,message,data},取 data 层;SSE 心跳推的是裸对象,两种都兼容 */
function unwrapPayload(j: unknown): unknown {
if (j && typeof j === 'object' && !Array.isArray(j)) {
const o = j as { code?: unknown; data?: unknown };
if (o.code !== undefined && 'data' in o) return o.data;
}
return j;
}
/**
* 字段补全:后端可能只返回部分字段,缺失一律补零值,
* 避免前端到处判空,也避免 undefined 传进图表。
@@ -259,6 +280,7 @@ function normalize(raw: Partial<DashboardData>): DashboardData {
status: pr.status ?? '',
traceStepCount: pr.traceStepCount ?? 0,
traceable: pr.traceable ?? false,
steps: pr.steps ?? [],
},
traces: raw.traces ?? [],
trends: raw.trends ?? { production: [] },