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