From eebac8f8b06722756605847c219581c47469e607 Mon Sep 17 00:00:00 2001 From: JIANG Date: Thu, 6 Nov 2025 16:21:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AE=BE=E5=A4=87=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E6=95=B0=E6=8D=AE=E6=BA=90=EF=BC=8C=E5=B9=B6=E5=9C=A8?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=A4=9A=E9=80=89=E6=97=B6=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=BA=90=E5=88=87=E6=8D=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/olmap/SCADADataPanel.tsx | 358 ++++++++++++++++++------ 1 file changed, 278 insertions(+), 80 deletions(-) diff --git a/src/components/olmap/SCADADataPanel.tsx b/src/components/olmap/SCADADataPanel.tsx index b8e1ae1..810c6dd 100644 --- a/src/components/olmap/SCADADataPanel.tsx +++ b/src/components/olmap/SCADADataPanel.tsx @@ -93,26 +93,28 @@ const fetchFromBackend = async ( const cleaningSCADAUrl = `${config.backendUrl}/querycleaningscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`; // 原始数据 const rawSCADAUrl = `${config.backendUrl}/queryscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`; + // 模拟数据接口 + const simulationSCADAUrl = `${config.backendUrl}/querysimulationscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`; try { - const response = await fetch(cleaningSCADAUrl); + let response; + response = await fetch(cleaningSCADAUrl); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); - const transformedData = transformBackendData(data, deviceIds); + let transformedData = transformBackendData(data, deviceIds); // 如果清洗数据接口返回空结果,使用原始数据接口 if (transformedData.length === 0) { console.log("[SCADADataPanel] 清洗数据接口无结果,使用原始数据接口"); - const rawResponse = await fetch(rawSCADAUrl); - if (!rawResponse.ok) { - throw new Error(`HTTP error! status: ${rawResponse.status}`); + response = await fetch(rawSCADAUrl); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } - const originData = await rawResponse.json(); - return transformBackendData(originData, deviceIds); + const data = await response.json(); + transformedData = transformBackendData(data, deviceIds); } - return transformedData; } catch (error) { console.error("[SCADADataPanel] 从后端获取数据失败:", error); @@ -129,11 +131,7 @@ const transformBackendData = ( deviceIds: string[] ): TimeSeriesPoint[] => { // 处理后端返回的对象格式: { deviceId: [{time: "...", value: ...}] } - if ( - backendData && - typeof backendData === "object" && - !Array.isArray(backendData) - ) { + if (backendData && !Array.isArray(backendData)) { // 检查是否是设备ID为键的对象格式 const hasDeviceKeys = deviceIds.some((id) => id in backendData); @@ -174,32 +172,6 @@ const transformBackendData = ( return result; } } - - // 如果后端返回的是数组格式,每个元素包含时间戳和各设备值 - if (Array.isArray(backendData)) { - return backendData.map((item: any) => ({ - timestamp: item.timestamp || item.time || item._time, - values: deviceIds.reduce>((acc, id) => { - acc[id] = item[id] ?? item.values?.[id] ?? null; - return acc; - }, {}), - })); - } - - // 如果后端返回的是对象格式,包含 timestamps 和 data - if (backendData && backendData.timestamps && backendData.data) { - return backendData.timestamps.map((timestamp: string, index: number) => { - const values = deviceIds.reduce>( - (acc, id) => { - acc[id] = backendData.data[id]?.[index] ?? null; - return acc; - }, - {} - ); - return { timestamp, values }; - }); - } - // 默认返回空数组 console.warn("[SCADADataPanel] 未知的后端数据格式:", backendData); return []; @@ -223,7 +195,8 @@ const ensureValidRange = ( const buildDataset = ( points: TimeSeriesPoint[], deviceIds: string[], - fractionDigits: number + fractionDigits: number, + showCleaning: boolean ) => { return points.map((point) => { const entry: Record = { @@ -231,15 +204,30 @@ const buildDataset = ( label: formatTimestamp(point.timestamp), }; - deviceIds.forEach((id) => { - const value = point.values[id]; - entry[id] = - typeof value === "number" - ? Number.isFinite(value) - ? parseFloat(value.toFixed(fractionDigits)) - : null - : value ?? null; - }); + if (showCleaning) { + deviceIds.forEach((id) => { + ["raw", "clean", "sim"].forEach((suffix) => { + const key = `${id}_${suffix}`; + const value = point.values[key]; + entry[key] = + typeof value === "number" + ? Number.isFinite(value) + ? parseFloat(value.toFixed(fractionDigits)) + : null + : value ?? null; + }); + }); + } else { + deviceIds.forEach((id) => { + const value = point.values[id]; + entry[id] = + typeof value === "number" + ? Number.isFinite(value) + ? parseFloat(value.toFixed(fractionDigits)) + : null + : value ?? null; + }); + } return entry; }); @@ -271,6 +259,82 @@ const SCADADataPanel: React.FC = ({ const { open } = useNotification(); const { data: user } = useGetIdentity(); + const customFetcher = useMemo(() => { + if (!showCleaning) { + return fetchTimeSeriesData; + } + + return async ( + deviceIds: string[], + range: { from: Date; to: Date } + ): Promise => { + const ids = deviceIds.join(","); + const starttime = dayjs(range.from).format("YYYY-MM-DD HH:mm:ss"); + const endtime = dayjs(range.to).format("YYYY-MM-DD HH:mm:ss"); + + const cleaningUrl = `${config.backendUrl}/querycleaningscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`; + const rawUrl = `${config.backendUrl}/queryscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`; + const simUrl = `${config.backendUrl}/querysimulationscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`; + + try { + const [cleanRes, rawRes, simRes] = await Promise.all([ + fetch(cleaningUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + fetch(rawUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + fetch(simUrl) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null), + ]); + + const timeMap = new Map>(); + + const processData = (data: any, suffix: string) => { + if (!data) return; + deviceIds.forEach((deviceId) => { + const deviceData = data[deviceId]; + if (Array.isArray(deviceData)) { + deviceData.forEach((item: any) => { + const timestamp = item.time || item.timestamp || item._time; + if (timestamp) { + if (!timeMap.has(timestamp)) { + timeMap.set(timestamp, {}); + } + const values = timeMap.get(timestamp)!; + values[`${deviceId}_${suffix}`] = + typeof item.value === "number" ? item.value : null; + } + }); + } + }); + }; + + processData(cleanRes, "clean"); + processData(rawRes, "raw"); + processData(simRes, "sim"); + + 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); + throw error; + } + }; + }, [showCleaning, fetchTimeSeriesData]); + const [from, setFrom] = useState(() => dayjs().subtract(1, "day")); const [to, setTo] = useState(() => dayjs()); const [activeTab, setActiveTab] = useState(defaultTab); @@ -280,6 +344,9 @@ const SCADADataPanel: React.FC = ({ const [isExpanded, setIsExpanded] = useState(true); const [deviceLabels, setDeviceLabels] = useState>({}); const [isCleaning, setIsCleaning] = useState(false); + const [selectedSource, setSelectedSource] = useState<"raw" | "clean" | "sim">( + "raw" + ); // 获取 SCADA 设备信息,生成 deviceLabels useEffect(() => { @@ -321,8 +388,8 @@ const SCADADataPanel: React.FC = ({ const hasData = timeSeries.length > 0; const dataset = useMemo( - () => buildDataset(timeSeries, deviceIds, fractionDigits), - [timeSeries, deviceIds, fractionDigits] + () => buildDataset(timeSeries, deviceIds, fractionDigits, showCleaning), + [timeSeries, deviceIds, fractionDigits, showCleaning] ); const handleFetch = useCallback( @@ -339,7 +406,7 @@ const SCADADataPanel: React.FC = ({ try { const { from: rangeFrom, to: rangeTo } = normalizedRange; - const result = await fetchTimeSeriesData(deviceIds, { + const result = await customFetcher(deviceIds, { from: rangeFrom.toDate(), to: rangeTo.toDate(), }); @@ -350,7 +417,7 @@ const SCADADataPanel: React.FC = ({ setLoadingState("error"); } }, - [deviceIds, fetchTimeSeriesData, hasDevices, normalizedRange] + [deviceIds, customFetcher, hasDevices, normalizedRange] ); // 处理数据清洗 @@ -450,22 +517,84 @@ const SCADADataPanel: React.FC = ({ }, ]; - const dynamic = deviceIds.map((id) => ({ - field: id, - headerName: deviceLabels?.[id] ?? id, - minWidth: 140, - flex: 1, - valueFormatter: (value: any) => { - if (value === null || value === undefined) return "--"; - if (Number.isFinite(Number(value))) { - return Number(value).toFixed(fractionDigits); + const dynamic = (() => { + if (showCleaning) { + if (deviceIds.length === 1) { + return deviceIds.flatMap((id) => [ + { + field: `${id}_raw`, + headerName: `${deviceLabels?.[id] ?? id} (原始)`, + minWidth: 140, + flex: 1, + valueFormatter: (value: any) => { + if (value === null || value === undefined) return "--"; + if (Number.isFinite(Number(value))) { + return Number(value).toFixed(fractionDigits); + } + return String(value); + }, + }, + { + field: `${id}_clean`, + headerName: `${deviceLabels?.[id] ?? id} (清洗)`, + minWidth: 140, + flex: 1, + valueFormatter: (value: any) => { + if (value === null || value === undefined) return "--"; + if (Number.isFinite(Number(value))) { + return Number(value).toFixed(fractionDigits); + } + return String(value); + }, + }, + { + field: `${id}_sim`, + headerName: `${deviceLabels?.[id] ?? id} (模拟)`, + minWidth: 140, + flex: 1, + valueFormatter: (value: any) => { + if (value === null || value === undefined) return "--"; + if (Number.isFinite(Number(value))) { + return Number(value).toFixed(fractionDigits); + } + return String(value); + }, + }, + ]); + } else { + return deviceIds.map((id) => ({ + field: `${id}_${selectedSource}`, + headerName: deviceLabels?.[id] ?? id, + minWidth: 140, + flex: 1, + valueFormatter: (value: any) => { + if (value === null || value === undefined) return "--"; + if (Number.isFinite(Number(value))) { + return Number(value).toFixed(fractionDigits); + } + return String(value); + }, + })); } - return String(value); - }, - })); + } else { + return deviceIds.map((id) => ({ + field: id, + headerName: deviceLabels?.[id] ?? id, + minWidth: 140, + flex: 1, + valueFormatter: (value: any) => { + if (value === null || value === undefined) return "--"; + if (Number.isFinite(Number(value))) { + return Number(value).toFixed(fractionDigits); + } + return String(value); + }, + })); + } + })(); return [...base, ...dynamic]; - }, [deviceIds, deviceLabels, fractionDigits]); + }, [deviceIds, deviceLabels, fractionDigits, showCleaning, selectedSource]); const rows = useMemo( () => @@ -557,17 +686,71 @@ const SCADADataPanel: React.FC = ({ }, }, ]} - series={deviceIds.map((id, index) => ({ - dataKey: id, - label: deviceLabels?.[id] ?? id, - showMark: dataset.length < 50, // 数据点少时显示标记 - curve: "catmullRom", // 使用平滑曲线 - color: colors[index % colors.length], - valueFormatter: (value: number | null) => - value !== null ? value.toFixed(fractionDigits) : "--", - area: false, - stack: undefined, - }))} + series={(() => { + if (showCleaning) { + if (deviceIds.length === 1) { + return deviceIds.flatMap((id, index) => [ + { + dataKey: `${id}_raw`, + label: `${deviceLabels?.[id] ?? id} (原始)`, + showMark: dataset.length < 50, + curve: "catmullRom", + color: colors[index % colors.length], + valueFormatter: (value: number | null) => + value !== null ? value.toFixed(fractionDigits) : "--", + area: false, + stack: undefined, + }, + { + dataKey: `${id}_clean`, + label: `${deviceLabels?.[id] ?? id} (清洗)`, + showMark: dataset.length < 50, + curve: "catmullRom", + color: colors[(index + 3) % colors.length], + valueFormatter: (value: number | null) => + value !== null ? value.toFixed(fractionDigits) : "--", + area: false, + stack: undefined, + }, + { + dataKey: `${id}_sim`, + label: `${deviceLabels?.[id] ?? id} (模拟)`, + showMark: dataset.length < 50, + curve: "catmullRom", + color: colors[(index + 6) % colors.length], + valueFormatter: (value: number | null) => + value !== null ? value.toFixed(fractionDigits) : "--", + area: false, + stack: undefined, + }, + ]); + } else { + return deviceIds.map((id, index) => ({ + dataKey: `${id}_${selectedSource}`, + label: deviceLabels?.[id] ?? id, + showMark: dataset.length < 50, + curve: "catmullRom", + color: colors[index % colors.length], + valueFormatter: (value: number | null) => + value !== null ? value.toFixed(fractionDigits) : "--", + area: false, + stack: undefined, + })); + } + } else { + return deviceIds.map((id, index) => ({ + dataKey: id, + label: deviceLabels?.[id] ?? id, + showMark: dataset.length < 50, + curve: "catmullRom", + color: colors[index % colors.length], + valueFormatter: (value: number | null) => + value !== null ? value.toFixed(fractionDigits) : "--", + area: false, + stack: undefined, + })); + } + })()} grid={{ vertical: true, horizontal: true }} sx={{ "& .MuiLineElement-root": { @@ -664,8 +847,6 @@ const SCADADataPanel: React.FC = ({ ); }; - const drawerWidth = 920; - return ( <> {/* 收起时的触发按钮 */} @@ -704,7 +885,7 @@ const SCADADataPanel: React.FC = ({ position: "absolute", top: 80, right: 16, - height: "760px", + height: showCleaning && deviceIds.length >= 2 ? "820px" : "760px", borderRadius: "12px", boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)", @@ -868,6 +1049,23 @@ const SCADADataPanel: React.FC = ({ + {showCleaning && deviceIds.length >= 2 && ( + + + 数据源: + + + setSelectedSource(value) + } + > + + + + + + )}