新增设备多个数据源,并在设备多选时提供数据源切换功能

This commit is contained in:
JIANG
2025-11-06 16:21:59 +08:00
parent 2f7c82cf08
commit eebac8f8b0

View File

@@ -93,26 +93,28 @@ const fetchFromBackend = async (
const cleaningSCADAUrl = `${config.backendUrl}/querycleaningscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
// 原始数据
const rawSCADAUrl = `${config.backendUrl}/queryscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
// 模拟数据接口
const simulationSCADAUrl = `${config.backendUrl}/querysimulationscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
try {
const response = await fetch(cleaningSCADAUrl);
let response;
response = await fetch(cleaningSCADAUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const transformedData = transformBackendData(data, deviceIds);
let transformedData = transformBackendData(data, deviceIds);
// 如果清洗数据接口返回空结果,使用原始数据接口
if (transformedData.length === 0) {
console.log("[SCADADataPanel] 清洗数据接口无结果,使用原始数据接口");
const rawResponse = await fetch(rawSCADAUrl);
if (!rawResponse.ok) {
throw new Error(`HTTP error! status: ${rawResponse.status}`);
response = await fetch(rawSCADAUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const originData = await rawResponse.json();
return transformBackendData(originData, deviceIds);
const data = await response.json();
transformedData = transformBackendData(data, deviceIds);
}
return transformedData;
} catch (error) {
console.error("[SCADADataPanel] 从后端获取数据失败:", error);
@@ -129,11 +131,7 @@ const transformBackendData = (
deviceIds: string[]
): TimeSeriesPoint[] => {
// 处理后端返回的对象格式: { deviceId: [{time: "...", value: ...}] }
if (
backendData &&
typeof backendData === "object" &&
!Array.isArray(backendData)
) {
if (backendData && !Array.isArray(backendData)) {
// 检查是否是设备ID为键的对象格式
const hasDeviceKeys = deviceIds.some((id) => id in backendData);
@@ -174,32 +172,6 @@ const transformBackendData = (
return result;
}
}
// 如果后端返回的是数组格式,每个元素包含时间戳和各设备值
if (Array.isArray(backendData)) {
return backendData.map((item: any) => ({
timestamp: item.timestamp || item.time || item._time,
values: deviceIds.reduce<Record<string, number | null>>((acc, id) => {
acc[id] = item[id] ?? item.values?.[id] ?? null;
return acc;
}, {}),
}));
}
// 如果后端返回的是对象格式,包含 timestamps 和 data
if (backendData && backendData.timestamps && backendData.data) {
return backendData.timestamps.map((timestamp: string, index: number) => {
const values = deviceIds.reduce<Record<string, number | null>>(
(acc, id) => {
acc[id] = backendData.data[id]?.[index] ?? null;
return acc;
},
{}
);
return { timestamp, values };
});
}
// 默认返回空数组
console.warn("[SCADADataPanel] 未知的后端数据格式:", backendData);
return [];
@@ -223,7 +195,8 @@ const ensureValidRange = (
const buildDataset = (
points: TimeSeriesPoint[],
deviceIds: string[],
fractionDigits: number
fractionDigits: number,
showCleaning: boolean
) => {
return points.map((point) => {
const entry: Record<string, any> = {
@@ -231,15 +204,30 @@ const buildDataset = (
label: formatTimestamp(point.timestamp),
};
deviceIds.forEach((id) => {
const value = point.values[id];
entry[id] =
typeof value === "number"
? Number.isFinite(value)
? parseFloat(value.toFixed(fractionDigits))
: null
: value ?? null;
});
if (showCleaning) {
deviceIds.forEach((id) => {
["raw", "clean", "sim"].forEach((suffix) => {
const key = `${id}_${suffix}`;
const value = point.values[key];
entry[key] =
typeof value === "number"
? Number.isFinite(value)
? parseFloat(value.toFixed(fractionDigits))
: null
: value ?? null;
});
});
} else {
deviceIds.forEach((id) => {
const value = point.values[id];
entry[id] =
typeof value === "number"
? Number.isFinite(value)
? parseFloat(value.toFixed(fractionDigits))
: null
: value ?? null;
});
}
return entry;
});
@@ -271,6 +259,82 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const { open } = useNotification();
const { data: user } = useGetIdentity<IUser>();
const customFetcher = useMemo(() => {
if (!showCleaning) {
return fetchTimeSeriesData;
}
return async (
deviceIds: string[],
range: { from: Date; to: Date }
): Promise<TimeSeriesPoint[]> => {
const ids = deviceIds.join(",");
const starttime = dayjs(range.from).format("YYYY-MM-DD HH:mm:ss");
const endtime = dayjs(range.to).format("YYYY-MM-DD HH:mm:ss");
const cleaningUrl = `${config.backendUrl}/querycleaningscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
const rawUrl = `${config.backendUrl}/queryscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
const simUrl = `${config.backendUrl}/querysimulationscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
try {
const [cleanRes, rawRes, simRes] = await Promise.all([
fetch(cleaningUrl)
.then((r) => (r.ok ? r.json() : null))
.catch(() => null),
fetch(rawUrl)
.then((r) => (r.ok ? r.json() : null))
.catch(() => null),
fetch(simUrl)
.then((r) => (r.ok ? r.json() : null))
.catch(() => null),
]);
const timeMap = new Map<string, Record<string, number | null>>();
const processData = (data: any, suffix: string) => {
if (!data) return;
deviceIds.forEach((deviceId) => {
const deviceData = data[deviceId];
if (Array.isArray(deviceData)) {
deviceData.forEach((item: any) => {
const timestamp = item.time || item.timestamp || item._time;
if (timestamp) {
if (!timeMap.has(timestamp)) {
timeMap.set(timestamp, {});
}
const values = timeMap.get(timestamp)!;
values[`${deviceId}_${suffix}`] =
typeof item.value === "number" ? item.value : null;
}
});
}
});
};
processData(cleanRes, "clean");
processData(rawRes, "raw");
processData(simRes, "sim");
const result = Array.from(timeMap.entries()).map(
([timestamp, values]) => ({
timestamp,
values,
})
);
result.sort(
(a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
return result;
} catch (error) {
console.error("[SCADADataPanel] 获取三种数据失败:", error);
throw error;
}
};
}, [showCleaning, fetchTimeSeriesData]);
const [from, setFrom] = useState<Dayjs>(() => dayjs().subtract(1, "day"));
const [to, setTo] = useState<Dayjs>(() => dayjs());
const [activeTab, setActiveTab] = useState<PanelTab>(defaultTab);
@@ -280,6 +344,9 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const [isExpanded, setIsExpanded] = useState<boolean>(true);
const [deviceLabels, setDeviceLabels] = useState<Record<string, string>>({});
const [isCleaning, setIsCleaning] = useState<boolean>(false);
const [selectedSource, setSelectedSource] = useState<"raw" | "clean" | "sim">(
"raw"
);
// 获取 SCADA 设备信息,生成 deviceLabels
useEffect(() => {
@@ -321,8 +388,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const hasData = timeSeries.length > 0;
const dataset = useMemo(
() => buildDataset(timeSeries, deviceIds, fractionDigits),
[timeSeries, deviceIds, fractionDigits]
() => buildDataset(timeSeries, deviceIds, fractionDigits, showCleaning),
[timeSeries, deviceIds, fractionDigits, showCleaning]
);
const handleFetch = useCallback(
@@ -339,7 +406,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
try {
const { from: rangeFrom, to: rangeTo } = normalizedRange;
const result = await fetchTimeSeriesData(deviceIds, {
const result = await customFetcher(deviceIds, {
from: rangeFrom.toDate(),
to: rangeTo.toDate(),
});
@@ -350,7 +417,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
setLoadingState("error");
}
},
[deviceIds, fetchTimeSeriesData, hasDevices, normalizedRange]
[deviceIds, customFetcher, hasDevices, normalizedRange]
);
// 处理数据清洗
@@ -450,22 +517,84 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
},
];
const dynamic = deviceIds.map<GridColDef>((id) => ({
field: id,
headerName: deviceLabels?.[id] ?? id,
minWidth: 140,
flex: 1,
valueFormatter: (value: any) => {
if (value === null || value === undefined) return "--";
if (Number.isFinite(Number(value))) {
return Number(value).toFixed(fractionDigits);
const dynamic = (() => {
if (showCleaning) {
if (deviceIds.length === 1) {
return deviceIds.flatMap<GridColDef>((id) => [
{
field: `${id}_raw`,
headerName: `${deviceLabels?.[id] ?? id} (原始)`,
minWidth: 140,
flex: 1,
valueFormatter: (value: any) => {
if (value === null || value === undefined) return "--";
if (Number.isFinite(Number(value))) {
return Number(value).toFixed(fractionDigits);
}
return String(value);
},
},
{
field: `${id}_clean`,
headerName: `${deviceLabels?.[id] ?? id} (清洗)`,
minWidth: 140,
flex: 1,
valueFormatter: (value: any) => {
if (value === null || value === undefined) return "--";
if (Number.isFinite(Number(value))) {
return Number(value).toFixed(fractionDigits);
}
return String(value);
},
},
{
field: `${id}_sim`,
headerName: `${deviceLabels?.[id] ?? id} (模拟)`,
minWidth: 140,
flex: 1,
valueFormatter: (value: any) => {
if (value === null || value === undefined) return "--";
if (Number.isFinite(Number(value))) {
return Number(value).toFixed(fractionDigits);
}
return String(value);
},
},
]);
} else {
return deviceIds.map<GridColDef>((id) => ({
field: `${id}_${selectedSource}`,
headerName: deviceLabels?.[id] ?? id,
minWidth: 140,
flex: 1,
valueFormatter: (value: any) => {
if (value === null || value === undefined) return "--";
if (Number.isFinite(Number(value))) {
return Number(value).toFixed(fractionDigits);
}
return String(value);
},
}));
}
return String(value);
},
}));
} else {
return deviceIds.map<GridColDef>((id) => ({
field: id,
headerName: deviceLabels?.[id] ?? id,
minWidth: 140,
flex: 1,
valueFormatter: (value: any) => {
if (value === null || value === undefined) return "--";
if (Number.isFinite(Number(value))) {
return Number(value).toFixed(fractionDigits);
}
return String(value);
},
}));
}
})();
return [...base, ...dynamic];
}, [deviceIds, deviceLabels, fractionDigits]);
}, [deviceIds, deviceLabels, fractionDigits, showCleaning, selectedSource]);
const rows = useMemo(
() =>
@@ -557,17 +686,71 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
},
},
]}
series={deviceIds.map((id, index) => ({
dataKey: id,
label: deviceLabels?.[id] ?? id,
showMark: dataset.length < 50, // 数据点少时显示标记
curve: "catmullRom", // 使用平滑曲线
color: colors[index % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
}))}
series={(() => {
if (showCleaning) {
if (deviceIds.length === 1) {
return deviceIds.flatMap((id, index) => [
{
dataKey: `${id}_raw`,
label: `${deviceLabels?.[id] ?? id} (原始)`,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[index % colors.length],
valueFormatter: (value: number | null) =>
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,
curve: "catmullRom",
color: colors[index % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
}));
}
} else {
return deviceIds.map((id, index) => ({
dataKey: id,
label: deviceLabels?.[id] ?? id,
showMark: dataset.length < 50,
curve: "catmullRom",
color: colors[index % colors.length],
valueFormatter: (value: number | null) =>
value !== null ? value.toFixed(fractionDigits) : "--",
area: false,
stack: undefined,
}));
}
})()}
grid={{ vertical: true, horizontal: true }}
sx={{
"& .MuiLineElement-root": {
@@ -664,8 +847,6 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
);
};
const drawerWidth = 920;
return (
<>
{/* 收起时的触发按钮 */}
@@ -704,7 +885,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
position: "absolute",
top: 80,
right: 16,
height: "760px",
height: showCleaning && deviceIds.length >= 2 ? "820px" : "760px",
borderRadius: "12px",
boxShadow:
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
@@ -868,6 +1049,23 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
</Tooltip>
</Stack>
</Stack>
{showCleaning && deviceIds.length >= 2 && (
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="body2" sx={{ fontWeight: 500 }}>
:
</Typography>
<Tabs
value={selectedSource}
onChange={(_, value: "raw" | "clean" | "sim") =>
setSelectedSource(value)
}
>
<Tab value="clean" label="清洗" />
<Tab value="raw" label="原始" />
<Tab value="sim" label="模拟" />
</Tabs>
</Stack>
)}
</Stack>
</LocalizationProvider>