feat(history): align units and history data
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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) };
|
||||
};
|
||||
Reference in New Issue
Block a user