feat(history): align units and history data

This commit is contained in:
2026-09-14 12:30:57 +08:00
parent 35b6819459
commit 08ddb4453d
21 changed files with 1849 additions and 3618 deletions
+35
View File
@@ -0,0 +1,35 @@
import { toTimeSeriesPoints, type ElementHistorySeries } from "./elementHistory";
describe("element history display conversion", () => {
it("converts every series through the shared unit utility", () => {
const series: ElementHistorySeries[] = [
{
element_id: "P-1",
element_type: "pipe",
device_id: null,
metric: "flow",
source: "realtime_simulation",
source_unit: "MLD",
display_unit: "m³/h",
unit_inferred: false,
points: [{ time: "2026-09-01T00:00:00Z", value: 2 }],
},
{
element_id: "J-1",
element_type: "junction",
device_id: "pressure-1",
metric: "pressure",
source: "scada_cleaned",
source_unit: "KPA",
display_unit: "m",
unit_inferred: false,
points: [{ time: "2026-09-01T00:00:00Z", value: 10 }],
},
];
const points = toTimeSeriesPoints(series);
expect(points[0].timestamp).toBe("2026-09-01T00:00:00Z");
expect(points[0].values["P-1_sim"]).toBeCloseTo(83.3333333334);
expect(points[0].values["pressure-1_clean"]).toBeCloseTo(1.019716213);
});
});
+126
View File
@@ -0,0 +1,126 @@
import config from "@/config/config";
import { apiFetch } from "@/lib/apiFetch";
import { toDisplayValue } from "@/utils/units";
export type HistoryElementType = "pipe" | "junction";
export type HistoryMode =
| "observed"
| "realtime_comparison"
| "analysis_comparison";
export type HistoryMetric = "flow" | "pressure";
export type HistorySource =
| "scada_raw"
| "scada_cleaned"
| "realtime_simulation"
| "analysis_simulation";
export interface ElementHistoryTarget {
element_id: string;
element_type: HistoryElementType;
device_ids?: string[];
}
export interface ElementHistorySeries {
element_id: string;
element_type: HistoryElementType;
device_id: string | null;
metric: HistoryMetric;
source: HistorySource;
source_unit: string;
display_unit: string;
unit_inferred: boolean;
points: Array<{ time: string; value: number | null }>;
}
export interface TimeSeriesPoint {
timestamp: string;
values: Record<string, number | null | undefined>;
}
export interface ElementHistoryResult {
points: TimeSeriesPoint[];
series: ElementHistorySeries[];
}
const SOURCE_SUFFIX: Record<HistorySource, string> = {
scada_raw: "raw",
scada_cleaned: "clean",
realtime_simulation: "sim",
analysis_simulation: "scheme_sim",
};
export const historySeriesKey = (series: ElementHistorySeries) =>
`${series.device_id ?? series.element_id}_${SOURCE_SUFFIX[series.source]}`;
export const historySourceLabel = (source: HistorySource) => {
switch (source) {
case "scada_raw":
return "原始监测";
case "scada_cleaned":
return "清洗监测";
case "realtime_simulation":
return "实时模拟";
case "analysis_simulation":
return "方案模拟";
}
};
export const historySeriesLabel = (series: ElementHistorySeries) =>
`${series.device_id ?? series.element_id} (${historySourceLabel(series.source)})`;
export const toTimeSeriesPoints = (
seriesList: ElementHistorySeries[],
): TimeSeriesPoint[] => {
const timeMap = new Map<string, Record<string, number | null>>();
seriesList.forEach((series) => {
const key = historySeriesKey(series);
series.points.forEach((point) => {
const values = timeMap.get(point.time) ?? {};
values[key] = toDisplayValue(
point.value,
series.metric,
series.source_unit,
);
timeMap.set(point.time, values);
});
});
return Array.from(timeMap, ([timestamp, values]) => ({ timestamp, values })).sort(
(left, right) =>
new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime(),
);
};
export const fetchElementHistory = async (
elements: ElementHistoryTarget[],
range: { from: Date; to: Date },
mode: HistoryMode,
runId?: string,
signal?: AbortSignal,
): Promise<ElementHistoryResult> => {
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/timeseries/views/element-history/query`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
start_time: range.from.toISOString(),
end_time: range.to.toISOString(),
mode,
...(runId ? { run_id: runId } : {}),
elements,
}),
signal,
},
);
if (!response.ok) {
const problem = await response.json().catch(() => null);
const detail =
typeof problem?.detail === "string"
? problem.detail
: `HTTP ${response.status}`;
throw new Error(`历史数据请求失败: ${detail}`);
}
const payload = (await response.json()) as { series?: ElementHistorySeries[] };
const series = Array.isArray(payload.series) ? payload.series : [];
return { series, points: toTimeSeriesPoints(series) };
};