fix(3d): restore complete SCADA scene context
The scene previously returned early when realtime rows were absent and selected map-tier assets for network mode, hiding SCADA markers and station interiors and floors. Use SCADA timestamps as valid frames, render node-aligned monitor markers, and pin the full network context in the manifest regression test. Keep the timeline close action inside its drag header.
This commit is contained in:
@@ -38,7 +38,7 @@ import {
|
||||
type SceneRuntimeState,
|
||||
} from "./sceneProtocol";
|
||||
|
||||
const SCENE_URL = "/three-dimensional/zjb/v29/preview.html?host=platform2&v=32-context-opacity";
|
||||
const SCENE_URL = "/three-dimensional/zjb/v29/preview.html?host=platform2&v=34-detailed-network-context";
|
||||
const SCENE_LOAD_TIMEOUT_MS = 30_000;
|
||||
|
||||
const initialRuntimeState: SceneRuntimeState = {
|
||||
|
||||
@@ -225,14 +225,14 @@ export function ThreeDimensionalTimeline({
|
||||
"pointer-events-auto relative z-10 w-full max-w-[950px] rounded-2xl opacity-95 transition-opacity duration-200 hover:opacity-100",
|
||||
)}
|
||||
>
|
||||
<div className="timeline-drag-handle relative flex h-7 cursor-move touch-none items-center justify-center rounded-t-2xl border-b border-white/40 bg-white/10">
|
||||
<span aria-hidden="true" className="h-1 w-10 rounded-full bg-slate-400/60 transition-colors hover:bg-slate-500/70" />
|
||||
<div className="timeline-drag-handle relative flex h-9 cursor-move touch-none items-center justify-center rounded-t-2xl border-b border-white/35 bg-white/[0.08]">
|
||||
<span aria-hidden="true" className="h-1 w-10 rounded-full bg-slate-400/55 transition-colors hover:bg-slate-500/70" />
|
||||
<button
|
||||
type="button"
|
||||
aria-label="收起时间轴"
|
||||
title="收起时间轴"
|
||||
onClick={onClose}
|
||||
className="absolute right-1 top-1/2 grid h-10 w-10 -translate-y-1/2 place-items-center rounded-lg text-slate-500 transition-[transform,color,background-color] hover:bg-white/60 hover:text-slate-800 active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/40"
|
||||
className="absolute right-2 top-1/2 grid h-8 w-8 -translate-y-1/2 place-items-center rounded-full bg-white/20 text-base text-slate-600 ring-1 ring-white/55 [box-shadow:inset_0_1px_0_rgba(255,255,255,0.72),0_4px_12px_rgba(15,43,69,0.08)] after:absolute after:-inset-1 after:content-[''] transition-[transform,color,background-color,box-shadow] [@media(hover:hover)]:hover:bg-white/50 [@media(hover:hover)]:hover:text-slate-900 [@media(hover:hover)]:hover:[box-shadow:inset_0_1px_0_rgba(255,255,255,0.88),0_6px_16px_rgba(15,43,69,0.14)] active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/45"
|
||||
>
|
||||
<FiX />
|
||||
</button>
|
||||
|
||||
@@ -20,6 +20,7 @@ describe("ZJB three-dimensional runtime assets", () => {
|
||||
packageId: string;
|
||||
renderRevision: number;
|
||||
assets: Array<{ file: string; sha256: string }>;
|
||||
networkContext: string[];
|
||||
networkModel: { file: string; metadata: string; sha256: string };
|
||||
sourceCatalogFile?: string;
|
||||
bindingsFile?: string;
|
||||
@@ -28,6 +29,16 @@ describe("ZJB three-dimensional runtime assets", () => {
|
||||
expect(manifest.packageId).toBe("zjb-web-v29");
|
||||
expect(manifest.renderRevision).toBe(29);
|
||||
expect(manifest.assets).toHaveLength(24);
|
||||
expect(manifest.networkContext).toEqual([
|
||||
"detail_roof",
|
||||
"detail_facade",
|
||||
"detail_interior",
|
||||
"detail_ceiling",
|
||||
"map_platforms",
|
||||
"map_railway",
|
||||
"map_roads",
|
||||
"map_pump_shell",
|
||||
]);
|
||||
manifest.assets.forEach((asset) => {
|
||||
const assetPath = join(sceneRoot, asset.file);
|
||||
expect(existsSync(assetPath)).toBe(true);
|
||||
@@ -73,6 +84,10 @@ describe("ZJB three-dimensional runtime assets", () => {
|
||||
join(sceneRoot, "asset-inspector.mjs"),
|
||||
"utf8",
|
||||
);
|
||||
const networkStyle = readFileSync(
|
||||
join(sceneRoot, "network-style.mjs"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(preview).toContain("tjwater:zjb-scene");
|
||||
expect(preview).toContain("HOST_VERSION=2");
|
||||
@@ -83,10 +98,13 @@ describe("ZJB three-dimensional runtime assets", () => {
|
||||
expect(preview).toContain("data.type==='clear-results'");
|
||||
expect(preview).toContain("data.type!=='command'");
|
||||
expect(preview).toContain("postHost('selection-changed'");
|
||||
expect(preview).toContain("next==='network'?manifest.networkContext:manifest.mapDefault");
|
||||
expect(preview).not.toContain("window.zjbNetwork");
|
||||
expect(preview).not.toContain("resultsFile");
|
||||
expect(html).not.toContain("绑定运行结果");
|
||||
expect(html).not.toContain("resultsFile");
|
||||
expect(inspector).not.toContain("document.getElementById");
|
||||
expect(networkStyle).toContain("ScadaSensorMarkers");
|
||||
expect(networkStyle).toContain("source==='scada'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,6 +149,42 @@ describe("buildSceneFrame", () => {
|
||||
expect(frame.stats.missingLinks).toBe(2);
|
||||
});
|
||||
|
||||
it("builds a SCADA-only frame when the matching simulation frame is absent", () => {
|
||||
const frame = buildSceneFrame({
|
||||
queryTime: new Date("2026-09-14T09:30:00.000Z"),
|
||||
model,
|
||||
nodeRows: [],
|
||||
linkRows: [],
|
||||
pressureDevices: [
|
||||
{ device_id: "S-1", device_type: "pressure", node_id: "J-1" },
|
||||
],
|
||||
scadaRows: [
|
||||
{
|
||||
time: "2026-09-14T09:30:00.000Z",
|
||||
device_id: "S-1",
|
||||
monitored_value: 30.8,
|
||||
cleaned_value: 30.9,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(frame.resultTime).toBe("2026-09-14T09:30:00.000Z");
|
||||
expect(frame.payload?.nodes["J-1"]).toEqual({
|
||||
pressure: 30.9,
|
||||
source: "scada",
|
||||
deviceId: "S-1",
|
||||
});
|
||||
expect(frame.payload?.links).toEqual({});
|
||||
expect(frame.stats).toMatchObject({
|
||||
simulationNodes: 0,
|
||||
simulationLinks: 0,
|
||||
scadaOverrides: 1,
|
||||
});
|
||||
expect(frame.warnings).toEqual([
|
||||
"当前时刻无在线模拟结果,已显示 SCADA 监测点。",
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores IDs outside the scene model instead of rejecting the frame", () => {
|
||||
const frame = buildSceneFrame({
|
||||
queryTime: new Date("2026-09-11T03:00:00.000Z"),
|
||||
@@ -261,4 +297,39 @@ describe("buildSceneFrame", () => {
|
||||
"SCADA 数据暂不可用,当前仅显示模拟结果。",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the SCADA-only warning returned by the frame builder", async () => {
|
||||
mockApiFetch
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => [] })
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => [] })
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ FLOW_UNITS: "LPS", PRESSURE_UNITS: "METERS" }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => [
|
||||
{
|
||||
time: "2026-09-14T09:30:00.000Z",
|
||||
device_id: "S-1",
|
||||
monitored_value: 30.8,
|
||||
cleaned_value: 30.9,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const frame = await fetchSceneFrame({
|
||||
queryTime: new Date("2026-09-14T09:30:00.000Z"),
|
||||
model,
|
||||
pressureDevices: [
|
||||
{ device_id: "S-1", device_type: "pressure", node_id: "J-1" },
|
||||
],
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
|
||||
expect(frame.stats.scadaOverrides).toBe(1);
|
||||
expect(frame.warnings).toEqual([
|
||||
"当前时刻无在线模拟结果,已显示 SCADA 监测点。",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,6 +157,28 @@ const resolveCommonFrameTime = (
|
||||
)[0];
|
||||
};
|
||||
|
||||
const resolveNearestRowTime = <T extends { time: string }>(
|
||||
queryTime: Date,
|
||||
rows: T[],
|
||||
) => {
|
||||
const target = queryTime.getTime();
|
||||
const candidates = Array.from(
|
||||
new Set(
|
||||
rows
|
||||
.map((row) => toTimestamp(row.time))
|
||||
.filter(
|
||||
(time): time is number =>
|
||||
time !== null &&
|
||||
Math.abs(time - target) <= SCENE_FRAME_TOLERANCE_MS,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (candidates.length === 0) return null;
|
||||
return candidates.sort(
|
||||
(left, right) => Math.abs(left - target) - Math.abs(right - target),
|
||||
)[0];
|
||||
};
|
||||
|
||||
const normalizeLinkStatus = (
|
||||
value: number | null,
|
||||
): SceneLinkResult["status"] => {
|
||||
@@ -210,7 +232,13 @@ export const buildSceneFrame = ({
|
||||
simulationUnits?: NetworkResultUnits;
|
||||
}): SceneFrame => {
|
||||
const selectedTime = queryTime.toISOString();
|
||||
const frameTime = resolveCommonFrameTime(queryTime, nodeRows, linkRows);
|
||||
const simulationFrameTime = resolveCommonFrameTime(
|
||||
queryTime,
|
||||
nodeRows,
|
||||
linkRows,
|
||||
);
|
||||
const frameTime =
|
||||
simulationFrameTime ?? resolveNearestRowTime(queryTime, scadaRows);
|
||||
if (frameTime === null) {
|
||||
return {
|
||||
selectedTime,
|
||||
@@ -313,7 +341,10 @@ export const buildSceneFrame = ({
|
||||
missingLinks: Math.max(0, model.linkIds.size - Object.keys(links).length),
|
||||
ignoredElements,
|
||||
},
|
||||
warnings: [],
|
||||
warnings:
|
||||
simulationFrameTime === null
|
||||
? ["当前时刻无在线模拟结果,已显示 SCADA 监测点。"]
|
||||
: [],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -404,5 +435,5 @@ export const fetchSceneFrame = async ({
|
||||
scadaRows,
|
||||
simulationUnits: networkOptions,
|
||||
});
|
||||
return { ...frame, warnings };
|
||||
return { ...frame, warnings: [...frame.warnings, ...warnings] };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user