更新 SCADA 设备面板样式,新增清洗按钮

This commit is contained in:
JIANG
2025-11-04 11:39:06 +08:00
parent 85d73bcd07
commit af44a7c503
4 changed files with 851 additions and 522 deletions

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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,191 +563,247 @@ 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>
)} )}
sx={{
width: "min(920px, calc(100vw - 2rem))", {/* 主面板 */}
maxHeight: "calc(100vh - 100px)", <Drawer
}} anchor="right"
> open={isExpanded && visible}
{/* Header */} variant="persistent"
<Box hideBackdrop
sx={{ sx={{
p: 2, width: isExpanded ? drawerWidth : 0,
borderBottom: 1, flexShrink: 0,
borderColor: "divider", "& .MuiDrawer-paper": {
backgroundColor: "primary.main", width: "min(920px, calc(100vw - 2rem))",
color: "primary.contrastText", 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,
},
},
}} }}
> >
<Stack <Box className="flex flex-col h-full bg-white rounded-xl overflow-hidden">
direction="row" {/* Header */}
alignItems="center" <Box
justifyContent="space-between" sx={{
> p: 2,
<Stack direction="row" spacing={1} alignItems="center"> borderBottom: 1,
<ShowChart fontSize="small" /> borderColor: "divider",
<Typography variant="h6" sx={{ fontWeight: "bold" }}> backgroundColor: "primary.main",
SCADA color: "primary.contrastText",
</Typography> }}
<Chip >
size="small" <Stack
label={`${deviceIds.length}`} direction="row"
sx={{ alignItems="center"
backgroundColor: "rgba(255,255,255,0.2)", justifyContent="space-between"
color: "primary.contrastText", >
fontWeight: "bold",
}}
/>
</Stack>
<Stack direction="row" spacing={1}>
<Tooltip title="展开/收起">
<IconButton
size="small"
onClick={() => setIsExpanded(!isExpanded)}
sx={{ color: "primary.contrastText" }}
>
{isExpanded ? <ExpandLess /> : <ExpandMore />}
</IconButton>
</Tooltip>
<Tooltip title="关闭">
<IconButton
size="small"
onClick={onClose}
sx={{ color: "primary.contrastText" }}
>
<Close fontSize="small" />
</IconButton>
</Tooltip>
</Stack>
</Stack>
</Box>
<Collapse in={isExpanded}>
{/* Controls */}
<Box sx={{ p: 2, backgroundColor: "grey.50" }}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Stack spacing={1.5}>
<Stack direction="row" spacing={1} alignItems="center"> <Stack direction="row" spacing={1} alignItems="center">
<DateTimePicker <ShowChart fontSize="small" />
label="开始时间" <Typography variant="h6" sx={{ fontWeight: "bold" }}>
value={from} SCADA
onChange={(value) => </Typography>
value && dayjs.isDayjs(value) && setFrom(value) <Chip
} size="small"
onAccept={(value) => { label={`${deviceIds.length}`}
if (value && dayjs.isDayjs(value) && hasDevices) { sx={{
handleFetch("date-change"); backgroundColor: "rgba(255,255,255,0.2)",
} color: "primary.contrastText",
fontWeight: "bold",
}} }}
maxDateTime={to}
slotProps={{ textField: { fullWidth: true, size: "small" } }}
/>
<DateTimePicker
label="结束时间"
value={to}
onChange={(value) =>
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" } }}
/> />
</Stack> </Stack>
<Stack <Stack direction="row" spacing={1}>
direction="row" <Tooltip title="收起">
spacing={1} <IconButton
alignItems="center" size="small"
justifyContent="space-between" onClick={() => setIsExpanded(false)}
> sx={{ color: "primary.contrastText" }}
<Tabs >
value={activeTab} <ChevronRight fontSize="small" />
onChange={(_, value: PanelTab) => setActiveTab(value)} </IconButton>
variant="fullWidth"
>
<Tab
value="chart"
icon={<ShowChart fontSize="small" />}
iconPosition="start"
label="曲线"
/>
<Tab
value="table"
icon={<TableChart fontSize="small" />}
iconPosition="start"
label="表格"
/>
</Tabs>
<Tooltip title="刷新数据">
<span>
<Button
variant="contained"
size="small"
color="primary"
startIcon={<Refresh fontSize="small" />}
disabled={!hasDevices || loadingState === "loading"}
onClick={() => handleFetch("manual")}
>
</Button>
</span>
</Tooltip> </Tooltip>
</Stack> </Stack>
</Stack> </Stack>
</LocalizationProvider> </Box>
{!hasDevices && ( {/* Controls */}
<Typography <Box sx={{ p: 2, backgroundColor: "grey.50" }}>
variant="caption" <LocalizationProvider dateAdapter={AdapterDayjs}>
color="warning.main" <Stack spacing={1.5}>
sx={{ mt: 1, display: "block" }} <Stack direction="row" spacing={1} alignItems="center">
> <DateTimePicker
label="开始时间"
</Typography> value={from}
)} onChange={(value) =>
{error && ( value && dayjs.isDayjs(value) && setFrom(value)
<Typography }
variant="caption" onAccept={(value) => {
color="error" if (value && dayjs.isDayjs(value) && hasDevices) {
sx={{ mt: 1, display: "block" }} handleFetch("date-change");
> }
{error} }}
</Typography> maxDateTime={to}
)} slotProps={{
textField: { fullWidth: true, size: "small" },
}}
/>
<DateTimePicker
label="结束时间"
value={to}
onChange={(value) =>
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" },
}}
/>
</Stack>
<Stack
direction="row"
spacing={1}
alignItems="center"
justifyContent="space-between"
>
<Tabs
value={activeTab}
onChange={(_, value: PanelTab) => setActiveTab(value)}
variant="fullWidth"
>
<Tab
value="chart"
icon={<ShowChart fontSize="small" />}
iconPosition="start"
label="曲线"
/>
<Tab
value="table"
icon={<TableChart fontSize="small" />}
iconPosition="start"
label="表格"
/>
</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="刷新数据">
<span>
<Button
variant="contained"
size="small"
color="primary"
startIcon={<Refresh fontSize="small" />}
disabled={!hasDevices || loadingState === "loading"}
onClick={() => handleFetch("manual")}
>
</Button>
</span>
</Tooltip>
</Stack>
</Stack>
</Stack>
</LocalizationProvider>
{!hasDevices && (
<Typography
variant="caption"
color="warning.main"
sx={{ mt: 1, display: "block" }}
>
</Typography>
)}
{error && (
<Typography
variant="caption"
color="error"
sx={{ mt: 1, display: "block" }}
>
{error}
</Typography>
)}
</Box>
<Divider />
{/* Content */}
<Box sx={{ flex: 1, position: "relative", p: 2, overflow: "auto" }}>
{loadingState === "loading" && (
<Box
sx={{
position: "absolute",
inset: 0,
backgroundColor: "rgba(255,255,255,0.6)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1,
}}
>
<CircularProgress size={48} />
</Box>
)}
{activeTab === "chart" ? renderChart() : renderTable()}
</Box>
</Box> </Box>
</Drawer>
<Divider /> </>
{/* Content */}
<Box sx={{ flex: 1, position: "relative", p: 2, overflow: "auto" }}>
{loadingState === "loading" && (
<Box
sx={{
position: "absolute",
inset: 0,
backgroundColor: "rgba(255,255,255,0.6)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1,
}}
>
<CircularProgress size={48} />
</Box>
)}
{activeTab === "chart" ? renderChart() : renderTable()}
</Box>
</Collapse>
</Paper>
); );
}; };

File diff suppressed because it is too large Load Diff