feat: 新增产线看板功能并优化多端系统体验
1. 新增SSE看板广播机制,业务操作后主动推送刷新事件 2. 新增产线工序横道图与工位状态展示面板 3. 移除dashboard无用的three.js依赖 4. 重构WMS客户端布局与WMS前端资源哈希 5. 新增工位终端时钟图标与大屏自适应布局 6. 新增系统截图脚本与说明书生成工具 7. 修复多处代码细节与空值处理逻辑
This commit is contained in:
@@ -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: [] },
|
||||
|
||||
Reference in New Issue
Block a user