diff --git a/src/app/(main)/network-simulation/page.tsx b/src/app/(main)/network-simulation/page.tsx index 24d3048..d454bea 100644 --- a/src/app/(main)/network-simulation/page.tsx +++ b/src/app/(main)/network-simulation/page.tsx @@ -21,10 +21,6 @@ export default function Home() { setPanelVisible(true); }, []); - const handleClosePanel = useCallback(() => { - setPanelVisible(false); - }, []); - return (
@@ -35,11 +31,7 @@ export default function Home() { onSelectionChange={handleSelectionChange} selectedDeviceIds={selectedDeviceIds} /> - +
); diff --git a/src/app/(main)/scada-data-cleaning/page.tsx b/src/app/(main)/scada-data-cleaning/page.tsx index 6d08430..2258ee8 100644 --- a/src/app/(main)/scada-data-cleaning/page.tsx +++ b/src/app/(main)/scada-data-cleaning/page.tsx @@ -20,10 +20,6 @@ export default function Home() { setPanelVisible(true); }, []); - const handleClosePanel = useCallback(() => { - setPanelVisible(false); - }, []); - return (
@@ -32,11 +28,12 @@ export default function Home() { onDeviceClick={handleDeviceClick} onSelectionChange={handleSelectionChange} selectedDeviceIds={selectedDeviceIds} + showCleaning={true} />
diff --git a/src/components/olmap/SCADADataPanel.tsx b/src/components/olmap/SCADADataPanel.tsx index 7cfa304..70ad401 100644 --- a/src/components/olmap/SCADADataPanel.tsx +++ b/src/components/olmap/SCADADataPanel.tsx @@ -8,21 +8,21 @@ import { CircularProgress, Divider, IconButton, - Paper, Stack, Tab, Tabs, Tooltip, Typography, - Collapse, + Drawer, } from "@mui/material"; import { Close, Refresh, ShowChart, TableChart, - ExpandLess, - ExpandMore, + CleaningServices, + ChevronLeft, + ChevronRight, } from "@mui/icons-material"; import { DataGrid, GridColDef } from "@mui/x-data-grid"; import { LineChart } from "@mui/x-charts"; @@ -55,12 +55,14 @@ export interface SCADADataPanelProps { ) => Promise; /** 可选:控制浮窗显示 */ visible?: boolean; - /** 可选:关闭浮窗的回调 */ - onClose?: () => void; /** 默认展示的选项卡 */ defaultTab?: "chart" | "table"; /** Y 轴数值的小数位数 */ fractionDigits?: number; + /** 是否显示清洗功能 */ + showCleaning?: boolean; + /** 清洗数据的回调 */ + onCleanData?: () => void; } type PanelTab = "chart" | "table"; @@ -242,9 +244,10 @@ const SCADADataPanel: React.FC = ({ deviceIds, fetchTimeSeriesData = defaultFetcher, visible = true, - onClose, defaultTab = "chart", fractionDigits = 2, + showCleaning = false, + onCleanData, }) => { const [from, setFrom] = useState(() => dayjs().subtract(1, "day")); const [to, setTo] = useState(() => dayjs()); @@ -560,191 +563,247 @@ const SCADADataPanel: React.FC = ({ ); }; + const drawerWidth = 920; + return ( - + {/* 收起时的触发按钮 */} + {!isExpanded && hasDevices && ( + setIsExpanded(true)} + > + + + + 历史数据 + + + + )} - sx={{ - width: "min(920px, calc(100vw - 2rem))", - maxHeight: "calc(100vh - 100px)", - }} - > - {/* Header */} - - - - - - SCADA 历史数据 - - - - - - setIsExpanded(!isExpanded)} - sx={{ color: "primary.contrastText" }} - > - {isExpanded ? : } - - - - - - - - - - - - - {/* Controls */} - - - + + {/* Header */} + + - - value && dayjs.isDayjs(value) && setFrom(value) - } - onAccept={(value) => { - if (value && dayjs.isDayjs(value) && hasDevices) { - handleFetch("date-change"); - } + + + SCADA 历史数据 + + - - value && dayjs.isDayjs(value) && setTo(value) - } - onAccept={(value) => { - if (value && dayjs.isDayjs(value) && hasDevices) { - handleFetch("date-change"); - } - }} - minDateTime={from} - slotProps={{ textField: { fullWidth: true, size: "small" } }} /> - - setActiveTab(value)} - variant="fullWidth" - > - } - iconPosition="start" - label="曲线" - /> - } - iconPosition="start" - label="表格" - /> - - - - - + + + setIsExpanded(false)} + sx={{ color: "primary.contrastText" }} + > + + - + - {!hasDevices && ( - - 未选择任何设备,无法获取数据。 - - )} - {error && ( - - 获取数据失败:{error} - - )} + {/* Controls */} + + + + + + value && dayjs.isDayjs(value) && setFrom(value) + } + onAccept={(value) => { + if (value && dayjs.isDayjs(value) && hasDevices) { + handleFetch("date-change"); + } + }} + maxDateTime={to} + slotProps={{ + textField: { fullWidth: true, size: "small" }, + }} + /> + + value && dayjs.isDayjs(value) && setTo(value) + } + onAccept={(value) => { + if (value && dayjs.isDayjs(value) && hasDevices) { + handleFetch("date-change"); + } + }} + minDateTime={from} + slotProps={{ + textField: { fullWidth: true, size: "small" }, + }} + /> + + + setActiveTab(value)} + variant="fullWidth" + > + } + iconPosition="start" + label="曲线" + /> + } + iconPosition="start" + label="表格" + /> + + + {showCleaning && ( + + + + + + )} + + + + + + + + + + + {!hasDevices && ( + + 未选择任何设备,无法获取数据。 + + )} + {error && ( + + 获取数据失败:{error} + + )} + + + + + {/* Content */} + + {loadingState === "loading" && ( + + + + )} + + {activeTab === "chart" ? renderChart() : renderTable()} + - - - - {/* Content */} - - {loadingState === "loading" && ( - - - - )} - - {activeTab === "chart" ? renderChart() : renderTable()} - - - + + ); }; diff --git a/src/components/olmap/SCADADeviceList.tsx b/src/components/olmap/SCADADeviceList.tsx index bcba84c..21b9c71 100644 --- a/src/components/olmap/SCADADeviceList.tsx +++ b/src/components/olmap/SCADADeviceList.tsx @@ -10,7 +10,6 @@ import React, { } from "react"; import { Box, - Paper, Typography, ListItem, ListItemButton, @@ -18,7 +17,6 @@ import { ListItemIcon, Chip, IconButton, - Collapse, FormControl, InputLabel, Select, @@ -28,16 +26,25 @@ import { Divider, InputBase, CircularProgress, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Alert, + Drawer, } from "@mui/material"; import { Search, MyLocation, - ExpandMore, - ExpandLess, FilterList, Clear, DeviceHub, TouchApp, + CleaningServices, + Sensors, + ChevronLeft, + ChevronRight, } from "@mui/icons-material"; import { FixedSizeList } from "react-window"; import { useNotification } from "@refinedev/core"; @@ -53,6 +60,9 @@ import { Point } from "ol/geom"; import { getVectorContext } from "ol/render"; import { unByKey } from "ol/Observable"; import config from "@/config/config"; +import dayjs, { Dayjs } from "dayjs"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; const STATUS_OPTIONS: { value: "online" | "offline" | "warning" | "error"; @@ -84,6 +94,8 @@ interface SCADADeviceListProps { multiSelect?: boolean; selectedDeviceIds?: string[]; onSelectionChange?: (ids: string[]) => void; + showCleaning?: boolean; + onCleanAllData?: (from: Date, to: Date) => void; } const SCADADeviceList: React.FC = ({ @@ -92,6 +104,8 @@ const SCADADeviceList: React.FC = ({ multiSelect = true, selectedDeviceIds, onSelectionChange, + showCleaning = false, + onCleanAllData, }) => { const [searchQuery, setSearchQuery] = useState(""); const [selectedType, setSelectedType] = useState("all"); @@ -113,6 +127,16 @@ const SCADADeviceList: React.FC = ({ const blinkListenerKeyRef = useRef(null); const debounceTimerRef = useRef(null); + const filterBoxRef = useRef(null); + const [listHeight, setListHeight] = useState(500); + + // 清洗对话框状态 + const [cleanDialogOpen, setCleanDialogOpen] = useState(false); + const [cleanStartTime, setCleanStartTime] = useState(() => + dayjs().subtract(1, "week") + ); + const [cleanEndTime, setCleanEndTime] = useState(() => dayjs()); + const [timeRangeError, setTimeRangeError] = useState(""); // 防抖更新搜索查询 const debouncedSetSearchQuery = useCallback((value: string) => { @@ -497,6 +521,67 @@ const SCADADeviceList: React.FC = ({ [map, effectiveDevices, multiSelect, open] ); + // 处理清洗对话框关闭 + const handleCleanDialogClose = useCallback(() => { + setCleanDialogOpen(false); + setTimeRangeError(""); + }, []); + + // 验证时间范围 + const validateTimeRange = useCallback((start: Dayjs, end: Dayjs): string => { + if (start.isAfter(end)) { + return "开始时间不能晚于结束时间"; + } + const daysDiff = end.diff(start, "day"); + if (daysDiff > 14) { + return "时间范围不能超过两周(14天)"; + } + return ""; + }, []); + + // 处理开始时间变化 + const handleCleanStartTimeChange = useCallback( + (newValue: Dayjs | Date | null) => { + if (newValue) { + const dayjsValue = dayjs.isDayjs(newValue) ? newValue : dayjs(newValue); + setCleanStartTime(dayjsValue); + const error = validateTimeRange(dayjsValue, cleanEndTime); + setTimeRangeError(error); + } + }, + [cleanEndTime, validateTimeRange] + ); + + // 处理结束时间变化 + const handleCleanEndTimeChange = useCallback( + (newValue: Dayjs | Date | null) => { + if (newValue) { + const dayjsValue = dayjs.isDayjs(newValue) ? newValue : dayjs(newValue); + setCleanEndTime(dayjsValue); + const error = validateTimeRange(cleanStartTime, dayjsValue); + setTimeRangeError(error); + } + }, + [cleanStartTime, validateTimeRange] + ); + + // 确认清洗 + const handleConfirmClean = useCallback(() => { + const error = validateTimeRange(cleanStartTime, cleanEndTime); + if (error) { + setTimeRangeError(error); + return; + } + onCleanAllData?.(cleanStartTime.toDate(), cleanEndTime.toDate()); + handleCleanDialogClose(); + }, [ + cleanStartTime, + cleanEndTime, + validateTimeRange, + onCleanAllData, + handleCleanDialogClose, + ]); + // 开始选择 SCADA 设备 const handleStartSelection = useCallback(() => { if (!map) return; @@ -586,352 +671,548 @@ const SCADADeviceList: React.FC = ({ }; }, []); + // 动态计算列表高度 + useEffect(() => { + const updateListHeight = () => { + if (filterBoxRef.current) { + const drawerHeight = 760; // Drawer 总高度 + const headerHeight = 73; // 头部高度(估算) + const dividerHeight = 1; // 分隔线高度 + const filterBoxHeight = filterBoxRef.current.offsetHeight; + const availableHeight = + drawerHeight - headerHeight - filterBoxHeight - dividerHeight - 8; // 减去一些边距 + setListHeight(Math.max(availableHeight, 200)); // 最小高度 200 + } + }; + + updateListHeight(); + // 使用 ResizeObserver 监听筛选框高度变化 + const resizeObserver = new ResizeObserver(updateListHeight); + if (filterBoxRef.current) { + resizeObserver.observe(filterBoxRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, [ + activeSelection.length, + searchQuery, + selectedType, + selectedStatus, + selectedReliability, + showCleaning, + ]); + + const drawerWidth = 360; + return ( - - {/* 头部控制栏 */} - + {/* 收起时的触发按钮 */} + {!isExpanded && ( + setIsExpanded(true)} + > + + + + SCADA设备 + + + + + )} + + {/* 主面板 */} + - - - - - SCADA 设备列表 - - - - - - setIsExpanded(!isExpanded)} - sx={{ color: "white" }} - > - {isExpanded ? : } - - - - - - - - {/* 搜索和筛选栏 */} - - - {/* 搜索框 */} - - { - setInputValue(e.target.value); - debouncedSetSearchQuery(e.target.value); - }} - inputProps={{ "aria-label": "search devices" }} - /> - - - - {searchQuery && ( - <> - + + {/* 头部控制栏 */} + + + + setIsExpanded(false)} + sx={{ color: "white" }} > - + - - )} - - - {/* 筛选器 */} - - - 设备类型 - - - - - 状态 - - - - - 可靠度 - - - - - - - - - - - {/* 筛选结果统计 */} - - - 共找到 {filteredDevices.length} 个设备 - {devices.length !== filteredDevices.length && - ` (共 ${effectiveDevices.length} 个设备)`} - - - {/* 地图选择按钮 */} - - - - - - - - {/* 清除选择按钮 */} - {activeSelection.length > 0 && ( - + + + + SCADA 设备列表 + } - sx={{ fontWeight: "medium" }} + sx={{ + backgroundColor: "rgba(255, 255, 255, 0.2)", + color: "white", + fontWeight: "bold", + }} /> + + + + + {/* 搜索和筛选栏 */} + + + {/* 搜索框 */} + + { + setInputValue(e.target.value); + debouncedSetSearchQuery(e.target.value); + }} + inputProps={{ "aria-label": "search devices" }} + /> + + + + {searchQuery && ( + <> + + + + + + )} - )} - - - + {/* 筛选器 */} + + + 设备类型 + + - {/* 设备列表 */} - - {loading ? ( - - - - ) : filteredDevices.length === 0 ? ( - - - - {searchQuery || - selectedType !== "all" || - selectedStatus !== "all" - ? "未找到匹配的设备" - : "暂无 SCADA 设备"} - - - ) : ( - - {({ - index, - style, - }: { - index: number; - style: React.CSSProperties; - }) => { - const device = filteredDevices[index]; - return ( -
- - handleDeviceClick(device, event)} - sx={{ - "&.Mui-selected": { - backgroundColor: "primary.50", - borderColor: "primary.main", - }, - "&:hover": { - backgroundColor: "grey.50", - }, - }} - > - - - {getStatusIcon(device.status.value)} - - + + 状态 + + - - - {device.name} - - - - - } - secondary={ - - - ID: {device.id} - - - 传输频率:{" "} - {getTransmissionFrequency( - device.transmission_frequency - )}{" "} - 分钟 - - - } - slotProps={{ - secondary: { - component: "div", // 使其支持多行 + + 可靠度 + + + + + + + + + + + {/* 筛选结果统计 */} + + + 共找到 {filteredDevices.length} 个设备 + {devices.length !== filteredDevices.length && + ` (共 ${effectiveDevices.length} 个设备)`} + + + {/* 地图选择按钮 */} + + + + + + + {/* 清洗全部数据按钮 */} + {showCleaning && ( + + setCleanDialogOpen(true)} + sx={{ + border: 1, + borderColor: "secondary.main", + "&:hover": { + backgroundColor: "secondary.50", + }, + }} + > + + + + )} + + + {/* 清除选择按钮 */} + {activeSelection.length > 0 && ( + + } + sx={{ fontWeight: "medium" }} + /> + + )} + + + + + + {/* 设备列表 */} + + {loading ? ( + + + + ) : filteredDevices.length === 0 ? ( + + + + {searchQuery || + selectedType !== "all" || + selectedStatus !== "all" + ? "未找到匹配的设备" + : "暂无 SCADA 设备"} + + + ) : ( + + {({ + index, + style, + }: { + index: number; + style: React.CSSProperties; + }) => { + const device = filteredDevices[index]; + return ( +
+ + handleDeviceClick(device, event)} + sx={{ + "&.Mui-selected": { + backgroundColor: "primary.50", + borderColor: "primary.main", + }, + "&:hover": { + backgroundColor: "grey.50", }, }} - /> + > + + + {getStatusIcon(device.status.value)} + + - - { - event.stopPropagation(); - handleZoomToDevice(device); - }} - sx={{ - ml: 1, - color: "primary.main", - "&:hover": { - backgroundColor: "primary.50", + + + {device.name} + + + + + } + secondary={ + + + ID: {device.id} + + + 传输频率:{" "} + {getTransmissionFrequency( + device.transmission_frequency + )}{" "} + 分钟 + + + } + slotProps={{ + secondary: { + component: "div", // 使其支持多行 }, }} - > - - - - - - {index < filteredDevices.length - 1 && ( - - )} -
- ); - }} -
- )} + /> + + + { + event.stopPropagation(); + handleZoomToDevice(device); + }} + sx={{ + ml: 1, + color: "primary.main", + "&:hover": { + backgroundColor: "primary.50", + }, + }} + > + + + +
+
+ {index < filteredDevices.length - 1 && ( + + )} +
+ ); + }} +
+ )} +
-
-
+ + {/* 清洗数据时间段选择对话框 */} + + + + + 清洗全部数据 + + + + + + 请选择要清洗数据的时间段,最长不超过两周(14天)。 + + + + + + + + + {timeRangeError && ( + {timeRangeError} + )} + + + + 时间范围: + {cleanStartTime.format("YYYY-MM-DD HH:mm")} 至{" "} + {cleanEndTime.format("YYYY-MM-DD HH:mm")} + + + 跨度: + {cleanEndTime.diff(cleanStartTime, "day")} 天{" "} + {cleanEndTime.diff(cleanStartTime, "hour") % 24} 小时 + + + + + + + + + + + ); };