为 SCADA 设备列表实现虚拟列表,优化渲染性能

This commit is contained in:
JIANG
2025-10-29 17:24:35 +08:00
parent a5954624a0
commit 09d037fd5a
3 changed files with 144 additions and 105 deletions

36
package-lock.json generated
View File

@@ -37,7 +37,7 @@
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-window": "^2.2.2", "react-window": "^1.8.10",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
@@ -46,6 +46,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19.1.0", "@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0", "@types/react-dom": "^19.1.0",
"@types/react-window": "^1.8.8",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "^15.0.3", "eslint-config-next": "^15.0.3",
@@ -7992,6 +7993,16 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/react-window": {
"version": "1.8.8",
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz",
"integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "2.0.11", "version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
@@ -14590,6 +14601,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@@ -16462,13 +16479,20 @@
} }
}, },
"node_modules/react-window": { "node_modules/react-window": {
"version": "2.2.2", "version": "1.8.10",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.2.tgz", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz",
"integrity": "sha512-kvHKwFImKBWNbx2S87NZOhQhAVkBthjmnOfHlhQI45p3A+D+V53E+CqQMsyHrxNe3ke+YtWXuKDa1eoHAaIWJg==", "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==",
"license": "MIT", "license": "MIT",
"dependencies": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
},
"engines": {
"node": ">8.0.0"
},
"peerDependencies": { "peerDependencies": {
"react": "^18.0.0 || ^19.0.0", "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^18.0.0 || ^19.0.0" "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/readable-stream": { "node_modules/readable-stream": {

View File

@@ -42,7 +42,7 @@
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-window": "^2.2.2", "react-window": "^1.8.10",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
@@ -51,6 +51,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19.1.0", "@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0", "@types/react-dom": "^19.1.0",
"@types/react-window": "^1.8.8",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "^15.0.3", "eslint-config-next": "^15.0.3",

View File

@@ -12,7 +12,6 @@ import {
Box, Box,
Paper, Paper,
Typography, Typography,
List,
ListItem, ListItem,
ListItemButton, ListItemButton,
ListItemText, ListItemText,
@@ -39,6 +38,7 @@ import {
Clear, Clear,
DeviceHub, DeviceHub,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { FixedSizeList } from "react-window";
import { useMap } from "@app/OlMap/MapComponent"; import { useMap } from "@app/OlMap/MapComponent";
import { GeoJSON } from "ol/format"; import { GeoJSON } from "ol/format";
@@ -452,108 +452,122 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
<List dense sx={{ p: 0 }}> <FixedSizeList
{filteredDevices.map((device, index) => ( height={400}
<React.Fragment key={device.id}> itemCount={filteredDevices.length}
<ListItem disablePadding> itemSize={80}
<ListItemButton width="100%"
selected={activeSelection.includes(device.id)} >
onClick={(event) => handleDeviceClick(device, event)} {({
sx={{ index,
"&.Mui-selected": { style,
backgroundColor: "primary.50", }: {
borderLeft: 3, index: number;
borderColor: "primary.main", style: React.CSSProperties;
}, }) => {
"&:hover": { const device = filteredDevices[index];
backgroundColor: "grey.50", return (
}, <div style={style}>
}} <ListItem disablePadding>
> <ListItemButton
<ListItemIcon sx={{ minWidth: 36 }}> selected={activeSelection.includes(device.id)}
<Typography onClick={(event) => handleDeviceClick(device, event)}
variant="caption" sx={{
sx={{ "&.Mui-selected": {
color: `${getStatusColor(device.status)}.main`, backgroundColor: "primary.50",
fontWeight: "bold", borderLeft: 3,
fontSize: 16, borderColor: "primary.main",
}} },
> "&:hover": {
{getStatusIcon(device.status)} backgroundColor: "grey.50",
</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>
}
slotProps={{
secondary: {
component: "div", // 使其支持多行
}, },
}} }}
/> >
<ListItemIcon sx={{ minWidth: 36 }}>
<Typography
variant="caption"
sx={{
color: `${getStatusColor(device.status)}.main`,
fontWeight: "bold",
fontSize: 16,
}}
>
{getStatusIcon(device.status)}
</Typography>
</ListItemIcon>
<Tooltip title="缩放到设备位置"> <ListItemText
<IconButton primary={
size="small" <Stack
onClick={(event) => { direction="row"
event.stopPropagation(); alignItems="center"
handleZoomToDevice(device); spacing={1}
}} >
sx={{ <Typography
ml: 1, variant="body2"
color: "primary.main", sx={{ fontWeight: "medium" }}
"&:hover": { >
backgroundColor: "primary.50", {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>
}
slotProps={{
secondary: {
component: "div", // 使其支持多行
}, },
}} }}
> />
<MyLocation fontSize="small" />
</IconButton> <Tooltip title="缩放到设备位置">
</Tooltip> <IconButton
</ListItemButton> size="small"
</ListItem> onClick={(event) => {
{index < filteredDevices.length - 1 && ( event.stopPropagation();
<Divider variant="inset" /> handleZoomToDevice(device);
)} }}
</React.Fragment> sx={{
))} ml: 1,
</List> color: "primary.main",
"&:hover": {
backgroundColor: "primary.50",
},
}}
>
<MyLocation fontSize="small" />
</IconButton>
</Tooltip>
</ListItemButton>
</ListItem>
{index < filteredDevices.length - 1 && (
<Divider variant="inset" />
)}
</div>
);
}}
</FixedSizeList>
)} )}
</Box> </Box>
</Collapse> </Collapse>