数据面板新增缩放滑块,调整样式

This commit is contained in:
JIANG
2025-11-07 09:57:39 +08:00
parent 7702ba9edf
commit 4f0714b5f6
3 changed files with 260 additions and 210 deletions

View File

@@ -14,6 +14,7 @@ import {
Tooltip, Tooltip,
Typography, Typography,
Drawer, Drawer,
Slider,
} from "@mui/material"; } from "@mui/material";
import { import {
Refresh, Refresh,
@@ -35,7 +36,6 @@ import { GeoJSON } from "ol/format";
import { useGetIdentity } from "@refinedev/core"; import { useGetIdentity } from "@refinedev/core";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import axios from "axios"; import axios from "axios";
import { set } from "date-fns";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
@@ -349,6 +349,9 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
"raw" | "clean" | "sim" | "all" "raw" | "clean" | "sim" | "all"
>("all"); >("all");
// 滑块状态:用于图表缩放
const [zoomRange, setZoomRange] = useState<[number, number]>([0, 100]);
// 获取 SCADA 设备信息,生成 deviceLabels // 获取 SCADA 设备信息,生成 deviceLabels
useEffect(() => { useEffect(() => {
const fetchDeviceLabels = async () => { const fetchDeviceLabels = async () => {
@@ -393,6 +396,21 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
[timeSeries, deviceIds, fractionDigits, showCleaning] [timeSeries, deviceIds, fractionDigits, showCleaning]
); );
// 根据滑块范围过滤数据集
const filteredDataset = useMemo(() => {
if (dataset.length === 0) return dataset;
const startIndex = Math.floor((zoomRange[0] / 100) * dataset.length);
const endIndex = Math.ceil((zoomRange[1] / 100) * dataset.length);
return dataset.slice(startIndex, endIndex);
}, [dataset, zoomRange]);
// 重置滑块范围当数据变化时
useEffect(() => {
setZoomRange([0, 100]);
}, [timeSeries]);
const handleFetch = useCallback( const handleFetch = useCallback(
async (reason: string) => { async (reason: string) => {
if (!hasDevices) { if (!hasDevices) {
@@ -404,9 +422,6 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
setLoadingState("loading"); setLoadingState("loading");
setError(null); setError(null);
if (deviceIds.length > 1) {
setSelectedSource("clean");
}
try { try {
const { from: rangeFrom, to: rangeTo } = normalizedRange; const { from: rangeFrom, to: rangeTo } = normalizedRange;
const result = await customFetcher(deviceIds, { const result = await customFetcher(deviceIds, {
@@ -510,6 +525,15 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
} }
}, [deviceIds.join(",")]); }, [deviceIds.join(",")]);
// 当设备数量变化时,调整数据源选择
useEffect(() => {
if (deviceIds.length > 1 && selectedSource === "all") {
setSelectedSource("clean");
} else if (deviceIds.length === 1 && selectedSource !== "all") {
setSelectedSource("all");
}
}, [deviceIds.length]);
const columns: GridColDef[] = useMemo(() => { const columns: GridColDef[] = useMemo(() => {
const base: GridColDef[] = [ const base: GridColDef[] = [
{ {
@@ -655,50 +679,107 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
"#3f51b5", // 靛蓝色 "#3f51b5", // 靛蓝色
]; ];
// 获取当前显示范围的时间边界
const getTimeRangeLabel = () => {
if (filteredDataset.length === 0) return "";
const firstTime = filteredDataset[0].time;
const lastTime = filteredDataset[filteredDataset.length - 1].time;
if (firstTime instanceof Date && lastTime instanceof Date) {
return `${dayjs(firstTime).format("MM-DD HH:mm")} ~ ${dayjs(
lastTime
).format("MM-DD HH:mm")}`;
}
return "";
};
return ( return (
<Box sx={{ width: "100%", height: "100%" }}> <Box
<LineChart sx={{
dataset={dataset} width: "100%",
height={520} height: "100%",
margin={{ left: 70, right: 40, top: 30, bottom: 90 }} display: "flex",
xAxis={[ flexDirection: "column",
{ }}
dataKey: "time", >
scaleType: "time", <Box sx={{ flex: 1 }}>
valueFormatter: (value) => <LineChart
value instanceof Date dataset={filteredDataset}
? dayjs(value).format("MM-DD HH:mm") height={480}
: String(value), margin={{ left: 70, right: 40, top: 30, bottom: 90 }}
tickLabelStyle: { xAxis={[
angle: -45, {
textAnchor: "end", dataKey: "time",
fontSize: 11, scaleType: "time",
fill: "#666", valueFormatter: (value) =>
value instanceof Date
? dayjs(value).format("MM-DD HH:mm")
: String(value),
tickLabelStyle: {
angle: -45,
textAnchor: "end",
fontSize: 11,
fill: "#666",
},
}, },
}, ]}
]} yAxis={[
yAxis={[ {
{ label: "数值",
label: "数值", labelStyle: {
labelStyle: { fontSize: 13,
fontSize: 13, fill: "#333",
fill: "#333", fontWeight: 500,
fontWeight: 500, },
tickLabelStyle: {
fontSize: 11,
fill: "#666",
},
}, },
tickLabelStyle: { ]}
fontSize: 11, series={(() => {
fill: "#666", if (showCleaning) {
}, if (selectedSource === "all") {
}, // 全部模式:显示所有设备的三种数据
]} return deviceIds.flatMap((id, index) => [
series={(() => { {
if (showCleaning) { dataKey: `${id}_raw`,
if (selectedSource === "all") { label: `${deviceLabels?.[id] ?? id} (原始)`,
// 全部模式:显示所有设备的三种数据 showMark: dataset.length < 50,
return deviceIds.flatMap((id, index) => [ curve: "catmullRom",
{ color: colors[index % colors.length],
dataKey: `${id}_raw`, valueFormatter: (value: number | null) =>
label: `${deviceLabels?.[id] ?? id} (原始)`, value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
},
{
dataKey: `${id}_clean`,
label: `${deviceLabels?.[id] ?? id} (清洗)`,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[(index + 3) % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
},
{
dataKey: `${id}_sim`,
label: `${deviceLabels?.[id] ?? id} (模拟)`,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[(index + 6) % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
},
]);
} else {
// 单一数据源模式:只显示选中的数据源
return deviceIds.map((id, index) => ({
dataKey: `${id}_${selectedSource}`,
label: deviceLabels?.[id] ?? id,
showMark: dataset.length < 50, showMark: dataset.length < 50,
curve: "catmullRom", curve: "catmullRom",
color: colors[index % colors.length], color: colors[index % colors.length],
@@ -706,34 +787,11 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
value !== null ? value.toFixed(fractionDigits) : "--", value !== null ? value.toFixed(fractionDigits) : "--",
area: false, area: false,
stack: undefined, stack: undefined,
}, }));
{ }
dataKey: `${id}_clean`,
label: `${deviceLabels?.[id] ?? id} (清洗)`,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[(index + 3) % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
},
{
dataKey: `${id}_sim`,
label: `${deviceLabels?.[id] ?? id} (模拟)`,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[(index + 6) % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
},
]);
} else { } else {
// 单一数据源模式:只显示选中的数据源
return deviceIds.map((id, index) => ({ return deviceIds.map((id, index) => ({
dataKey: `${id}_${selectedSource}`, dataKey: id,
label: deviceLabels?.[id] ?? id, label: deviceLabels?.[id] ?? id,
showMark: dataset.length < 50, showMark: dataset.length < 50,
curve: "catmullRom", curve: "catmullRom",
@@ -744,68 +802,134 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
stack: undefined, stack: undefined,
})); }));
} }
} else { })()}
return deviceIds.map((id, index) => ({ grid={{ vertical: true, horizontal: true }}
dataKey: id, sx={{
label: deviceLabels?.[id] ?? id, "& .MuiLineElement-root": {
showMark: dataset.length < 50, strokeWidth: 2.5,
curve: "catmullRom", strokeLinecap: "round",
color: colors[index % colors.length], strokeLinejoin: "round",
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
}));
}
})()}
grid={{ vertical: true, horizontal: true }}
sx={{
"& .MuiLineElement-root": {
strokeWidth: 2.5,
strokeLinecap: "round",
strokeLinejoin: "round",
},
"& .MuiMarkElement-root": {
scale: "0.8",
strokeWidth: 2,
},
"& .MuiChartsAxis-line": {
stroke: "#e0e0e0",
strokeWidth: 1,
},
"& .MuiChartsAxis-tick": {
stroke: "#e0e0e0",
strokeWidth: 1,
},
"& .MuiChartsGrid-line": {
stroke: "#f5f5f5",
strokeWidth: 1,
strokeDasharray: "3 3",
},
}}
slotProps={{
legend: {
direction: "row",
position: { horizontal: "middle", vertical: "bottom" },
padding: { bottom: 2, left: 0, right: 0 },
itemMarkWidth: 16,
itemMarkHeight: 3,
markGap: 8,
itemGap: 16,
labelStyle: {
fontSize: 12,
fill: "#333",
fontWeight: 500,
}, },
}, "& .MuiMarkElement-root": {
loadingOverlay: { scale: "0.8",
style: { backgroundColor: "rgba(255, 255, 255, 0.7)" }, strokeWidth: 2,
}, },
}} "& .MuiChartsAxis-line": {
tooltip={{ stroke: "#e0e0e0",
trigger: "axis", strokeWidth: 1,
}} },
/> "& .MuiChartsAxis-tick": {
stroke: "#e0e0e0",
strokeWidth: 1,
},
"& .MuiChartsGrid-line": {
stroke: "#d0d0d0",
strokeWidth: 0.8,
strokeDasharray: "4 4",
},
}}
slotProps={{
legend: {
direction: "row",
position: { horizontal: "middle", vertical: "bottom" },
padding: { bottom: 2, left: 0, right: 0 },
itemMarkWidth: 16,
itemMarkHeight: 3,
markGap: 8,
itemGap: 16,
labelStyle: {
fontSize: 12,
fill: "#333",
fontWeight: 500,
},
},
loadingOverlay: {
style: { backgroundColor: "rgba(255, 255, 255, 0.7)" },
},
}}
tooltip={{
trigger: "axis",
}}
/>
</Box>
{/* 时间范围滑块 */}
<Box sx={{ px: 3, pb: 2, pt: 1 }}>
<Stack direction="row" spacing={2} alignItems="center">
<Typography
variant="body2"
sx={{ minWidth: 60, color: "text.secondary", fontSize: "0.8rem" }}
>
</Typography>
<Slider
value={zoomRange}
onChange={(_, newValue) =>
setZoomRange(newValue as [number, number])
}
valueLabelDisplay="auto"
valueLabelFormat={(value) => {
const index = Math.floor((value / 100) * dataset.length);
if (dataset[index] && dataset[index].time instanceof Date) {
return dayjs(dataset[index].time).format("MM-DD HH:mm");
}
return `${value}%`;
}}
marks={[
{
value: 0,
label:
dataset.length > 0 && dataset[0].time instanceof Date
? dayjs(dataset[0].time).format("MM-DD HH:mm")
: "起始",
},
{
value: 100,
label:
dataset.length > 0 &&
dataset[dataset.length - 1].time instanceof Date
? dayjs(dataset[dataset.length - 1].time).format(
"MM-DD HH:mm"
)
: "结束",
},
]}
sx={{
flex: 1,
"& .MuiSlider-thumb": {
width: 16,
height: 16,
},
"& .MuiSlider-markLabel": {
fontSize: "0.7rem",
color: "text.secondary",
},
}}
/>
<Button
size="small"
variant="outlined"
onClick={() => setZoomRange([0, 100])}
sx={{ minWidth: 60, fontSize: "0.75rem" }}
>
</Button>
</Stack>
{getTimeRangeLabel() && (
<Typography
variant="caption"
sx={{
color: "primary.main",
display: "block",
textAlign: "center",
mt: 0.5,
}}
>
: {getTimeRangeLabel()} ( {filteredDataset.length}{" "}
)
</Typography>
)}
</Box>
</Box> </Box>
); );
}; };
@@ -892,7 +1016,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
position: "absolute", position: "absolute",
top: 80, top: 80,
right: 16, right: 16,
height: "820px", height: "860px",
borderRadius: "12px", borderRadius: "12px",
boxShadow: boxShadow:
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)", "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",

View File

@@ -135,7 +135,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null); const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const filterBoxRef = useRef<HTMLDivElement | null>(null); const filterBoxRef = useRef<HTMLDivElement | null>(null);
const [listHeight, setListHeight] = useState<number>(560); const [listHeight, setListHeight] = useState<number>(600);
// 清洗对话框状态 // 清洗对话框状态
const [cleanDialogOpen, setCleanDialogOpen] = useState<boolean>(false); const [cleanDialogOpen, setCleanDialogOpen] = useState<boolean>(false);
@@ -748,7 +748,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
useEffect(() => { useEffect(() => {
const updateListHeight = () => { const updateListHeight = () => {
if (filterBoxRef.current) { if (filterBoxRef.current) {
const drawerHeight = 820; // Drawer 总高度 const drawerHeight = 860; // Drawer 总高度
const headerHeight = 73; // 头部高度(估算) const headerHeight = 73; // 头部高度(估算)
const dividerHeight = 1; // 分隔线高度 const dividerHeight = 1; // 分隔线高度
const filterBoxHeight = filterBoxRef.current.offsetHeight; const filterBoxHeight = filterBoxRef.current.offsetHeight;
@@ -810,7 +810,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
flexShrink: 0, flexShrink: 0,
"& .MuiDrawer-paper": { "& .MuiDrawer-paper": {
width: 360, width: 360,
height: "820px", height: "860px",
boxSizing: "border-box", boxSizing: "border-box",
position: "absolute", position: "absolute",
top: 80, top: 80,

View File

@@ -1,74 +0,0 @@
"use client";
import React, { useCallback, useMemo, useState } from "react";
import { Box, Stack } from "@mui/material";
import SCADADeviceList from "./SCADADeviceList";
import SCADADataPanel from "./SCADADataPanel";
/**
* 集成面板:左侧为 SCADA 设备列表(支持从地图选择),右侧为历史数据面板(曲线/表格)。
* - 使用 SCADADeviceList 内置的地图点击选择功能
* - 使用 SCADADataPanel 的时间段查询与图表展示
* - 两者通过选中设备 ID 进行联动
*/
export interface SCADAIntegratedPanelProps {
/** 初始选中设备 ID 列表 */
initialSelection?: string[];
/** 是否展示数据清洗相关功能(传递给两个子面板) */
showCleaning?: boolean;
/** 是否显示右侧数据面板 */
showDataPanel?: boolean;
/** 数据面板默认选项卡 */
dataPanelDefaultTab?: "chart" | "table";
/** 数据面板小数位数 */
fractionDigits?: number;
}
const SCADAIntegratedPanel: React.FC<SCADAIntegratedPanelProps> = ({
initialSelection = [],
showCleaning = false,
showDataPanel = true,
dataPanelDefaultTab = "chart",
fractionDigits = 2,
}) => {
const [selectedIds, setSelectedIds] = useState<string[]>(initialSelection);
// 通过变更 key 强制重新挂载 DataPanel从而在“清洗全部”后自动刷新
const [dataPanelKey, setDataPanelKey] = useState<number>(0);
const handleSelectionChange = useCallback((ids: string[]) => {
setSelectedIds(ids);
}, []);
// 清洗全部数据后,强制刷新右侧数据面板(重新挂载触发首轮查询)
const handleCleanAllData = useCallback((_from: Date, _to: Date) => {
setDataPanelKey((k) => k + 1);
}, []);
const hasSelection = useMemo(() => selectedIds.length > 0, [selectedIds]);
return (
<Box sx={{ position: "relative", width: "100%", height: "100%" }}>
{/* 左侧:设备列表(内置抽屉布局) */}
<SCADADeviceList
selectedDeviceIds={selectedIds}
onSelectionChange={handleSelectionChange}
showCleaning={showCleaning}
onCleanAllData={handleCleanAllData}
/>
{/* 右侧:历史数据面板(内置抽屉布局) */}
{showDataPanel && (
<SCADADataPanel
key={`data-panel-${dataPanelKey}`}
deviceIds={selectedIds}
visible={true}
defaultTab={dataPanelDefaultTab}
fractionDigits={fractionDigits}
showCleaning={showCleaning}
/>
)}
</Box>
);
};
export default SCADAIntegratedPanel;