新增设备多个数据源,并在设备多选时提供数据源切换功能
This commit is contained in:
@@ -93,26 +93,28 @@ const fetchFromBackend = async (
|
|||||||
const cleaningSCADAUrl = `${config.backendUrl}/querycleaningscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
|
const cleaningSCADAUrl = `${config.backendUrl}/querycleaningscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
|
||||||
// 原始数据
|
// 原始数据
|
||||||
const rawSCADAUrl = `${config.backendUrl}/queryscadadatabydeviceidandtimerange/?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 {
|
try {
|
||||||
const response = await fetch(cleaningSCADAUrl);
|
let response;
|
||||||
|
response = await fetch(cleaningSCADAUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const transformedData = transformBackendData(data, deviceIds);
|
let transformedData = transformBackendData(data, deviceIds);
|
||||||
|
|
||||||
// 如果清洗数据接口返回空结果,使用原始数据接口
|
// 如果清洗数据接口返回空结果,使用原始数据接口
|
||||||
if (transformedData.length === 0) {
|
if (transformedData.length === 0) {
|
||||||
console.log("[SCADADataPanel] 清洗数据接口无结果,使用原始数据接口");
|
console.log("[SCADADataPanel] 清洗数据接口无结果,使用原始数据接口");
|
||||||
const rawResponse = await fetch(rawSCADAUrl);
|
response = await fetch(rawSCADAUrl);
|
||||||
if (!rawResponse.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${rawResponse.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const originData = await rawResponse.json();
|
const data = await response.json();
|
||||||
return transformBackendData(originData, deviceIds);
|
transformedData = transformBackendData(data, deviceIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return transformedData;
|
return transformedData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SCADADataPanel] 从后端获取数据失败:", error);
|
console.error("[SCADADataPanel] 从后端获取数据失败:", error);
|
||||||
@@ -129,11 +131,7 @@ const transformBackendData = (
|
|||||||
deviceIds: string[]
|
deviceIds: string[]
|
||||||
): TimeSeriesPoint[] => {
|
): TimeSeriesPoint[] => {
|
||||||
// 处理后端返回的对象格式: { deviceId: [{time: "...", value: ...}] }
|
// 处理后端返回的对象格式: { deviceId: [{time: "...", value: ...}] }
|
||||||
if (
|
if (backendData && !Array.isArray(backendData)) {
|
||||||
backendData &&
|
|
||||||
typeof backendData === "object" &&
|
|
||||||
!Array.isArray(backendData)
|
|
||||||
) {
|
|
||||||
// 检查是否是设备ID为键的对象格式
|
// 检查是否是设备ID为键的对象格式
|
||||||
const hasDeviceKeys = deviceIds.some((id) => id in backendData);
|
const hasDeviceKeys = deviceIds.some((id) => id in backendData);
|
||||||
|
|
||||||
@@ -174,32 +172,6 @@ const transformBackendData = (
|
|||||||
return result;
|
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);
|
console.warn("[SCADADataPanel] 未知的后端数据格式:", backendData);
|
||||||
return [];
|
return [];
|
||||||
@@ -223,7 +195,8 @@ const ensureValidRange = (
|
|||||||
const buildDataset = (
|
const buildDataset = (
|
||||||
points: TimeSeriesPoint[],
|
points: TimeSeriesPoint[],
|
||||||
deviceIds: string[],
|
deviceIds: string[],
|
||||||
fractionDigits: number
|
fractionDigits: number,
|
||||||
|
showCleaning: boolean
|
||||||
) => {
|
) => {
|
||||||
return points.map((point) => {
|
return points.map((point) => {
|
||||||
const entry: Record<string, any> = {
|
const entry: Record<string, any> = {
|
||||||
@@ -231,15 +204,30 @@ const buildDataset = (
|
|||||||
label: formatTimestamp(point.timestamp),
|
label: formatTimestamp(point.timestamp),
|
||||||
};
|
};
|
||||||
|
|
||||||
deviceIds.forEach((id) => {
|
if (showCleaning) {
|
||||||
const value = point.values[id];
|
deviceIds.forEach((id) => {
|
||||||
entry[id] =
|
["raw", "clean", "sim"].forEach((suffix) => {
|
||||||
typeof value === "number"
|
const key = `${id}_${suffix}`;
|
||||||
? Number.isFinite(value)
|
const value = point.values[key];
|
||||||
? parseFloat(value.toFixed(fractionDigits))
|
entry[key] =
|
||||||
: null
|
typeof value === "number"
|
||||||
: value ?? null;
|
? 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;
|
return entry;
|
||||||
});
|
});
|
||||||
@@ -271,6 +259,82 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
const { data: user } = useGetIdentity<IUser>();
|
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 [from, setFrom] = useState<Dayjs>(() => dayjs().subtract(1, "day"));
|
||||||
const [to, setTo] = useState<Dayjs>(() => dayjs());
|
const [to, setTo] = useState<Dayjs>(() => dayjs());
|
||||||
const [activeTab, setActiveTab] = useState<PanelTab>(defaultTab);
|
const [activeTab, setActiveTab] = useState<PanelTab>(defaultTab);
|
||||||
@@ -280,6 +344,9 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
const [isExpanded, setIsExpanded] = useState<boolean>(true);
|
||||||
const [deviceLabels, setDeviceLabels] = useState<Record<string, string>>({});
|
const [deviceLabels, setDeviceLabels] = useState<Record<string, string>>({});
|
||||||
const [isCleaning, setIsCleaning] = useState<boolean>(false);
|
const [isCleaning, setIsCleaning] = useState<boolean>(false);
|
||||||
|
const [selectedSource, setSelectedSource] = useState<"raw" | "clean" | "sim">(
|
||||||
|
"raw"
|
||||||
|
);
|
||||||
|
|
||||||
// 获取 SCADA 设备信息,生成 deviceLabels
|
// 获取 SCADA 设备信息,生成 deviceLabels
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -321,8 +388,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
const hasData = timeSeries.length > 0;
|
const hasData = timeSeries.length > 0;
|
||||||
|
|
||||||
const dataset = useMemo(
|
const dataset = useMemo(
|
||||||
() => buildDataset(timeSeries, deviceIds, fractionDigits),
|
() => buildDataset(timeSeries, deviceIds, fractionDigits, showCleaning),
|
||||||
[timeSeries, deviceIds, fractionDigits]
|
[timeSeries, deviceIds, fractionDigits, showCleaning]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFetch = useCallback(
|
const handleFetch = useCallback(
|
||||||
@@ -339,7 +406,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { from: rangeFrom, to: rangeTo } = normalizedRange;
|
const { from: rangeFrom, to: rangeTo } = normalizedRange;
|
||||||
const result = await fetchTimeSeriesData(deviceIds, {
|
const result = await customFetcher(deviceIds, {
|
||||||
from: rangeFrom.toDate(),
|
from: rangeFrom.toDate(),
|
||||||
to: rangeTo.toDate(),
|
to: rangeTo.toDate(),
|
||||||
});
|
});
|
||||||
@@ -350,7 +417,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
setLoadingState("error");
|
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) => ({
|
const dynamic = (() => {
|
||||||
field: id,
|
if (showCleaning) {
|
||||||
headerName: deviceLabels?.[id] ?? id,
|
if (deviceIds.length === 1) {
|
||||||
minWidth: 140,
|
return deviceIds.flatMap<GridColDef>((id) => [
|
||||||
flex: 1,
|
{
|
||||||
valueFormatter: (value: any) => {
|
field: `${id}_raw`,
|
||||||
if (value === null || value === undefined) return "--";
|
headerName: `${deviceLabels?.[id] ?? id} (原始)`,
|
||||||
if (Number.isFinite(Number(value))) {
|
minWidth: 140,
|
||||||
return Number(value).toFixed(fractionDigits);
|
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];
|
return [...base, ...dynamic];
|
||||||
}, [deviceIds, deviceLabels, fractionDigits]);
|
}, [deviceIds, deviceLabels, fractionDigits, showCleaning, selectedSource]);
|
||||||
|
|
||||||
const rows = useMemo(
|
const rows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -557,17 +686,71 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
series={deviceIds.map((id, index) => ({
|
series={(() => {
|
||||||
dataKey: id,
|
if (showCleaning) {
|
||||||
label: deviceLabels?.[id] ?? id,
|
if (deviceIds.length === 1) {
|
||||||
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) : "--",
|
showMark: dataset.length < 50,
|
||||||
area: false,
|
curve: "catmullRom",
|
||||||
stack: undefined,
|
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 }}
|
grid={{ vertical: true, horizontal: true }}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiLineElement-root": {
|
"& .MuiLineElement-root": {
|
||||||
@@ -664,8 +847,6 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawerWidth = 920;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 收起时的触发按钮 */}
|
{/* 收起时的触发按钮 */}
|
||||||
@@ -704,7 +885,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 80,
|
top: 80,
|
||||||
right: 16,
|
right: 16,
|
||||||
height: "760px",
|
height: showCleaning && deviceIds.length >= 2 ? "820px" : "760px",
|
||||||
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)",
|
||||||
@@ -868,6 +1049,23 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</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>
|
</Stack>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user