feat: initialize drainage network frontend
This commit is contained in:
@@ -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
|
||||
}
|
||||
}))
|
||||
};
|
||||
Reference in New Issue
Block a user