feat: initialize drainage network frontend

This commit is contained in:
2026-07-10 16:16:17 +08:00
commit 66de96d9e4
133 changed files with 33057 additions and 0 deletions
@@ -0,0 +1,202 @@
import type {
ScheduledConditionRecord,
ScheduledConditionStatus,
ScheduledConditionTaskId
} from "@/features/workbench/types";
export type RunningWorkflowStepDefinition = {
id: string;
title: string;
detail: string;
};
export type RunningWorkflowDefinition = {
title: string;
reportLabel: string;
steps: RunningWorkflowStepDefinition[];
};
export type RunningWorkflowStepStatus = "done" | "current" | "pending";
export const runningWorkflowDefinitions: Record<ScheduledConditionTaskId, RunningWorkflowDefinition> = {
"scada-diagnosis": {
title: "SCADA 诊断流程",
reportLabel: "生成诊断报告",
steps: [
{
id: "ingest-telemetry",
title: "读取 SCADA 遥测",
detail: "拉取压力、流量和关键阀门测点,校验时间戳与回传频率。"
},
{
id: "freshness-check",
title: "检查新鲜度完整率",
detail: "按测点组计算缺测、延迟和连续异常样本比例。"
},
{
id: "consistency-check",
title: "压流一致性诊断",
detail: "比对压力变化、流量变化和相邻测点趋势是否符合水力逻辑。"
},
{
id: "isolate-sources",
title: "标记异常数据源",
detail: "圈定疑似通信异常或越界测点,给后续模拟和调度提供屏蔽建议。"
},
{
id: "diagnosis-report",
title: "整理诊断结论",
detail: "汇总数据质量、异常证据和是否允许进入后续工况任务。"
}
]
},
"smart-dispatch": {
title: "智能调度流程",
reportLabel: "生成调度指令",
steps: [
{
id: "read-condition",
title: "读取实时工况",
detail: "汇总目标分区压力、流量、泵站出水和阀门开度。"
},
{
id: "candidate-actions",
title: "生成候选动作",
detail: "枚举泵站目标压力、阀门边界和高频复核测点的可执行动作。"
},
{
id: "simulate-impact",
title: "模拟影响范围",
detail: "预估指令执行后的压力改善、分区压差和关键用户影响。"
},
{
id: "rank-actions",
title: "排序调度方案",
detail: "按改善幅度、操作代价和数据可信度筛选建议指令。"
},
{
id: "dispatch-draft",
title: "形成复核清单",
detail: "输出待调度员确认的管网要素调整指令和复核条件。"
}
]
},
"network-simulation": {
title: "定时模拟流程",
reportLabel: "生成模拟报告",
steps: [
{
id: "sync-boundary",
title: "同步模型边界",
detail: "读取最新泵站启停、阀门开度、需水量和 SCADA 边界条件。"
},
{
id: "run-hydraulic-model",
title: "运行水力模型",
detail: "滚动计算全网节点压力、管段流量和分区边界平衡。"
},
{
id: "compare-telemetry",
title: "比对实测偏差",
detail: "将模拟压力、流量与在线测点回传值进行偏差校验。"
},
{
id: "locate-deviation",
title: "定位偏差来源",
detail: "识别模型参数、边界条件或遥测源导致的主要偏差区域。"
},
{
id: "simulation-report",
title: "输出模型复核",
detail: "给出可信度、关注指标和是否允许用于调度决策。"
}
]
},
"pump-energy": {
title: "泵站能耗流程",
reportLabel: "生成能耗报告",
steps: [
{
id: "read-pump-load",
title: "读取泵组负载",
detail: "采集泵站频率、电流、出水压力、出水量和启停记录。"
},
{
id: "calculate-energy",
title: "计算单位电耗",
detail: "按当前窗口排水量折算单位排水电耗和效率偏差。"
},
{
id: "compare-efficiency",
title: "比对经济区间",
detail: "结合泵组效率曲线判断当前组合是否处于低效并联区间。"
},
{
id: "pressure-guard",
title: "校验供压约束",
detail: "复核节能建议不会引发末端低压或分区边界异常。"
},
{
id: "energy-advice",
title: "形成节能建议",
detail: "输出启停优化、压力设定和继续观察条件。"
}
]
}
};
export function getRunningWorkflowDefinition(condition: ScheduledConditionRecord) {
return runningWorkflowDefinitions[condition.taskId];
}
export function getRunningWorkflowState(
condition: ScheduledConditionRecord,
workflow: RunningWorkflowDefinition,
nowMs: number
) {
if (condition.status !== "running") {
return { currentStepIndex: workflow.steps.length - 1, progress: getCompletedProgress(condition.status) };
}
const scheduledAtMs = Date.parse(condition.scheduledAt);
const startedAtMs = Number.isFinite(scheduledAtMs) ? scheduledAtMs : nowMs;
const durationMs = Math.max(condition.durationMinutes ?? 1, 1) * 60_000;
const elapsedMs = Math.max(0, nowMs - startedAtMs);
const rawRatio = elapsedMs / durationMs;
const runningRatio = Math.min(Math.max(rawRatio, 0), 0.999);
const currentStepIndex = Math.min(
workflow.steps.length - 1,
Math.floor(runningRatio * workflow.steps.length)
);
const progress = Math.min(99, Math.max(0, Math.floor(runningRatio * 100)));
return { currentStepIndex, progress };
}
export function getWorkflowStepStatus(
condition: ScheduledConditionRecord,
stepIndex: number,
currentStepIndex: number
): RunningWorkflowStepStatus {
if (condition.status !== "running") {
return "done";
}
if (stepIndex < currentStepIndex) {
return "done";
}
if (stepIndex === currentStepIndex) {
return "current";
}
return "pending";
}
function getCompletedProgress(status: ScheduledConditionStatus) {
if (status === "error") {
return 0;
}
return 100;
}
@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { createOperationalScheduledConditions } from "./scheduled-conditions";
describe("createOperationalScheduledConditions", () => {
it("keeps a scheduled condition running during the previous gap window", () => {
const conditions = createOperationalScheduledConditions(new Date("2026-07-07T11:43:30+08:00"));
const runningCondition = conditions.find(
(condition) => condition.kind === "condition" && condition.status === "running"
);
expect(runningCondition?.title).toBe("SCADA 数据诊断");
expect(runningCondition?.durationMinutes).toBe(5);
expect(runningCondition?.scheduledAt).toContain("T11:40:");
});
it("rolls the running condition into history when the next interval starts", () => {
const conditions = createOperationalScheduledConditions(new Date("2026-07-07T11:45:30+08:00"));
const previousCondition = conditions.find(
(condition) => condition.kind === "condition" && condition.id.includes("scada-diagnosis-202607071140")
);
const currentCondition = conditions.find(
(condition) => condition.kind === "condition" && condition.id.includes("scada-diagnosis-202607071145")
);
expect(previousCondition?.status).not.toBe("running");
expect(previousCondition?.durationMinutes).toBe(5);
expect(currentCondition?.status).toBe("running");
expect(currentCondition?.durationMinutes).toBe(5);
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
import type { WorkbenchScenario, WorkbenchUser } from "../types";
export const WORKBENCH_SCENARIOS: WorkbenchScenario[] = [
{
id: "scenario-a",
name: "方案 A",
description: "关闭事故点上下游阀门,优先保障核心片区压力。",
status: "active"
},
{
id: "scenario-b",
name: "方案 B",
description: "扩大隔离范围,降低抢修期间二次爆管风险。",
status: "review"
},
{
id: "scenario-c",
name: "夜间保供",
description: "按夜间低峰工况调整泵站与分区调度策略。",
status: "draft"
}
];
export const WORKBENCH_USER: WorkbenchUser = {
name: "调度员",
role: "排水运行调度中心"
};
@@ -0,0 +1,73 @@
import type { MapAnnotation } from "../types";
export const mapAnnotations: MapAnnotation[] = [
{
id: "burst",
label: "爆管位置",
coordinate: [121.5215, 30.9084],
kind: "burst"
},
{
id: "dn600",
label: "DN600",
coordinate: [121.512, 30.9102],
kind: "pipe-label"
},
{
id: "east-gate",
label: "东直门小区",
coordinate: [121.545, 30.951],
kind: "district"
},
{
id: "impact",
label: "影响范围 约 1.82 km²",
coordinate: [121.567, 30.896],
kind: "tooltip"
}
];
export const impactAreaPolygon = {
type: "FeatureCollection" as const,
features: [
{
type: "Feature" as const,
properties: {
id: "impact-area",
label: "影响范围",
area: "1.82 km²"
},
geometry: {
type: "Polygon" as const,
coordinates: [
[
[121.5205, 30.9084],
[121.548, 30.928],
[121.604, 30.918],
[121.626, 30.886],
[121.584, 30.862],
[121.538, 30.873],
[121.506, 30.895],
[121.5205, 30.9084]
]
]
}
}
]
};
export const annotationPoints = {
type: "FeatureCollection" as const,
features: mapAnnotations.map((annotation) => ({
type: "Feature" as const,
properties: {
id: annotation.id,
label: annotation.label,
kind: annotation.kind
},
geometry: {
type: "Point" as const,
coordinates: annotation.coordinate
}
}))
};