From 05f8e500d634dc56bb013fd7f0e7edb8a24cd326 Mon Sep 17 00:00:00 2001 From: JIANG Date: Tue, 16 Dec 2025 17:43:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8E=86=E5=8F=B2=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=BB=84=E4=BB=B6=E4=BC=A0=E5=8F=82=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=8E=B7=E5=8F=96=E7=AD=96=E7=95=A5=E6=A8=A1=E6=8B=9F?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/OlMap/Controls/HistoryDataPanel.tsx | 271 ++++++++++++++------ 1 file changed, 199 insertions(+), 72 deletions(-) diff --git a/src/app/OlMap/Controls/HistoryDataPanel.tsx b/src/app/OlMap/Controls/HistoryDataPanel.tsx index 813b110..558a2fa 100644 --- a/src/app/OlMap/Controls/HistoryDataPanel.tsx +++ b/src/app/OlMap/Controls/HistoryDataPanel.tsx @@ -47,10 +47,19 @@ export interface TimeSeriesPoint { export interface SCADADataPanelProps { /** 选中的设备 ID 列表 */ deviceIds: string[]; - /** 自定义数据获取器,默认使用后端 API */ + /** 数据类型: realtime-查询模拟值和监测值, none-仅查询监测值, scheme-查询策略模拟值和监测值 */ + type?: "realtime" | "scheme" | "none"; + /** 策略类型 */ + scheme_type?: string; + /** 策略名称 */ + scheme_name?: string; + /** 自定义数据获取器,默认使用后端 API */ fetchTimeSeriesData?: ( deviceIds: string[], - range: { from: Date; to: Date } + range: { from: Date; to: Date }, + type?: "realtime" | "scheme" | "none", + scheme_type?: string, + scheme_name?: string ) => Promise; /** 默认展示的选项卡 */ defaultTab?: "chart" | "table"; @@ -67,7 +76,10 @@ type LoadingState = "idle" | "loading" | "success" | "error"; */ const fetchFromBackend = async ( deviceIds: string[], - range: { from: Date; to: Date } + range: { from: Date; to: Date }, + type: "realtime" | "scheme" | "none" = "realtime", + scheme_type?: string, + scheme_name?: string ): Promise => { if (deviceIds.length === 0) { return []; @@ -76,49 +88,138 @@ const fetchFromBackend = async ( const device_ids = deviceIds.join(","); const start_time = dayjs(range.from).toISOString(); const end_time = dayjs(range.to).toISOString(); + + // 监测值数据接口 + const monitoredDataUrl = `${config.BACKEND_URL}/timescaledb/scada/by-ids-field-time-range?device_ids=${device_ids}&field=monitored_value&start_time=${start_time}&end_time=${end_time}`; // 清洗数据接口 - const cleaningDataUrl = `${config.BACKEND_URL}/timescaledb/scada/by-ids-field-time-range?device_ids=${device_ids}&field=cleaned_value&start_time=${start_time}&end_time=${end_time}`; - // 原始数据 - const rawDataUrl = `${config.BACKEND_URL}/timescaledb/scada/by-ids-field-time-range?device_ids=${device_ids}&field=monitored_value&start_time=${start_time}&end_time=${end_time}`; + const cleanedDataUrl = `${config.BACKEND_URL}/timescaledb/scada/by-ids-field-time-range?device_ids=${device_ids}&field=cleaned_value&start_time=${start_time}&end_time=${end_time}`; // 模拟数据接口 const simulationDataUrl = `${config.BACKEND_URL}/timescaledb/composite/scada-simulation?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}`; + // 策略模拟数据接口 + const schemeSimulationDataUrl = `${config.BACKEND_URL}/timescaledb/composite/scada-simulation?device_ids=${device_ids}&start_time=${start_time}&end_time=${end_time}&scheme_type=${scheme_type}&scheme_name=${scheme_name}`; try { - // 优先查询清洗数据和模拟数据 - const [cleaningRes, simulationRes] = await Promise.all([ - fetch(cleaningDataUrl) - .then((r) => (r.ok ? r.json() : null)) - .catch(() => null), - fetch(simulationDataUrl) - .then((r) => (r.ok ? r.json() : null)) - .catch(() => null), - ]); + if (type === "none") { + // 查询清洗值和监测值 + const [cleanedRes, monitoredRes] = await Promise.all([ + fetch(cleanedDataUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + fetch(monitoredDataUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + ]); - const cleaningData = transformBackendData(cleaningRes, deviceIds); - const simulationData = transformBackendData(simulationRes, deviceIds); + const cleanedData = transformBackendData(cleanedRes, deviceIds); + const monitoredData = transformBackendData(monitoredRes, deviceIds); - // 如果清洗数据有数据,返回清洗和模拟数据 - if (cleaningData.length > 0) { return mergeTimeSeriesData( - cleaningData, - simulationData, + cleanedData, + monitoredData, deviceIds, "clean", - "sim" + "monitored" ); + } else if (type === "scheme") { + // 查询策略模拟值、清洗值和监测值 + const [cleanedRes, monitoredRes, schemeSimRes] = await Promise.all([ + fetch(cleanedDataUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + fetch(monitoredDataUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + fetch(schemeSimulationDataUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + ]); + + const cleanedData = transformBackendData(cleanedRes, deviceIds); + const monitoredData = transformBackendData(monitoredRes, deviceIds); + const schemeSimData = transformBackendData(schemeSimRes, deviceIds); + + // 合并三组数据 + const timeMap = new Map>(); + + [cleanedData, monitoredData, schemeSimData].forEach((data, index) => { + const suffix = ["clean", "monitored", "scheme_sim"][index]; + data.forEach((point) => { + if (!timeMap.has(point.timestamp)) { + timeMap.set(point.timestamp, {}); + } + const values = timeMap.get(point.timestamp)!; + deviceIds.forEach((deviceId) => { + const value = point.values[deviceId]; + if (value !== undefined) { + values[`${deviceId}_${suffix}`] = value; + } + }); + }); + }); + + const result = Array.from(timeMap.entries()).map( + ([timestamp, values]) => ({ + timestamp, + values, + }) + ); + + result.sort( + (a, b) => + new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ); + + return result; } else { - // 如果清洗数据没有数据,查询原始数据,返回模拟和原始数据 - const rawRes = await fetch(rawDataUrl) - .then((r) => (r.ok ? r.json() : null)) - .catch(() => null); - const rawData = transformBackendData(rawRes, deviceIds); - return mergeTimeSeriesData( - simulationData, - rawData, - deviceIds, - "sim", - "raw" + // realtime: 查询模拟值、清洗值和监测值 + const [cleanedRes, monitoredRes, simulationRes] = await Promise.all([ + fetch(cleanedDataUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + fetch(monitoredDataUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + fetch(simulationDataUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + ]); + + const cleanedData = transformBackendData(cleanedRes, deviceIds); + const monitoredData = transformBackendData(monitoredRes, deviceIds); + const simulationData = transformBackendData(simulationRes, deviceIds); + + // 合并三组数据 + const timeMap = new Map>(); + + [cleanedData, monitoredData, simulationData].forEach((data, index) => { + const suffix = ["clean", "monitored", "sim"][index]; + data.forEach((point) => { + if (!timeMap.has(point.timestamp)) { + timeMap.set(point.timestamp, {}); + } + const values = timeMap.get(point.timestamp)!; + deviceIds.forEach((deviceId) => { + const value = point.values[deviceId]; + if (value !== undefined) { + values[`${deviceId}_${suffix}`] = value; + } + }); + }); + }); + + const result = Array.from(timeMap.entries()).map( + ([timestamp, values]) => ({ + timestamp, + values, + }) ); + + result.sort( + (a, b) => + new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ); + + return result; } } catch (error) { console.error("[SCADADataPanel] 从后端获取数据失败:", error); @@ -250,7 +351,7 @@ const buildDataset = ( }; deviceIds.forEach((id) => { - ["raw", "clean", "sim"].forEach((suffix) => { + ["clean", "monitored", "sim", "scheme_sim"].forEach((suffix) => { const key = `${id}_${suffix}`; const value = point.values[key]; if (value !== undefined && value !== null) { @@ -284,6 +385,9 @@ const emptyStateMessages: Record< const SCADADataPanel: React.FC = ({ deviceIds, + type = "realtime", + scheme_type, + scheme_name, fetchTimeSeriesData = defaultFetcher, defaultTab = "chart", fractionDigits = 2, @@ -361,10 +465,16 @@ const SCADADataPanel: React.FC = ({ setError(null); try { const { from: rangeFrom, to: rangeTo } = normalizedRange; - const result = await customFetcher(deviceIds, { - from: rangeFrom.toDate(), - to: rangeTo.toDate(), - }); + const result = await customFetcher( + deviceIds, + { + from: rangeFrom.toDate(), + to: rangeTo.toDate(), + }, + type, + scheme_type, + scheme_name + ); setTimeSeries(result); setLoadingState("success"); } catch (err) { @@ -372,7 +482,15 @@ const SCADADataPanel: React.FC = ({ setLoadingState("error"); } }, - [deviceIds, customFetcher, hasDevices, normalizedRange] + [ + deviceIds, + customFetcher, + hasDevices, + normalizedRange, + type, + scheme_type, + scheme_name, + ] ); // 设备变化时自动查询 @@ -479,40 +597,49 @@ const SCADADataPanel: React.FC = ({ const getSeries = () => { return deviceIds.flatMap((id, index) => { const series = []; - ["raw", "clean", "sim"].forEach((suffix, sIndex) => { - const key = `${id}_${suffix}`; - const hasData = dataset.some( - (item) => item[key] !== null && item[key] !== undefined - ); - if (hasData) { - series.push({ - name: `${deviceLabels?.[id] ?? id} (${ - suffix === "raw" ? "原始" : suffix === "clean" ? "清洗" : "模拟" - })`, - type: "line", - symbol: "none", - sampling: "lttb", - itemStyle: { - color: colors[(index * 3 + sIndex) % colors.length], - }, - data: dataset.map((item) => item[key]), - areaStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { - offset: 0, - color: colors[(index * 3 + sIndex) % colors.length], - }, - { - offset: 1, - color: "rgba(255, 255, 255, 0)", - }, - ]), - opacity: 0.3, - }, - }); + ["clean", "monitored", "sim", "scheme_sim"].forEach( + (suffix, sIndex) => { + const key = `${id}_${suffix}`; + const hasData = dataset.some( + (item) => item[key] !== null && item[key] !== undefined + ); + if (hasData) { + const displayName = + suffix === "clean" + ? "清洗值" + : suffix === "monitored" + ? "监测值" + : suffix === "sim" + ? "模拟" + : "策略模拟"; + + series.push({ + name: `${deviceLabels?.[id] ?? id} (${displayName})`, + type: "line", + symbol: "none", + sampling: "lttb", + itemStyle: { + color: colors[(index * 4 + sIndex) % colors.length], + }, + data: dataset.map((item) => item[key]), + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: colors[(index * 4 + sIndex) % colors.length], + }, + { + offset: 1, + color: "rgba(255, 255, 255, 0)", + }, + ]), + opacity: 0.3, + }, + }); + } } - }); - // 如果没有clean/raw/sim数据,则使用fallback + ); + // 如果没有任何数据,则使用fallback if (series.length === 0) { series.push({ name: deviceLabels?.[id] ?? id,