完成管网在线模拟页面组件基本样式和布局

This commit is contained in:
JIANG
2025-09-30 17:55:15 +08:00
parent fc84b255ea
commit 5c888b60f0
13 changed files with 2028 additions and 54 deletions

View File

@@ -0,0 +1,430 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
import {
Box,
Paper,
TextField,
Typography,
List,
ListItem,
ListItemButton,
ListItemText,
ListItemIcon,
Chip,
IconButton,
Collapse,
InputAdornment,
FormControl,
InputLabel,
Select,
MenuItem,
Tooltip,
Stack,
Divider,
InputBase,
} from "@mui/material";
import {
Search,
MyLocation,
ExpandMore,
ExpandLess,
FilterList,
Clear,
Visibility,
VisibilityOff,
DeviceHub,
} from "@mui/icons-material";
interface SCADADevice {
id: string;
name: string;
type: string;
coordinates: [number, number];
status: "online" | "offline" | "warning" | "error";
properties?: Record<string, any>;
}
interface SCADADeviceListProps {
devices?: SCADADevice[];
onDeviceClick?: (device: SCADADevice) => void;
onZoomToDevice?: (coordinates: [number, number]) => void;
multiSelect?: boolean;
selectedDeviceIds?: string[];
onSelectionChange?: (ids: string[]) => void;
}
const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
devices = [],
onDeviceClick,
onZoomToDevice,
multiSelect = true,
selectedDeviceIds,
onSelectionChange,
}) => {
const [searchQuery, setSearchQuery] = useState<string>("");
const [selectedType, setSelectedType] = useState<string>("all");
const [selectedStatus, setSelectedStatus] = useState<string>("all");
const [isExpanded, setIsExpanded] = useState<boolean>(true);
const [internalSelection, setInternalSelection] = useState<string[]>([]);
const activeSelection = selectedDeviceIds ?? internalSelection;
useEffect(() => {
if (selectedDeviceIds) {
setInternalSelection(selectedDeviceIds);
}
}, [selectedDeviceIds]);
// 获取设备类型列表
const deviceTypes = useMemo(() => {
const types = Array.from(new Set(devices.map((device) => device.type)));
return types.sort();
}, [devices]);
// 获取设备状态列表
const deviceStatuses = useMemo(() => {
const statuses = Array.from(
new Set(devices.map((device) => device.status))
);
return statuses.sort();
}, [devices]);
// 过滤设备列表
const filteredDevices = useMemo(() => {
return devices.filter((device) => {
const matchesSearch =
searchQuery === "" ||
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.type.toLowerCase().includes(searchQuery.toLowerCase());
const matchesType =
selectedType === "all" || device.type === selectedType;
const matchesStatus =
selectedStatus === "all" || device.status === selectedStatus;
return matchesSearch && matchesType && matchesStatus;
});
}, [devices, searchQuery, selectedType, selectedStatus]);
// 状态颜色映射
const getStatusColor = (status: string) => {
switch (status) {
case "online":
return "success";
case "offline":
return "default";
case "warning":
return "warning";
case "error":
return "error";
default:
return "default";
}
};
// 状态图标映射
const getStatusIcon = (status: string) => {
switch (status) {
case "online":
return "●";
case "offline":
return "○";
case "warning":
return "▲";
case "error":
return "✕";
default:
return "●";
}
};
// 处理设备点击
const handleDeviceClick = (device: SCADADevice, event?: React.MouseEvent) => {
onDeviceClick?.(device);
setInternalSelection((prev) => {
const exists = prev.includes(device.id);
const nextSelection = multiSelect
? exists
? prev.filter((id) => id !== device.id)
: [...prev, device.id]
: exists
? []
: [device.id];
onSelectionChange?.(nextSelection);
return nextSelection;
});
};
// 处理缩放到设备
const handleZoomToDevice = (device: SCADADevice, event: React.MouseEvent) => {
event.stopPropagation();
onZoomToDevice?.(device.coordinates);
};
// 清除搜索
const handleClearSearch = () => {
setSearchQuery("");
};
// 重置所有筛选条件
const handleResetFilters = () => {
setSearchQuery("");
setSelectedType("all");
setSelectedStatus("all");
};
return (
<Paper className="absolute left-4 top-20 w-90 max-h-[calc(100vh-100px)] bg-white/95 backdrop-blur-[10px] rounded-xl shadow-lg overflow-hidden flex flex-col opacity-95 transition-opacity duration-200 ease-in-out hover:opacity-100">
{/* 头部控制栏 */}
<Box
sx={{
p: 2,
borderBottom: 1,
borderColor: "divider",
backgroundColor: "primary.main",
color: "white",
}}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
>
<Stack direction="row" alignItems="center" spacing={1}>
<DeviceHub fontSize="small" />
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
SCADA
</Typography>
<Chip
label={filteredDevices.length}
size="small"
sx={{
backgroundColor: "rgba(255, 255, 255, 0.2)",
color: "white",
fontWeight: "bold",
}}
/>
</Stack>
<Stack direction="row" spacing={1}>
<Tooltip title="展开/收起">
<IconButton
size="small"
onClick={() => setIsExpanded(!isExpanded)}
sx={{ color: "white" }}
>
{isExpanded ? <ExpandLess /> : <ExpandMore />}
</IconButton>
</Tooltip>
</Stack>
</Stack>
</Box>
<Collapse in={isExpanded}>
{/* 搜索和筛选栏 */}
<Box sx={{ p: 2, backgroundColor: "grey.50" }}>
<Stack spacing={2}>
{/* 搜索框 */}
<Box className="h-10 flex items-center border border-gray-300 rounded-lg p-0.5">
<InputBase
sx={{ ml: 1, flex: 1 }}
placeholder="搜索设备名称、ID 或类型..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
inputProps={{ "aria-label": "search devices" }}
/>
<IconButton type="button" sx={{ p: "6px" }} aria-label="search">
<Search fontSize="small" />
</IconButton>
{searchQuery && (
<>
<Divider sx={{ height: 28, m: 0.5 }} orientation="vertical" />
<IconButton
color="primary"
sx={{ p: "6px" }}
onClick={handleClearSearch}
aria-label="clear"
>
<Clear fontSize="small" />
</IconButton>
</>
)}
</Box>
{/* 筛选器 */}
<Stack direction="row" spacing={2}>
<FormControl size="small" sx={{ minWidth: 120 }}>
<InputLabel></InputLabel>
<Select
value={selectedType}
label="设备类型"
onChange={(e) => setSelectedType(e.target.value)}
>
<MenuItem value="all"></MenuItem>
{deviceTypes.map((type) => (
<MenuItem key={type} value={type}>
{type}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel></InputLabel>
<Select
value={selectedStatus}
label="状态"
onChange={(e) => setSelectedStatus(e.target.value)}
>
<MenuItem value="all"></MenuItem>
{deviceStatuses.map((status) => (
<MenuItem key={status} value={status}>
{status}
</MenuItem>
))}
</Select>
</FormControl>
<Tooltip title="重置筛选条件">
<IconButton onClick={handleResetFilters}>
<FilterList />
</IconButton>
</Tooltip>
</Stack>
{/* 筛选结果统计 */}
<Typography variant="caption" color="text.secondary">
{filteredDevices.length}
{devices.length !== filteredDevices.length &&
` (共 ${devices.length} 个设备)`}
</Typography>
</Stack>
</Box>
<Divider />
{/* 设备列表 */}
<Box sx={{ flex: 1, overflow: "auto", maxHeight: 400 }}>
{filteredDevices.length === 0 ? (
<Box
sx={{
p: 4,
textAlign: "center",
color: "text.secondary",
}}
>
<DeviceHub sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} />
<Typography variant="body2">
{searchQuery ||
selectedType !== "all" ||
selectedStatus !== "all"
? "未找到匹配的设备"
: "暂无 SCADA 设备"}
</Typography>
</Box>
) : (
<List dense sx={{ p: 0 }}>
{filteredDevices.map((device, index) => (
<React.Fragment key={device.id}>
<ListItem disablePadding>
<ListItemButton
selected={activeSelection.includes(device.id)}
onClick={(event) => handleDeviceClick(device, event)}
sx={{
"&.Mui-selected": {
backgroundColor: "primary.50",
borderLeft: 3,
borderColor: "primary.main",
},
"&:hover": {
backgroundColor: "grey.50",
},
}}
>
<ListItemIcon sx={{ minWidth: 36 }}>
<Typography
variant="caption"
sx={{
color: `${getStatusColor(device.status)}.main`,
fontWeight: "bold",
fontSize: 16,
}}
>
{getStatusIcon(device.status)}
</Typography>
</ListItemIcon>
<ListItemText
primary={
<Stack
direction="row"
alignItems="center"
spacing={1}
>
<Typography
variant="body2"
sx={{ fontWeight: "medium" }}
>
{device.name}
</Typography>
<Chip
label={device.type}
size="small"
variant="outlined"
sx={{ fontSize: "0.7rem", height: 20 }}
/>
</Stack>
}
secondary={
<Stack spacing={0.5}>
<Typography
variant="caption"
color="text.secondary"
>
ID: {device.id}
</Typography>
<Typography
variant="caption"
color="text.secondary"
>
: {device.coordinates[0].toFixed(6)},{" "}
{device.coordinates[1].toFixed(6)}
</Typography>
</Stack>
}
/>
<Tooltip title="缩放到设备位置">
<IconButton
size="small"
onClick={(e) => handleZoomToDevice(device, e)}
sx={{
ml: 1,
color: "primary.main",
"&:hover": {
backgroundColor: "primary.50",
},
}}
>
<MyLocation fontSize="small" />
</IconButton>
</Tooltip>
</ListItemButton>
</ListItem>
{index < filteredDevices.length - 1 && (
<Divider variant="inset" />
)}
</React.Fragment>
))}
</List>
)}
</Box>
</Collapse>
</Paper>
);
};
export default SCADADeviceList;