From 4f0714b5f6cd3e07a0b9e7adbd0103852d581130 Mon Sep 17 00:00:00 2001 From: JIANG Date: Fri, 7 Nov 2025 09:57:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=9D=A2=E6=9D=BF=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E7=BC=A9=E6=94=BE=E6=BB=91=E5=9D=97=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/olmap/SCADADataPanel.tsx | 390 ++++++++++++------ src/components/olmap/SCADADeviceList.tsx | 6 +- src/components/olmap/SCADAIntegratedPanel.tsx | 74 ---- 3 files changed, 260 insertions(+), 210 deletions(-) delete mode 100644 src/components/olmap/SCADAIntegratedPanel.tsx diff --git a/src/components/olmap/SCADADataPanel.tsx b/src/components/olmap/SCADADataPanel.tsx index efff612..adad948 100644 --- a/src/components/olmap/SCADADataPanel.tsx +++ b/src/components/olmap/SCADADataPanel.tsx @@ -14,6 +14,7 @@ import { Tooltip, Typography, Drawer, + Slider, } from "@mui/material"; import { Refresh, @@ -35,7 +36,6 @@ import { GeoJSON } from "ol/format"; import { useGetIdentity } from "@refinedev/core"; import { useNotification } from "@refinedev/core"; import axios from "axios"; -import { set } from "date-fns"; dayjs.extend(utc); dayjs.extend(timezone); @@ -349,6 +349,9 @@ const SCADADataPanel: React.FC = ({ "raw" | "clean" | "sim" | "all" >("all"); + // 滑块状态:用于图表缩放 + const [zoomRange, setZoomRange] = useState<[number, number]>([0, 100]); + // 获取 SCADA 设备信息,生成 deviceLabels useEffect(() => { const fetchDeviceLabels = async () => { @@ -393,6 +396,21 @@ const SCADADataPanel: React.FC = ({ [timeSeries, deviceIds, fractionDigits, showCleaning] ); + // 根据滑块范围过滤数据集 + const filteredDataset = useMemo(() => { + if (dataset.length === 0) return dataset; + + const startIndex = Math.floor((zoomRange[0] / 100) * dataset.length); + const endIndex = Math.ceil((zoomRange[1] / 100) * dataset.length); + + return dataset.slice(startIndex, endIndex); + }, [dataset, zoomRange]); + + // 重置滑块范围当数据变化时 + useEffect(() => { + setZoomRange([0, 100]); + }, [timeSeries]); + const handleFetch = useCallback( async (reason: string) => { if (!hasDevices) { @@ -404,9 +422,6 @@ const SCADADataPanel: React.FC = ({ setLoadingState("loading"); setError(null); - if (deviceIds.length > 1) { - setSelectedSource("clean"); - } try { const { from: rangeFrom, to: rangeTo } = normalizedRange; const result = await customFetcher(deviceIds, { @@ -510,6 +525,15 @@ const SCADADataPanel: React.FC = ({ } }, [deviceIds.join(",")]); + // 当设备数量变化时,调整数据源选择 + useEffect(() => { + if (deviceIds.length > 1 && selectedSource === "all") { + setSelectedSource("clean"); + } else if (deviceIds.length === 1 && selectedSource !== "all") { + setSelectedSource("all"); + } + }, [deviceIds.length]); + const columns: GridColDef[] = useMemo(() => { const base: GridColDef[] = [ { @@ -655,50 +679,107 @@ const SCADADataPanel: React.FC = ({ "#3f51b5", // 靛蓝色 ]; + // 获取当前显示范围的时间边界 + const getTimeRangeLabel = () => { + if (filteredDataset.length === 0) return ""; + const firstTime = filteredDataset[0].time; + const lastTime = filteredDataset[filteredDataset.length - 1].time; + if (firstTime instanceof Date && lastTime instanceof Date) { + return `${dayjs(firstTime).format("MM-DD HH:mm")} ~ ${dayjs( + lastTime + ).format("MM-DD HH:mm")}`; + } + return ""; + }; + return ( - - - value instanceof Date - ? dayjs(value).format("MM-DD HH:mm") - : String(value), - tickLabelStyle: { - angle: -45, - textAnchor: "end", - fontSize: 11, - fill: "#666", + + + + value instanceof Date + ? dayjs(value).format("MM-DD HH:mm") + : String(value), + tickLabelStyle: { + angle: -45, + textAnchor: "end", + fontSize: 11, + fill: "#666", + }, }, - }, - ]} - yAxis={[ - { - label: "数值", - labelStyle: { - fontSize: 13, - fill: "#333", - fontWeight: 500, + ]} + yAxis={[ + { + label: "数值", + labelStyle: { + fontSize: 13, + fill: "#333", + fontWeight: 500, + }, + tickLabelStyle: { + fontSize: 11, + fill: "#666", + }, }, - tickLabelStyle: { - fontSize: 11, - fill: "#666", - }, - }, - ]} - series={(() => { - if (showCleaning) { - if (selectedSource === "all") { - // 全部模式:显示所有设备的三种数据 - return deviceIds.flatMap((id, index) => [ - { - dataKey: `${id}_raw`, - label: `${deviceLabels?.[id] ?? id} (原始)`, + ]} + series={(() => { + if (showCleaning) { + if (selectedSource === "all") { + // 全部模式:显示所有设备的三种数据 + 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], @@ -706,34 +787,11 @@ const SCADADataPanel: React.FC = ({ 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}`, + dataKey: id, label: deviceLabels?.[id] ?? id, showMark: dataset.length < 50, curve: "catmullRom", @@ -744,68 +802,134 @@ const SCADADataPanel: React.FC = ({ 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": { - strokeWidth: 2.5, - strokeLinecap: "round", - strokeLinejoin: "round", - }, - "& .MuiMarkElement-root": { - scale: "0.8", - strokeWidth: 2, - }, - "& .MuiChartsAxis-line": { - stroke: "#e0e0e0", - strokeWidth: 1, - }, - "& .MuiChartsAxis-tick": { - stroke: "#e0e0e0", - strokeWidth: 1, - }, - "& .MuiChartsGrid-line": { - stroke: "#f5f5f5", - strokeWidth: 1, - strokeDasharray: "3 3", - }, - }} - slotProps={{ - legend: { - direction: "row", - position: { horizontal: "middle", vertical: "bottom" }, - padding: { bottom: 2, left: 0, right: 0 }, - itemMarkWidth: 16, - itemMarkHeight: 3, - markGap: 8, - itemGap: 16, - labelStyle: { - fontSize: 12, - fill: "#333", - fontWeight: 500, + })()} + grid={{ vertical: true, horizontal: true }} + sx={{ + "& .MuiLineElement-root": { + strokeWidth: 2.5, + strokeLinecap: "round", + strokeLinejoin: "round", }, - }, - loadingOverlay: { - style: { backgroundColor: "rgba(255, 255, 255, 0.7)" }, - }, - }} - tooltip={{ - trigger: "axis", - }} - /> + "& .MuiMarkElement-root": { + scale: "0.8", + strokeWidth: 2, + }, + "& .MuiChartsAxis-line": { + stroke: "#e0e0e0", + strokeWidth: 1, + }, + "& .MuiChartsAxis-tick": { + stroke: "#e0e0e0", + strokeWidth: 1, + }, + "& .MuiChartsGrid-line": { + stroke: "#d0d0d0", + strokeWidth: 0.8, + strokeDasharray: "4 4", + }, + }} + slotProps={{ + legend: { + direction: "row", + position: { horizontal: "middle", vertical: "bottom" }, + padding: { bottom: 2, left: 0, right: 0 }, + itemMarkWidth: 16, + itemMarkHeight: 3, + markGap: 8, + itemGap: 16, + labelStyle: { + fontSize: 12, + fill: "#333", + fontWeight: 500, + }, + }, + loadingOverlay: { + style: { backgroundColor: "rgba(255, 255, 255, 0.7)" }, + }, + }} + tooltip={{ + trigger: "axis", + }} + /> + + + {/* 时间范围滑块 */} + + + + 时间范围 + + + setZoomRange(newValue as [number, number]) + } + valueLabelDisplay="auto" + valueLabelFormat={(value) => { + const index = Math.floor((value / 100) * dataset.length); + if (dataset[index] && dataset[index].time instanceof Date) { + return dayjs(dataset[index].time).format("MM-DD HH:mm"); + } + return `${value}%`; + }} + marks={[ + { + value: 0, + label: + dataset.length > 0 && dataset[0].time instanceof Date + ? dayjs(dataset[0].time).format("MM-DD HH:mm") + : "起始", + }, + { + value: 100, + label: + dataset.length > 0 && + dataset[dataset.length - 1].time instanceof Date + ? dayjs(dataset[dataset.length - 1].time).format( + "MM-DD HH:mm" + ) + : "结束", + }, + ]} + sx={{ + flex: 1, + "& .MuiSlider-thumb": { + width: 16, + height: 16, + }, + "& .MuiSlider-markLabel": { + fontSize: "0.7rem", + color: "text.secondary", + }, + }} + /> + + + {getTimeRangeLabel() && ( + + 当前显示: {getTimeRangeLabel()} (共 {filteredDataset.length}{" "} + 个数据点) + + )} + ); }; @@ -892,7 +1016,7 @@ const SCADADataPanel: React.FC = ({ position: "absolute", top: 80, right: 16, - height: "820px", + height: "860px", borderRadius: "12px", boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)", diff --git a/src/components/olmap/SCADADeviceList.tsx b/src/components/olmap/SCADADeviceList.tsx index f05f9ce..0a48efe 100644 --- a/src/components/olmap/SCADADeviceList.tsx +++ b/src/components/olmap/SCADADeviceList.tsx @@ -135,7 +135,7 @@ const SCADADeviceList: React.FC = ({ const debounceTimerRef = useRef(null); const filterBoxRef = useRef(null); - const [listHeight, setListHeight] = useState(560); + const [listHeight, setListHeight] = useState(600); // 清洗对话框状态 const [cleanDialogOpen, setCleanDialogOpen] = useState(false); @@ -748,7 +748,7 @@ const SCADADeviceList: React.FC = ({ useEffect(() => { const updateListHeight = () => { if (filterBoxRef.current) { - const drawerHeight = 820; // Drawer 总高度 + const drawerHeight = 860; // Drawer 总高度 const headerHeight = 73; // 头部高度(估算) const dividerHeight = 1; // 分隔线高度 const filterBoxHeight = filterBoxRef.current.offsetHeight; @@ -810,7 +810,7 @@ const SCADADeviceList: React.FC = ({ flexShrink: 0, "& .MuiDrawer-paper": { width: 360, - height: "820px", + height: "860px", boxSizing: "border-box", position: "absolute", top: 80, diff --git a/src/components/olmap/SCADAIntegratedPanel.tsx b/src/components/olmap/SCADAIntegratedPanel.tsx deleted file mode 100644 index 982021f..0000000 --- a/src/components/olmap/SCADAIntegratedPanel.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import React, { useCallback, useMemo, useState } from "react"; -import { Box, Stack } from "@mui/material"; -import SCADADeviceList from "./SCADADeviceList"; -import SCADADataPanel from "./SCADADataPanel"; - -/** - * 集成面板:左侧为 SCADA 设备列表(支持从地图选择),右侧为历史数据面板(曲线/表格)。 - * - 使用 SCADADeviceList 内置的地图点击选择功能 - * - 使用 SCADADataPanel 的时间段查询与图表展示 - * - 两者通过选中设备 ID 进行联动 - */ -export interface SCADAIntegratedPanelProps { - /** 初始选中设备 ID 列表 */ - initialSelection?: string[]; - /** 是否展示数据清洗相关功能(传递给两个子面板) */ - showCleaning?: boolean; - /** 是否显示右侧数据面板 */ - showDataPanel?: boolean; - /** 数据面板默认选项卡 */ - dataPanelDefaultTab?: "chart" | "table"; - /** 数据面板小数位数 */ - fractionDigits?: number; -} - -const SCADAIntegratedPanel: React.FC = ({ - initialSelection = [], - showCleaning = false, - showDataPanel = true, - dataPanelDefaultTab = "chart", - fractionDigits = 2, -}) => { - const [selectedIds, setSelectedIds] = useState(initialSelection); - // 通过变更 key 强制重新挂载 DataPanel,从而在“清洗全部”后自动刷新 - const [dataPanelKey, setDataPanelKey] = useState(0); - - const handleSelectionChange = useCallback((ids: string[]) => { - setSelectedIds(ids); - }, []); - - // 清洗全部数据后,强制刷新右侧数据面板(重新挂载触发首轮查询) - const handleCleanAllData = useCallback((_from: Date, _to: Date) => { - setDataPanelKey((k) => k + 1); - }, []); - - const hasSelection = useMemo(() => selectedIds.length > 0, [selectedIds]); - - return ( - - {/* 左侧:设备列表(内置抽屉布局) */} - - - {/* 右侧:历史数据面板(内置抽屉布局) */} - {showDataPanel && ( - - )} - - ); -}; - -export default SCADAIntegratedPanel;