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="表格"
- />
-
-
-
- }
- disabled={!hasDevices || loadingState === "loading"}
- onClick={() => handleFetch("manual")}
- >
- 刷新
-
-
+
+
+ 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 && (
+
+
+ }
+ disabled={!hasDevices || loadingState === "loading"}
+ onClick={onCleanData}
+ >
+ 清洗
+
+
+
+ )}
+
+
+ }
+ disabled={!hasDevices || loadingState === "loading"}
+ onClick={() => handleFetch("manual")}
+ >
+ 刷新
+
+
+
+
+
+
+
+
+ {!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 && (
+
+ )}
+
+ );
+ }}
+
+ )}
+
-
-
+
+ {/* 清洗数据时间段选择对话框 */}
+
+
+ >
);
};