新增数据清洗功能
This commit is contained in:
@@ -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" />}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user