新增数据清洗功能

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, Drawer,
} from "@mui/material"; } from "@mui/material";
import { import {
Close,
Refresh, Refresh,
ShowChart, ShowChart,
TableChart, TableChart,
@@ -31,13 +30,20 @@ import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone"; import timezone from "dayjs/plugin/timezone";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
import clsx from "clsx"; import config, { NETWORK_NAME } from "@/config/config";
import config from "@/config/config";
import { GeoJSON } from "ol/format"; import { GeoJSON } from "ol/format";
import { useGetIdentity } from "@refinedev/core";
import { useNotification } from "@refinedev/core";
import axios from "axios";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
type IUser = {
id: number;
name: string;
};
export interface TimeSeriesPoint { export interface TimeSeriesPoint {
/** ISO8601 时间戳 */ /** ISO8601 时间戳 */
timestamp: string; timestamp: string;
@@ -85,8 +91,8 @@ const fetchFromBackend = async (
const endtime = dayjs(range.to).format("YYYY-MM-DD HH:mm:ss"); const endtime = dayjs(range.to).format("YYYY-MM-DD HH:mm:ss");
// 清洗数据接口 // 清洗数据接口
const cleaningSCADAUrl = `${config.backendUrl}/querycleaningscadadatabydeviceidandtimerange/?ids=${ids}&starttime=${starttime}&endtime=${endtime}`; 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 { try {
const response = await fetch(cleaningSCADAUrl); const response = await fetch(cleaningSCADAUrl);
@@ -94,7 +100,20 @@ const fetchFromBackend = async (
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();
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) { } catch (error) {
console.error("[SCADADataPanel] 从后端获取数据失败:", error); console.error("[SCADADataPanel] 从后端获取数据失败:", error);
throw error; throw error;
@@ -232,11 +251,11 @@ const emptyStateMessages: Record<
> = { > = {
chart: { chart: {
title: "暂无时序数据", title: "暂无时序数据",
subtitle: "请选择设备并点击刷新来获取曲线", subtitle: "请切换时间段来获取曲线",
}, },
table: { table: {
title: "暂无表格数据", title: "暂无表格数据",
subtitle: "请选择设备并点击刷新来获取记录", subtitle: "请切换时间段来获取记录",
}, },
}; };
@@ -249,6 +268,9 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
showCleaning = false, showCleaning = false,
onCleanData, onCleanData,
}) => { }) => {
const { open } = useNotification();
const { data: user } = useGetIdentity<IUser>();
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);
@@ -257,6 +279,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
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);
// 获取 SCADA 设备信息,生成 deviceLabels // 获取 SCADA 设备信息,生成 deviceLabels
useEffect(() => { useEffect(() => {
@@ -330,6 +353,84 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
[deviceIds, fetchTimeSeriesData, hasDevices, normalizedRange] [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(() => { useEffect(() => {
if (hasDevices) { if (hasDevices) {
@@ -337,7 +438,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
} else { } else {
setTimeSeries([]); setTimeSeries([]);
} }
}, [deviceIds.join(",")]); // 移除 hasDevices因为它由 deviceIds 决定,避免潜在的依赖循环 }, [deviceIds.join(",")]);
const columns: GridColDef[] = useMemo(() => { const columns: GridColDef[] = useMemo(() => {
const base: GridColDef[] = [ const base: GridColDef[] = [
@@ -389,7 +490,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
justifyContent: "center", justifyContent: "center",
py: 8, py: 8,
color: "text.secondary", color: "text.secondary",
height: 420, height: "100%",
}} }}
> >
<ShowChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} /> <ShowChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
@@ -421,10 +522,10 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
]; ];
return ( return (
<Box sx={{ width: "100%", height: 420 }}> <Box sx={{ width: "100%", height: "100%" }}>
<LineChart <LineChart
dataset={dataset} dataset={dataset}
height={420} height={520}
margin={{ left: 70, right: 40, top: 30, bottom: 90 }} margin={{ left: 70, right: 40, top: 30, bottom: 90 }}
xAxis={[ xAxis={[
{ {
@@ -496,7 +597,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
legend: { legend: {
direction: "row", direction: "row",
position: { horizontal: "middle", vertical: "bottom" }, position: { horizontal: "middle", vertical: "bottom" },
padding: { top: 20, bottom: 10, left: 0, right: 0 }, padding: { bottom: 2, left: 0, right: 0 },
itemMarkWidth: 16, itemMarkWidth: 16,
itemMarkHeight: 3, itemMarkHeight: 3,
markGap: 8, markGap: 8,
@@ -542,7 +643,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
pageSizeOptions={[25, 50, 100]} pageSizeOptions={[25, 50, 100]}
sx={{ sx={{
border: "none", border: "none",
height: "420px", height: "100%",
"& .MuiDataGrid-cell": { "& .MuiDataGrid-cell": {
borderColor: "#f0f0f0", borderColor: "#f0f0f0",
}, },
@@ -732,11 +833,21 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
variant="outlined" variant="outlined"
size="small" size="small"
color="secondary" color="secondary"
startIcon={<CleaningServices fontSize="small" />} startIcon={
disabled={!hasDevices || loadingState === "loading"} isCleaning ? (
onClick={onCleanData} <CircularProgress size={16} />
) : (
<CleaningServices fontSize="small" />
)
}
disabled={
!hasDevices ||
loadingState === "loading" ||
isCleaning
}
onClick={handleCleanData}
> >
{isCleaning ? "清洗中..." : "清洗"}
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
@@ -744,7 +855,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
<Tooltip title="刷新数据"> <Tooltip title="刷新数据">
<span> <span>
<Button <Button
variant="contained" variant="outlined"
size="small" size="small"
color="primary" color="primary"
startIcon={<Refresh fontSize="small" />} startIcon={<Refresh fontSize="small" />}

View File

@@ -48,6 +48,9 @@ import {
} from "@mui/icons-material"; } from "@mui/icons-material";
import { FixedSizeList } from "react-window"; import { FixedSizeList } from "react-window";
import { useNotification } from "@refinedev/core"; 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 { useMap } from "@app/OlMap/MapComponent";
import { GeoJSON } from "ol/format"; import { GeoJSON } from "ol/format";
@@ -59,7 +62,6 @@ import Feature from "ol/Feature";
import { Point } from "ol/geom"; import { Point } from "ol/geom";
import { getVectorContext } from "ol/render"; import { getVectorContext } from "ol/render";
import { unByKey } from "ol/Observable"; import { unByKey } from "ol/Observable";
import config from "@/config/config";
import dayjs, { Dayjs } from "dayjs"; import dayjs, { Dayjs } from "dayjs";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
@@ -98,6 +100,11 @@ interface SCADADeviceListProps {
onCleanAllData?: (from: Date, to: Date) => void; onCleanAllData?: (from: Date, to: Date) => void;
} }
type IUser = {
id: number;
name: string;
};
const SCADADeviceList: React.FC<SCADADeviceListProps> = ({ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
devices = [], devices = [],
onDeviceClick, onDeviceClick,
@@ -154,6 +161,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
const map = useMap(); // 移到此处,确保在条件检查前调用 const map = useMap(); // 移到此处,确保在条件检查前调用
const { open } = useNotification(); const { open } = useNotification();
const { data: user } = useGetIdentity<IUser>();
useEffect(() => { useEffect(() => {
if (selectedDeviceIds) { if (selectedDeviceIds) {
@@ -566,18 +574,82 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
); );
// 确认清洗 // 确认清洗
const handleConfirmClean = useCallback(() => { const handleConfirmClean = useCallback(async () => {
const error = validateTimeRange(cleanStartTime, cleanEndTime); const error = validateTimeRange(cleanStartTime, cleanEndTime);
if (error) { if (error) {
setTimeRangeError(error); setTimeRangeError(error);
return; 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(); handleCleanDialogClose();
}, [ }, [
cleanStartTime, cleanStartTime,
cleanEndTime, cleanEndTime,
validateTimeRange, validateTimeRange,
internalDevices,
user,
open,
onCleanAllData, onCleanAllData,
handleCleanDialogClose, handleCleanDialogClose,
]); ]);