新增数据清洗功能

This commit is contained in:
JIANG
2025-11-05 15:33:04 +08:00
parent c8caba1453
commit bf067aa8eb
2 changed files with 205 additions and 22 deletions

View File

@@ -16,7 +16,6 @@ import {
Drawer,
} from "@mui/material";
import {
Close,
Refresh,
ShowChart,
TableChart,
@@ -31,13 +30,20 @@ import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
import clsx from "clsx";
import config from "@/config/config";
import config, { NETWORK_NAME } from "@/config/config";
import { GeoJSON } from "ol/format";
import { useGetIdentity } from "@refinedev/core";
import { useNotification } from "@refinedev/core";
import axios from "axios";
dayjs.extend(utc);
dayjs.extend(timezone);
type IUser = {
id: number;
name: string;
};
export interface TimeSeriesPoint {
/** ISO8601 时间戳 */
timestamp: string;
@@ -85,8 +91,8 @@ const fetchFromBackend = async (
const endtime = dayjs(range.to).format("YYYY-MM-DD HH:mm:ss");
// 清洗数据接口
const cleaningSCADAUrl = `${config.backendUrl}/querycleaningscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
const originSCADAUrl = `${config.backendUrl}/queryscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
// 原始数据
const rawSCADAUrl = `${config.backendUrl}/queryscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`;
try {
const response = await fetch(cleaningSCADAUrl);
@@ -94,7 +100,20 @@ const fetchFromBackend = async (
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return transformBackendData(data, deviceIds);
const 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}`);
}
const originData = await rawResponse.json();
return transformBackendData(originData, deviceIds);
}
return transformedData;
} catch (error) {
console.error("[SCADADataPanel] 从后端获取数据失败:", error);
throw error;
@@ -232,11 +251,11 @@ const emptyStateMessages: Record<
> = {
chart: {
title: "暂无时序数据",
subtitle: "请选择设备并点击刷新来获取曲线",
subtitle: "请切换时间段来获取曲线",
},
table: {
title: "暂无表格数据",
subtitle: "请选择设备并点击刷新来获取记录",
subtitle: "请切换时间段来获取记录",
},
};
@@ -249,6 +268,9 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
showCleaning = false,
onCleanData,
}) => {
const { open } = useNotification();
const { data: user } = useGetIdentity<IUser>();
const [from, setFrom] = useState<Dayjs>(() => dayjs().subtract(1, "day"));
const [to, setTo] = useState<Dayjs>(() => dayjs());
const [activeTab, setActiveTab] = useState<PanelTab>(defaultTab);
@@ -257,6 +279,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const [error, setError] = useState<string | null>(null);
const [isExpanded, setIsExpanded] = useState<boolean>(true);
const [deviceLabels, setDeviceLabels] = useState<Record<string, string>>({});
const [isCleaning, setIsCleaning] = useState<boolean>(false);
// 获取 SCADA 设备信息,生成 deviceLabels
useEffect(() => {
@@ -330,6 +353,84 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
[deviceIds, fetchTimeSeriesData, hasDevices, normalizedRange]
);
// 处理数据清洗
const handleCleanData = useCallback(async () => {
if (!hasDevices) {
open?.({
type: "error",
message: "请先选择设备",
});
return;
}
if (!user || !user.name) {
open?.({
type: "error",
message: "用户信息无效,请重新登录",
});
return;
}
setIsCleaning(true);
try {
const { from: rangeFrom, to: rangeTo } = normalizedRange;
const startTime = dayjs(rangeFrom).format("YYYY-MM-DD HH:mm:ss");
const endTime = dayjs(rangeTo).format("YYYY-MM-DD HH:mm:ss");
// 调用后端清洗接口
const response = await axios.post(
`${config.backendUrl}/scadadevicedatacleaning/`,
null,
{
params: {
network: NETWORK_NAME,
ids_list: deviceIds.join(","), // 修改:将数组转为逗号分隔字符串
start_time: startTime,
end_time: endTime,
user_name: user.name,
},
}
);
console.log("[SCADADataPanel] 清洗响应:", response.data);
// 处理成功响应
if (response.data === "success" || response.data?.success === true) {
open?.({
type: "success",
message: "数据清洗成功",
description: `已完成 ${deviceIds.length} 个设备的数据清洗`,
});
// 清洗完成后自动刷新数据
await handleFetch("after-cleaning");
// 如果父组件提供了回调,也调用它
onCleanData?.();
} else {
throw new Error(response.data?.message || "清洗失败");
}
} catch (err: any) {
console.error("[SCADADataPanel] 数据清洗失败:", err);
open?.({
type: "error",
message: "数据清洗失败",
description: err.response?.data?.message || err.message || "未知错误",
});
} finally {
setIsCleaning(false);
}
}, [
deviceIds,
hasDevices,
normalizedRange,
user,
open,
onCleanData,
handleFetch,
]);
// 设备变化时自动查询
useEffect(() => {
if (hasDevices) {
@@ -337,7 +438,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
} else {
setTimeSeries([]);
}
}, [deviceIds.join(",")]); // 移除 hasDevices因为它由 deviceIds 决定,避免潜在的依赖循环
}, [deviceIds.join(",")]);
const columns: GridColDef[] = useMemo(() => {
const base: GridColDef[] = [
@@ -389,7 +490,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
justifyContent: "center",
py: 8,
color: "text.secondary",
height: 420,
height: "100%",
}}
>
<ShowChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
@@ -421,10 +522,10 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
];
return (
<Box sx={{ width: "100%", height: 420 }}>
<Box sx={{ width: "100%", height: "100%" }}>
<LineChart
dataset={dataset}
height={420}
height={520}
margin={{ left: 70, right: 40, top: 30, bottom: 90 }}
xAxis={[
{
@@ -496,7 +597,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
legend: {
direction: "row",
position: { horizontal: "middle", vertical: "bottom" },
padding: { top: 20, bottom: 10, left: 0, right: 0 },
padding: { bottom: 2, left: 0, right: 0 },
itemMarkWidth: 16,
itemMarkHeight: 3,
markGap: 8,
@@ -542,7 +643,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
pageSizeOptions={[25, 50, 100]}
sx={{
border: "none",
height: "420px",
height: "100%",
"& .MuiDataGrid-cell": {
borderColor: "#f0f0f0",
},
@@ -732,11 +833,21 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
variant="outlined"
size="small"
color="secondary"
startIcon={<CleaningServices fontSize="small" />}
disabled={!hasDevices || loadingState === "loading"}
onClick={onCleanData}
startIcon={
isCleaning ? (
<CircularProgress size={16} />
) : (
<CleaningServices fontSize="small" />
)
}
disabled={
!hasDevices ||
loadingState === "loading" ||
isCleaning
}
onClick={handleCleanData}
>
{isCleaning ? "清洗中..." : "清洗"}
</Button>
</span>
</Tooltip>
@@ -744,7 +855,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
<Tooltip title="刷新数据">
<span>
<Button
variant="contained"
variant="outlined"
size="small"
color="primary"
startIcon={<Refresh fontSize="small" />}

View File

@@ -48,6 +48,9 @@ import {
} from "@mui/icons-material";
import { FixedSizeList } from "react-window";
import { useNotification } from "@refinedev/core";
import axios from "axios";
import { useGetIdentity } from "@refinedev/core";
import config, { NETWORK_NAME } from "@/config/config";
import { useMap } from "@app/OlMap/MapComponent";
import { GeoJSON } from "ol/format";
@@ -59,7 +62,6 @@ import Feature from "ol/Feature";
import { Point } from "ol/geom";
import { getVectorContext } from "ol/render";
import { unByKey } from "ol/Observable";
import config from "@/config/config";
import dayjs, { Dayjs } from "dayjs";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
@@ -98,6 +100,11 @@ interface SCADADeviceListProps {
onCleanAllData?: (from: Date, to: Date) => void;
}
type IUser = {
id: number;
name: string;
};
const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
devices = [],
onDeviceClick,
@@ -154,6 +161,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
const map = useMap(); // 移到此处,确保在条件检查前调用
const { open } = useNotification();
const { data: user } = useGetIdentity<IUser>();
useEffect(() => {
if (selectedDeviceIds) {
@@ -566,18 +574,82 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
);
// 确认清洗
const handleConfirmClean = useCallback(() => {
const handleConfirmClean = useCallback(async () => {
const error = validateTimeRange(cleanStartTime, cleanEndTime);
if (error) {
setTimeRangeError(error);
return;
}
onCleanAllData?.(cleanStartTime.toDate(), cleanEndTime.toDate());
// 获取所有设备ID
const allDeviceIds = internalDevices.map((d) => d.id);
if (allDeviceIds.length === 0) {
open?.({
type: "error",
message: "无设备可清洗",
});
return;
}
if (!user || !user.name) {
open?.({
type: "error",
message: "用户信息无效,请重新登录",
});
return;
}
try {
const startTime = dayjs(cleanStartTime).format("YYYY-MM-DD HH:mm:ss");
const endTime = dayjs(cleanEndTime).format("YYYY-MM-DD HH:mm:ss");
// 调用后端清洗接口
const response = await axios.post(
`${config.backendUrl}/scadadevicedatacleaning/`,
null,
{
params: {
network: NETWORK_NAME,
ids_list: allDeviceIds.join(","), // 将数组转为逗号分隔字符串
start_time: startTime,
end_time: endTime,
user_name: user.name,
},
}
);
console.log("[SCADADeviceList] 全部清洗响应:", response.data);
// 处理成功响应
if (response.data === "success" || response.data?.success === true) {
open?.({
type: "success",
message: "全部数据清洗成功",
description: `已完成 ${allDeviceIds.length} 个设备的数据清洗`,
});
// 如果父组件提供了回调,也调用它
onCleanAllData?.(cleanStartTime.toDate(), cleanEndTime.toDate());
} else {
throw new Error(response.data?.message || "清洗失败");
}
} catch (err: any) {
console.error("[SCADADeviceList] 数据清洗失败:", err);
open?.({
type: "error",
message: "数据清洗失败",
description: err.response?.data?.message || err.message || "未知错误",
});
}
handleCleanDialogClose();
}, [
cleanStartTime,
cleanEndTime,
validateTimeRange,
internalDevices,
user,
open,
onCleanAllData,
handleCleanDialogClose,
]);