更新 SCADA 设备面板样式,新增清洗按钮
This commit is contained in:
@@ -21,10 +21,6 @@ export default function Home() {
|
|||||||
setPanelVisible(true);
|
setPanelVisible(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClosePanel = useCallback(() => {
|
|
||||||
setPanelVisible(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full overflow-hidden">
|
<div className="relative w-full h-full overflow-hidden">
|
||||||
<MapComponent>
|
<MapComponent>
|
||||||
@@ -35,11 +31,7 @@ export default function Home() {
|
|||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
selectedDeviceIds={selectedDeviceIds}
|
selectedDeviceIds={selectedDeviceIds}
|
||||||
/>
|
/>
|
||||||
<SCADADataPanel
|
<SCADADataPanel deviceIds={selectedDeviceIds} visible={panelVisible} />
|
||||||
deviceIds={selectedDeviceIds}
|
|
||||||
visible={panelVisible}
|
|
||||||
onClose={handleClosePanel}
|
|
||||||
/>
|
|
||||||
</MapComponent>
|
</MapComponent>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ export default function Home() {
|
|||||||
setPanelVisible(true);
|
setPanelVisible(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClosePanel = useCallback(() => {
|
|
||||||
setPanelVisible(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full overflow-hidden">
|
<div className="relative w-full h-full overflow-hidden">
|
||||||
<MapComponent>
|
<MapComponent>
|
||||||
@@ -32,11 +28,12 @@ export default function Home() {
|
|||||||
onDeviceClick={handleDeviceClick}
|
onDeviceClick={handleDeviceClick}
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
selectedDeviceIds={selectedDeviceIds}
|
selectedDeviceIds={selectedDeviceIds}
|
||||||
|
showCleaning={true}
|
||||||
/>
|
/>
|
||||||
<SCADADataPanel
|
<SCADADataPanel
|
||||||
deviceIds={selectedDeviceIds}
|
deviceIds={selectedDeviceIds}
|
||||||
visible={panelVisible}
|
visible={panelVisible}
|
||||||
onClose={handleClosePanel}
|
showCleaning={true}
|
||||||
/>
|
/>
|
||||||
</MapComponent>
|
</MapComponent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ import {
|
|||||||
CircularProgress,
|
CircularProgress,
|
||||||
Divider,
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
Paper,
|
|
||||||
Stack,
|
Stack,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
Collapse,
|
Drawer,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
Close,
|
Close,
|
||||||
Refresh,
|
Refresh,
|
||||||
ShowChart,
|
ShowChart,
|
||||||
TableChart,
|
TableChart,
|
||||||
ExpandLess,
|
CleaningServices,
|
||||||
ExpandMore,
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||||
import { LineChart } from "@mui/x-charts";
|
import { LineChart } from "@mui/x-charts";
|
||||||
@@ -55,12 +55,14 @@ export interface SCADADataPanelProps {
|
|||||||
) => Promise<TimeSeriesPoint[]>;
|
) => Promise<TimeSeriesPoint[]>;
|
||||||
/** 可选:控制浮窗显示 */
|
/** 可选:控制浮窗显示 */
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
/** 可选:关闭浮窗的回调 */
|
|
||||||
onClose?: () => void;
|
|
||||||
/** 默认展示的选项卡 */
|
/** 默认展示的选项卡 */
|
||||||
defaultTab?: "chart" | "table";
|
defaultTab?: "chart" | "table";
|
||||||
/** Y 轴数值的小数位数 */
|
/** Y 轴数值的小数位数 */
|
||||||
fractionDigits?: number;
|
fractionDigits?: number;
|
||||||
|
/** 是否显示清洗功能 */
|
||||||
|
showCleaning?: boolean;
|
||||||
|
/** 清洗数据的回调 */
|
||||||
|
onCleanData?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PanelTab = "chart" | "table";
|
type PanelTab = "chart" | "table";
|
||||||
@@ -242,9 +244,10 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
deviceIds,
|
deviceIds,
|
||||||
fetchTimeSeriesData = defaultFetcher,
|
fetchTimeSeriesData = defaultFetcher,
|
||||||
visible = true,
|
visible = true,
|
||||||
onClose,
|
|
||||||
defaultTab = "chart",
|
defaultTab = "chart",
|
||||||
fractionDigits = 2,
|
fractionDigits = 2,
|
||||||
|
showCleaning = false,
|
||||||
|
onCleanData,
|
||||||
}) => {
|
}) => {
|
||||||
const [from, setFrom] = useState<Dayjs>(() => dayjs().subtract(1, "day"));
|
const [from, setFrom] = useState<Dayjs>(() => dayjs().subtract(1, "day"));
|
||||||
const [to, setTo] = useState<Dayjs>(() => dayjs());
|
const [to, setTo] = useState<Dayjs>(() => dayjs());
|
||||||
@@ -560,17 +563,60 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const drawerWidth = 920;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<>
|
||||||
className={clsx(
|
{/* 收起时的触发按钮 */}
|
||||||
"absolute right-4 top-20 bg-white rounded-xl shadow-lg overflow-hidden flex flex-col transition-all duration-300",
|
{!isExpanded && hasDevices && (
|
||||||
visible ? "opacity-95 hover:opacity-100" : "opacity-0 -z-10"
|
<Box
|
||||||
|
className="absolute top-20 right-4 bg-white shadow-2xl rounded-lg cursor-pointer hover:shadow-xl transition-all duration-300 opacity-95 hover:opacity-100"
|
||||||
|
onClick={() => setIsExpanded(true)}
|
||||||
|
>
|
||||||
|
<Box className="flex flex-col items-center py-3 px-3 gap-1">
|
||||||
|
<ShowChart className="text-[#257DD4] w-5 h-5" />
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
className="text-gray-700 font-semibold my-1 text-xs"
|
||||||
|
style={{ writingMode: "vertical-rl" }}
|
||||||
|
>
|
||||||
|
历史数据
|
||||||
|
</Typography>
|
||||||
|
<ChevronLeft className="text-gray-600 w-4 h-4" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 主面板 */}
|
||||||
|
<Drawer
|
||||||
|
anchor="right"
|
||||||
|
open={isExpanded && visible}
|
||||||
|
variant="persistent"
|
||||||
|
hideBackdrop
|
||||||
sx={{
|
sx={{
|
||||||
|
width: isExpanded ? drawerWidth : 0,
|
||||||
|
flexShrink: 0,
|
||||||
|
"& .MuiDrawer-paper": {
|
||||||
width: "min(920px, calc(100vw - 2rem))",
|
width: "min(920px, calc(100vw - 2rem))",
|
||||||
maxHeight: "calc(100vh - 100px)",
|
boxSizing: "border-box",
|
||||||
|
position: "absolute",
|
||||||
|
top: 80,
|
||||||
|
right: 16,
|
||||||
|
height: "760px",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow:
|
||||||
|
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
opacity: 0.95,
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
border: "none",
|
||||||
|
"&:hover": {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Box className="flex flex-col h-full bg-white rounded-xl overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -602,29 +648,19 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
<Tooltip title="展开/收起">
|
<Tooltip title="收起">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(false)}
|
||||||
sx={{ color: "primary.contrastText" }}
|
sx={{ color: "primary.contrastText" }}
|
||||||
>
|
>
|
||||||
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
<ChevronRight fontSize="small" />
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="关闭">
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={onClose}
|
|
||||||
sx={{ color: "primary.contrastText" }}
|
|
||||||
>
|
|
||||||
<Close fontSize="small" />
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Collapse in={isExpanded}>
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<Box sx={{ p: 2, backgroundColor: "grey.50" }}>
|
<Box sx={{ p: 2, backgroundColor: "grey.50" }}>
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
@@ -642,7 +678,9 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
maxDateTime={to}
|
maxDateTime={to}
|
||||||
slotProps={{ textField: { fullWidth: true, size: "small" } }}
|
slotProps={{
|
||||||
|
textField: { fullWidth: true, size: "small" },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="结束时间"
|
label="结束时间"
|
||||||
@@ -656,7 +694,9 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
minDateTime={from}
|
minDateTime={from}
|
||||||
slotProps={{ textField: { fullWidth: true, size: "small" } }}
|
slotProps={{
|
||||||
|
textField: { fullWidth: true, size: "small" },
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack
|
<Stack
|
||||||
@@ -683,6 +723,23 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
label="表格"
|
label="表格"
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
{showCleaning && (
|
||||||
|
<Tooltip title="清洗数据">
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<CleaningServices fontSize="small" />}
|
||||||
|
disabled={!hasDevices || loadingState === "loading"}
|
||||||
|
onClick={onCleanData}
|
||||||
|
>
|
||||||
|
清洗
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Tooltip title="刷新数据">
|
<Tooltip title="刷新数据">
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
@@ -699,6 +756,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
|
|
||||||
{!hasDevices && (
|
{!hasDevices && (
|
||||||
@@ -743,8 +801,9 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
|
|
||||||
{activeTab === "chart" ? renderChart() : renderTable()}
|
{activeTab === "chart" ? renderChart() : renderTable()}
|
||||||
</Box>
|
</Box>
|
||||||
</Collapse>
|
</Box>
|
||||||
</Paper>
|
</Drawer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Paper,
|
|
||||||
Typography,
|
Typography,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
@@ -18,7 +17,6 @@ import {
|
|||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
Chip,
|
Chip,
|
||||||
IconButton,
|
IconButton,
|
||||||
Collapse,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Select,
|
Select,
|
||||||
@@ -28,16 +26,25 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
InputBase,
|
InputBase,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
Alert,
|
||||||
|
Drawer,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
MyLocation,
|
MyLocation,
|
||||||
ExpandMore,
|
|
||||||
ExpandLess,
|
|
||||||
FilterList,
|
FilterList,
|
||||||
Clear,
|
Clear,
|
||||||
DeviceHub,
|
DeviceHub,
|
||||||
TouchApp,
|
TouchApp,
|
||||||
|
CleaningServices,
|
||||||
|
Sensors,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { FixedSizeList } from "react-window";
|
import { FixedSizeList } from "react-window";
|
||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
@@ -53,6 +60,9 @@ import { Point } from "ol/geom";
|
|||||||
import { getVectorContext } from "ol/render";
|
import { getVectorContext } from "ol/render";
|
||||||
import { unByKey } from "ol/Observable";
|
import { unByKey } from "ol/Observable";
|
||||||
import config from "@/config/config";
|
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: {
|
const STATUS_OPTIONS: {
|
||||||
value: "online" | "offline" | "warning" | "error";
|
value: "online" | "offline" | "warning" | "error";
|
||||||
@@ -84,6 +94,8 @@ interface SCADADeviceListProps {
|
|||||||
multiSelect?: boolean;
|
multiSelect?: boolean;
|
||||||
selectedDeviceIds?: string[];
|
selectedDeviceIds?: string[];
|
||||||
onSelectionChange?: (ids: string[]) => void;
|
onSelectionChange?: (ids: string[]) => void;
|
||||||
|
showCleaning?: boolean;
|
||||||
|
onCleanAllData?: (from: Date, to: Date) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
||||||
@@ -92,6 +104,8 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
multiSelect = true,
|
multiSelect = true,
|
||||||
selectedDeviceIds,
|
selectedDeviceIds,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
|
showCleaning = false,
|
||||||
|
onCleanAllData,
|
||||||
}) => {
|
}) => {
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
const [selectedType, setSelectedType] = useState<string>("all");
|
const [selectedType, setSelectedType] = useState<string>("all");
|
||||||
@@ -113,6 +127,16 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
const blinkListenerKeyRef = useRef<any>(null);
|
const blinkListenerKeyRef = useRef<any>(null);
|
||||||
|
|
||||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const filterBoxRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [listHeight, setListHeight] = useState<number>(500);
|
||||||
|
|
||||||
|
// 清洗对话框状态
|
||||||
|
const [cleanDialogOpen, setCleanDialogOpen] = useState<boolean>(false);
|
||||||
|
const [cleanStartTime, setCleanStartTime] = useState<Dayjs>(() =>
|
||||||
|
dayjs().subtract(1, "week")
|
||||||
|
);
|
||||||
|
const [cleanEndTime, setCleanEndTime] = useState<Dayjs>(() => dayjs());
|
||||||
|
const [timeRangeError, setTimeRangeError] = useState<string>("");
|
||||||
|
|
||||||
// 防抖更新搜索查询
|
// 防抖更新搜索查询
|
||||||
const debouncedSetSearchQuery = useCallback((value: string) => {
|
const debouncedSetSearchQuery = useCallback((value: string) => {
|
||||||
@@ -497,6 +521,67 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
[map, effectiveDevices, multiSelect, open]
|
[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 设备
|
// 开始选择 SCADA 设备
|
||||||
const handleStartSelection = useCallback(() => {
|
const handleStartSelection = useCallback(() => {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
@@ -586,8 +671,93 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 动态计算列表高度
|
||||||
|
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 (
|
return (
|
||||||
<Paper className="absolute left-4 top-20 w-90 max-h-[calc(100vh-100px)] bg-white rounded-xl shadow-lg overflow-hidden flex flex-col opacity-95 transition-opacity duration-200 ease-in-out hover:opacity-100">
|
<>
|
||||||
|
{/* 收起时的触发按钮 */}
|
||||||
|
{!isExpanded && (
|
||||||
|
<Box
|
||||||
|
className="absolute top-20 left-4 bg-white shadow-2xl rounded-lg cursor-pointer hover:shadow-xl transition-all duration-300 opacity-95 hover:opacity-100"
|
||||||
|
onClick={() => setIsExpanded(true)}
|
||||||
|
>
|
||||||
|
<Box className="flex flex-col items-center py-3 px-3 gap-1">
|
||||||
|
<Sensors className="text-[#1976d2] w-5 h-5" />
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
className="text-gray-700 font-semibold my-1 text-xs"
|
||||||
|
style={{ writingMode: "vertical-rl" }}
|
||||||
|
>
|
||||||
|
SCADA设备
|
||||||
|
</Typography>
|
||||||
|
<ChevronRight className="text-gray-600 w-4 h-4" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 主面板 */}
|
||||||
|
<Drawer
|
||||||
|
anchor="left"
|
||||||
|
open={isExpanded}
|
||||||
|
variant="persistent"
|
||||||
|
hideBackdrop
|
||||||
|
sx={{
|
||||||
|
width: isExpanded ? drawerWidth : 0,
|
||||||
|
flexShrink: 0,
|
||||||
|
"& .MuiDrawer-paper": {
|
||||||
|
width: drawerWidth,
|
||||||
|
height: "760px",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
position: "absolute",
|
||||||
|
top: 80,
|
||||||
|
left: 16,
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow:
|
||||||
|
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
opacity: 0.95,
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
border: "none",
|
||||||
|
"&:hover": {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="flex flex-col h-full bg-white rounded-xl overflow-hidden">
|
||||||
{/* 头部控制栏 */}
|
{/* 头部控制栏 */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -604,7 +774,16 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
>
|
>
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<DeviceHub fontSize="small" />
|
<Tooltip title="收起">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => setIsExpanded(false)}
|
||||||
|
sx={{ color: "white" }}
|
||||||
|
>
|
||||||
|
<ChevronLeft fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Sensors fontSize="small" />
|
||||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
||||||
SCADA 设备列表
|
SCADA 设备列表
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -618,23 +797,11 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" spacing={1}>
|
|
||||||
<Tooltip title="展开/收起">
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
|
||||||
sx={{ color: "white" }}
|
|
||||||
>
|
|
||||||
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Collapse in={isExpanded}>
|
|
||||||
{/* 搜索和筛选栏 */}
|
{/* 搜索和筛选栏 */}
|
||||||
<Box sx={{ p: 2, backgroundColor: "grey.50" }}>
|
<Box ref={filterBoxRef} sx={{ p: 2, backgroundColor: "grey.50" }}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
{/* 搜索框 */}
|
{/* 搜索框 */}
|
||||||
<Box className="h-10 flex items-center border border-gray-300 rounded-md p-0.5">
|
<Box className="h-10 flex items-center border border-gray-300 rounded-md p-0.5">
|
||||||
@@ -653,7 +820,10 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<>
|
<>
|
||||||
<Divider sx={{ height: 28, m: 0.5 }} orientation="vertical" />
|
<Divider
|
||||||
|
sx={{ height: 28, m: 0.5 }}
|
||||||
|
orientation="vertical"
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ p: "6px" }}
|
sx={{ p: "6px" }}
|
||||||
@@ -730,7 +900,9 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* 地图选择按钮 */}
|
{/* 地图选择按钮 */}
|
||||||
<Tooltip title={isSelecting ? "结束地图选择" : "从地图选择设备"}>
|
<Tooltip
|
||||||
|
title={isSelecting ? "结束地图选择" : "从地图选择设备"}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color={isSelecting ? "primary" : "default"}
|
color={isSelecting ? "primary" : "default"}
|
||||||
@@ -740,15 +912,39 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
sx={{
|
sx={{
|
||||||
border: 1,
|
border: 1,
|
||||||
borderColor: isSelecting ? "primary.main" : "divider",
|
borderColor: isSelecting ? "primary.main" : "divider",
|
||||||
backgroundColor: isSelecting ? "primary.50" : "transparent",
|
backgroundColor: isSelecting
|
||||||
|
? "primary.50"
|
||||||
|
: "transparent",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: isSelecting ? "primary.100" : "grey.100",
|
backgroundColor: isSelecting
|
||||||
|
? "primary.100"
|
||||||
|
: "grey.100",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TouchApp fontSize="small" />
|
<TouchApp fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* 清洗全部数据按钮 */}
|
||||||
|
{showCleaning && (
|
||||||
|
<Tooltip title="清洗全部数据">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => setCleanDialogOpen(true)}
|
||||||
|
sx={{
|
||||||
|
border: 1,
|
||||||
|
borderColor: "secondary.main",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "secondary.50",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CleaningServices fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{/* 清除选择按钮 */}
|
{/* 清除选择按钮 */}
|
||||||
@@ -770,7 +966,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{/* 设备列表 */}
|
{/* 设备列表 */}
|
||||||
<Box sx={{ flex: 1, overflow: "auto", maxHeight: 400 }}>
|
<Box sx={{ flex: 1, overflow: "hidden" }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -801,7 +997,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
height={400}
|
height={listHeight}
|
||||||
itemCount={filteredDevices.length}
|
itemCount={filteredDevices.length}
|
||||||
itemSize={92}
|
itemSize={92}
|
||||||
width="100%"
|
width="100%"
|
||||||
@@ -930,8 +1126,93 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
</FixedSizeList>
|
</FixedSizeList>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Collapse>
|
</Box>
|
||||||
</Paper>
|
|
||||||
|
{/* 清洗数据时间段选择对话框 */}
|
||||||
|
<Dialog
|
||||||
|
open={cleanDialogOpen}
|
||||||
|
onClose={handleCleanDialogClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<CleaningServices color="secondary" />
|
||||||
|
<Typography variant="h6">清洗全部数据</Typography>
|
||||||
|
</Stack>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Stack spacing={3} sx={{ mt: 2 }}>
|
||||||
|
<Alert severity="info">
|
||||||
|
请选择要清洗数据的时间段,最长不超过两周(14天)。
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
|
<DateTimePicker
|
||||||
|
label="开始时间"
|
||||||
|
value={cleanStartTime}
|
||||||
|
onChange={handleCleanStartTimeChange}
|
||||||
|
maxDateTime={cleanEndTime}
|
||||||
|
slotProps={{
|
||||||
|
textField: {
|
||||||
|
fullWidth: true,
|
||||||
|
error: !!timeRangeError,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateTimePicker
|
||||||
|
label="结束时间"
|
||||||
|
value={cleanEndTime}
|
||||||
|
onChange={handleCleanEndTimeChange}
|
||||||
|
minDateTime={cleanStartTime}
|
||||||
|
maxDateTime={dayjs()}
|
||||||
|
slotProps={{
|
||||||
|
textField: {
|
||||||
|
fullWidth: true,
|
||||||
|
error: !!timeRangeError,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LocalizationProvider>
|
||||||
|
|
||||||
|
{timeRangeError && (
|
||||||
|
<Alert severity="error">{timeRangeError}</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ p: 2, backgroundColor: "grey.50", borderRadius: 1 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
<strong>时间范围:</strong>
|
||||||
|
{cleanStartTime.format("YYYY-MM-DD HH:mm")} 至{" "}
|
||||||
|
{cleanEndTime.format("YYYY-MM-DD HH:mm")}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
>
|
||||||
|
<strong>跨度:</strong>
|
||||||
|
{cleanEndTime.diff(cleanStartTime, "day")} 天{" "}
|
||||||
|
{cleanEndTime.diff(cleanStartTime, "hour") % 24} 小时
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
|
<Button onClick={handleCleanDialogClose}>取消</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={handleConfirmClean}
|
||||||
|
disabled={!!timeRangeError}
|
||||||
|
startIcon={<CleaningServices />}
|
||||||
|
>
|
||||||
|
确认清洗
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user