组件居中,可拖拽
This commit is contained in:
@@ -1,6 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import Draggable from "react-draggable";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -294,6 +302,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
const [selectedSource, setSelectedSource] = useState<
|
const [selectedSource, setSelectedSource] = useState<
|
||||||
"raw" | "clean" | "sim" | "all"
|
"raw" | "clean" | "sim" | "all"
|
||||||
>(() => (deviceIds.length === 1 ? "all" : "clean"));
|
>(() => (deviceIds.length === 1 ? "all" : "clean"));
|
||||||
|
const draggableRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// 获取 SCADA 设备信息,生成 deviceLabels
|
// 获取 SCADA 设备信息,生成 deviceLabels
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -648,214 +657,216 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 主面板 */}
|
{/* 主面板 */}
|
||||||
<Box
|
<Draggable nodeRef={draggableRef}>
|
||||||
sx={{
|
|
||||||
position: "fixed",
|
|
||||||
left: "50%",
|
|
||||||
top: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
width: "min(920px, calc(100vw - 2rem))",
|
|
||||||
maxWidth: "100vw",
|
|
||||||
height: "860px",
|
|
||||||
maxHeight: "calc(100vh - 2rem)",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
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: "opacity 0.3s ease-in-out",
|
|
||||||
border: "none",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
zIndex: 1300,
|
|
||||||
backgroundColor: "white",
|
|
||||||
overflow: "hidden",
|
|
||||||
"&:hover": {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
className="flex flex-col h-full rounded-xl"
|
ref={draggableRef}
|
||||||
sx={{ height: "100%", width: "100%" }}
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "1rem",
|
||||||
|
top: "1rem",
|
||||||
|
width: "min(920px, calc(100vw - 2rem))",
|
||||||
|
maxWidth: "100vw",
|
||||||
|
height: "860px",
|
||||||
|
maxHeight: "calc(100vh - 2rem)",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
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: "opacity 0.3s ease-in-out",
|
||||||
|
border: "none",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
zIndex: 1300,
|
||||||
|
backgroundColor: "white",
|
||||||
|
overflow: "hidden",
|
||||||
|
"&:hover": {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
className="flex flex-col h-full rounded-xl"
|
||||||
p: 2,
|
sx={{ height: "100%", width: "100%" }}
|
||||||
borderBottom: 1,
|
|
||||||
borderColor: "divider",
|
|
||||||
backgroundColor: "primary.main",
|
|
||||||
color: "primary.contrastText",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Stack
|
{/* Header */}
|
||||||
direction="row"
|
<Box
|
||||||
alignItems="center"
|
sx={{
|
||||||
justifyContent="space-between"
|
p: 2,
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
backgroundColor: "primary.main",
|
||||||
|
color: "primary.contrastText",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack
|
||||||
<ShowChart fontSize="small" />
|
direction="row"
|
||||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
alignItems="center"
|
||||||
历史数据
|
justifyContent="space-between"
|
||||||
</Typography>
|
>
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
label={`${deviceIds.length}`}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "rgba(255,255,255,0.2)",
|
|
||||||
color: "primary.contrastText",
|
|
||||||
fontWeight: "bold",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Controls */}
|
|
||||||
<Box sx={{ p: 2, backgroundColor: "grey.50" }}>
|
|
||||||
<LocalizationProvider
|
|
||||||
dateAdapter={AdapterDayjs}
|
|
||||||
adapterLocale="zh-cn"
|
|
||||||
>
|
|
||||||
<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}
|
历史数据
|
||||||
onChange={(value) => {
|
</Typography>
|
||||||
if (value && dayjs.isDayjs(value) && value.isValid()) {
|
<Chip
|
||||||
setFrom(value);
|
size="small"
|
||||||
}
|
label={`${deviceIds.length}`}
|
||||||
}}
|
sx={{
|
||||||
onAccept={(value) => {
|
backgroundColor: "rgba(255,255,255,0.2)",
|
||||||
if (
|
color: "primary.contrastText",
|
||||||
value &&
|
fontWeight: "bold",
|
||||||
dayjs.isDayjs(value) &&
|
|
||||||
value.isValid() &&
|
|
||||||
hasDevices
|
|
||||||
) {
|
|
||||||
handleFetch("date-change");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
maxDateTime={to}
|
|
||||||
slotProps={{
|
|
||||||
textField: { fullWidth: true, size: "small" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DateTimePicker
|
|
||||||
label="结束时间"
|
|
||||||
value={to}
|
|
||||||
onChange={(value) => {
|
|
||||||
if (value && dayjs.isDayjs(value) && value.isValid()) {
|
|
||||||
setTo(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onAccept={(value) => {
|
|
||||||
if (
|
|
||||||
value &&
|
|
||||||
dayjs.isDayjs(value) &&
|
|
||||||
value.isValid() &&
|
|
||||||
hasDevices
|
|
||||||
) {
|
|
||||||
handleFetch("date-change");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
minDateTime={from}
|
|
||||||
slotProps={{
|
|
||||||
textField: { fullWidth: true, size: "small" },
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack
|
</Stack>
|
||||||
direction="row"
|
</Box>
|
||||||
spacing={1}
|
|
||||||
alignItems="center"
|
{/* Controls */}
|
||||||
justifyContent="space-between"
|
<Box sx={{ p: 2, backgroundColor: "grey.50" }}>
|
||||||
>
|
<LocalizationProvider
|
||||||
<Tabs
|
dateAdapter={AdapterDayjs}
|
||||||
value={activeTab}
|
adapterLocale="zh-cn"
|
||||||
onChange={(_, value: PanelTab) => setActiveTab(value)}
|
>
|
||||||
variant="fullWidth"
|
<Stack spacing={1.5}>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<DateTimePicker
|
||||||
|
label="开始时间"
|
||||||
|
value={from}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value && dayjs.isDayjs(value) && value.isValid()) {
|
||||||
|
setFrom(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onAccept={(value) => {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
dayjs.isDayjs(value) &&
|
||||||
|
value.isValid() &&
|
||||||
|
hasDevices
|
||||||
|
) {
|
||||||
|
handleFetch("date-change");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maxDateTime={to}
|
||||||
|
slotProps={{
|
||||||
|
textField: { fullWidth: true, size: "small" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DateTimePicker
|
||||||
|
label="结束时间"
|
||||||
|
value={to}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value && dayjs.isDayjs(value) && value.isValid()) {
|
||||||
|
setTo(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onAccept={(value) => {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
dayjs.isDayjs(value) &&
|
||||||
|
value.isValid() &&
|
||||||
|
hasDevices
|
||||||
|
) {
|
||||||
|
handleFetch("date-change");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
minDateTime={from}
|
||||||
|
slotProps={{
|
||||||
|
textField: { fullWidth: true, size: "small" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
>
|
>
|
||||||
<Tab
|
<Tabs
|
||||||
value="chart"
|
value={activeTab}
|
||||||
icon={<ShowChart fontSize="small" />}
|
onChange={(_, value: PanelTab) => setActiveTab(value)}
|
||||||
iconPosition="start"
|
variant="fullWidth"
|
||||||
label="曲线"
|
>
|
||||||
/>
|
<Tab
|
||||||
<Tab
|
value="chart"
|
||||||
value="table"
|
icon={<ShowChart fontSize="small" />}
|
||||||
icon={<TableChart fontSize="small" />}
|
iconPosition="start"
|
||||||
iconPosition="start"
|
label="曲线"
|
||||||
label="表格"
|
/>
|
||||||
/>
|
<Tab
|
||||||
</Tabs>
|
value="table"
|
||||||
<Stack direction="row" spacing={1}>
|
icon={<TableChart fontSize="small" />}
|
||||||
<Tooltip title="刷新数据">
|
iconPosition="start"
|
||||||
<span>
|
label="表格"
|
||||||
<Button
|
/>
|
||||||
variant="outlined"
|
</Tabs>
|
||||||
size="small"
|
<Stack direction="row" spacing={1}>
|
||||||
color="primary"
|
<Tooltip title="刷新数据">
|
||||||
startIcon={<Refresh fontSize="small" />}
|
<span>
|
||||||
disabled={!hasDevices || loadingState === "loading"}
|
<Button
|
||||||
onClick={() => handleFetch("manual")}
|
variant="outlined"
|
||||||
>
|
size="small"
|
||||||
刷新
|
color="primary"
|
||||||
</Button>
|
startIcon={<Refresh fontSize="small" />}
|
||||||
</span>
|
disabled={!hasDevices || loadingState === "loading"}
|
||||||
</Tooltip>
|
onClick={() => handleFetch("manual")}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</LocalizationProvider>
|
||||||
</LocalizationProvider>
|
|
||||||
|
|
||||||
{!hasDevices && (
|
{!hasDevices && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="warning.main"
|
color="warning.main"
|
||||||
sx={{ mt: 1, display: "block" }}
|
sx={{ mt: 1, display: "block" }}
|
||||||
>
|
>
|
||||||
未选择任何设备,无法获取数据。
|
未选择任何设备,无法获取数据。
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="error"
|
color="error"
|
||||||
sx={{ mt: 1, display: "block" }}
|
sx={{ mt: 1, display: "block" }}
|
||||||
>
|
>
|
||||||
获取数据失败:{error}
|
获取数据失败:{error}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<Box sx={{ flex: 1, position: "relative", p: 2, overflow: "auto" }}>
|
<Box sx={{ flex: 1, position: "relative", p: 2, overflow: "auto" }}>
|
||||||
{loadingState === "loading" && (
|
{loadingState === "loading" && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
inset: 0,
|
inset: 0,
|
||||||
backgroundColor: "rgba(255,255,255,0.6)",
|
backgroundColor: "rgba(255,255,255,0.6)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress size={48} />
|
<CircularProgress size={48} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "chart" ? renderChart() : renderTable()}
|
{activeTab === "chart" ? renderChart() : renderTable()}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Draggable>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user