Files
TJWaterFrontend_Refine/src/components/olmap/BurstDetection/SchemeQuery.tsx
T

356 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useState } from "react";
import {
Box,
Button,
Card,
CardContent,
Checkbox,
Chip,
Collapse,
FormControlLabel,
IconButton,
Tooltip,
Typography,
} from "@mui/material";
import { InfoOutlined as InfoIcon } from "@mui/icons-material";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-cn";
import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api";
import { NETWORK_NAME } from "@config/config";
import {
BurstDetectionResult,
BurstDetectionSchemeDetail,
BurstDetectionSchemeRecord,
} from "./types";
interface Props {
onViewResult: (result: BurstDetectionResult) => void;
schemes?: BurstDetectionSchemeRecord[];
onSchemesChange?: (schemes: BurstDetectionSchemeRecord[]) => void;
}
const SchemeQuery: React.FC<Props> = ({ onViewResult, schemes: externalSchemes, onSchemesChange }) => {
const { open } = useNotification();
const [queryAll, setQueryAll] = useState(true);
const [queryDate, setQueryDate] = useState<Dayjs | null>(dayjs());
const [internalSchemes, setInternalSchemes] = useState<BurstDetectionSchemeRecord[]>([]);
const [loading, setLoading] = useState(false);
const [expandedId, setExpandedId] = useState<number | null>(null);
const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes;
const setSchemes = onSchemesChange || setInternalSchemes;
const buildDisplayResult = (
scheme: Pick<BurstDetectionSchemeRecord, "scheme_name" | "username" | "create_time">,
detail?: BurstDetectionSchemeDetail,
): BurstDetectionResult | null => {
const payload = detail?.result_payload;
const summary = detail?.result_summary;
const fallbackLatestDay = summary?.latest_day;
if (!payload && !summary) return null;
return {
network: payload?.network ?? detail?.network ?? NETWORK_NAME,
sensor_nodes: payload?.sensor_nodes ?? detail?.sensor_nodes ?? [],
observed_source: payload?.observed_source ?? detail?.observed_source ?? "stored_scheme",
sample_count: payload?.sample_count ?? 0,
points_per_day: payload?.points_per_day ?? detail?.algorithm_params?.points_per_day ?? 1440,
day_count: payload?.day_count ?? payload?.rows?.length ?? 0,
rows: payload?.rows ?? (fallbackLatestDay ? [fallbackLatestDay] : []),
summary:
payload?.summary ??
(summary
? summary
: {
burst_detected: false,
latest_day: fallbackLatestDay ?? { Day: 0, Score: 0, Prediction: 1, IsBurst: false },
most_anomalous_day: 0,
anomaly_days: [],
anomaly_day_count: 0,
latest_sensor_rankings: [],
}),
scada_window: payload?.scada_window ?? detail?.scada_window,
scheme_name: payload?.scheme_name ?? scheme.scheme_name,
username: payload?.username ?? scheme.username,
create_time: payload?.create_time ?? scheme.create_time,
algorithm_params: payload?.algorithm_params ?? detail?.algorithm_params,
};
};
const handleQuery = async () => {
setLoading(true);
try {
const params: Record<string, string> = { network: NETWORK_NAME };
if (!queryAll && queryDate) {
params.query_date = queryDate.startOf("day").toISOString();
}
const response = await api.get("/api/v1/burst-detection/schemes/", { params });
const nextSchemes = response.data as BurstDetectionSchemeRecord[];
setSchemes(nextSchemes);
open?.({
type: "success",
message: "查询成功",
description: `共找到 ${nextSchemes.length} 条侦测记录。`,
});
} catch (error: any) {
open?.({
type: "error",
message: "查询失败",
description: error?.response?.data?.detail ?? "无法获取侦测方案列表",
});
} finally {
setLoading(false);
}
};
const handleViewSchemeResult = async (schemeName: string) => {
try {
const response = await api.get(
`/api/v1/burst-detection/schemes/${encodeURIComponent(schemeName)}`,
{ params: { network: NETWORK_NAME } },
);
const schemeRecord = response.data as BurstDetectionSchemeRecord & {
result_payload?: BurstDetectionResult;
};
const normalizedResult =
schemeRecord.result_payload ??
buildDisplayResult(
{
scheme_name: schemeRecord.scheme_name,
username: schemeRecord.username,
create_time: schemeRecord.create_time,
},
schemeRecord.scheme_detail,
);
if (!normalizedResult) {
throw new Error("方案详情缺少侦测结果数据");
}
onViewResult(normalizedResult);
open?.({
type: "success",
message: "方案加载成功",
description: `已加载方案:${schemeName}`,
});
} catch (error: any) {
open?.({
type: "error",
message: "查看详情失败",
description: error?.response?.data?.detail ?? error?.message ?? "无法获取方案详情",
});
}
};
return (
<Box className="flex h-full flex-col">
<Box className="mb-2 rounded bg-gray-50 p-2">
<Box className="flex items-center justify-between gap-2">
<Box className="flex items-center gap-2">
<FormControlLabel
control={
<Checkbox
size="small"
checked={queryAll}
onChange={(event) => setQueryAll(event.target.checked)}
/>
}
label={<Typography variant="body2"></Typography>}
className="m-0"
/>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
<DatePicker
value={queryDate}
onChange={setQueryDate}
disabled={queryAll}
format="YYYY-MM-DD"
slotProps={{ textField: { size: "small", sx: { width: 180 } } }}
/>
</LocalizationProvider>
</Box>
<Button
variant="contained"
onClick={handleQuery}
disabled={loading}
size="small"
className="bg-blue-600 hover:bg-blue-700"
sx={{ minWidth: 80 }}
>
{loading ? "查询中..." : "查询"}
</Button>
</Box>
</Box>
<Box className="flex-1 overflow-auto">
{schemes.length === 0 ? (
<Box className="flex h-full flex-col items-center justify-center text-center text-gray-400">
<Typography variant="body2"></Typography>
<Typography variant="caption" className="mt-1">
</Typography>
</Box>
) : (
<Box className="space-y-2 p-2">
<Typography variant="caption" className="px-2 text-gray-500">
{schemes.length}
</Typography>
{schemes.map((scheme) => {
const summary = scheme.scheme_detail?.result_summary;
const payload = scheme.scheme_detail?.result_payload;
const isBurst = payload?.summary?.burst_detected ?? summary?.burst_detected ?? false;
const anomalyDayCount =
payload?.summary?.anomaly_day_count ?? summary?.anomaly_day_count ?? 0;
const mostAnomalousDay =
payload?.summary?.most_anomalous_day ?? summary?.most_anomalous_day ?? "-";
const sensorCount = payload?.sensor_nodes?.length ?? scheme.scheme_detail?.sensor_nodes?.length ?? 0;
return (
<Card key={scheme.scheme_id} variant="outlined" className="transition-shadow hover:shadow-md">
<CardContent className="p-3 pb-2 last:pb-3">
<Box className="mb-2 flex items-start justify-between gap-2">
<Box className="min-w-0 flex-1">
<Box className="mb-1 flex items-center gap-2">
<Typography
variant="body2"
className="truncate font-medium"
title={scheme.scheme_name}
>
{scheme.scheme_name}
</Typography>
<Chip
size="small"
color={isBurst ? "error" : "success"}
variant="outlined"
label={isBurst ? "存在异常" : "正常"}
className="h-5"
/>
</Box>
<Typography variant="caption" className="block text-gray-500">
{dayjs(scheme.create_time).format("YYYY-MM-DD HH:mm")}
</Typography>
</Box>
<Box className="ml-2 flex gap-1">
<Tooltip title={expandedId === scheme.scheme_id ? "收起详情" : "查看详情"}>
<IconButton
size="small"
onClick={() =>
setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id)
}
color="primary"
className="p-1"
>
<InfoIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
<Box className="grid grid-cols-3 gap-2">
<Box className="rounded bg-gray-50 p-2">
<Typography variant="caption" className="text-gray-500">
</Typography>
<Typography variant="body2" className="font-semibold text-gray-900">
{anomalyDayCount}
</Typography>
</Box>
<Box className="rounded bg-gray-50 p-2">
<Typography variant="caption" className="text-gray-500">
</Typography>
<Typography variant="body2" className="font-semibold text-gray-900">
{isBurst
? typeof mostAnomalousDay === "number"
? `${mostAnomalousDay}`
: mostAnomalousDay
: "无"}
</Typography>
</Box>
<Box className="rounded bg-gray-50 p-2">
<Typography variant="caption" className="text-gray-500">
</Typography>
<Typography variant="body2" className="font-semibold text-gray-900">
{sensorCount}
</Typography>
</Box>
</Box>
<Collapse in={expandedId === scheme.scheme_id}>
<Box className="mt-2 border-t border-gray-200 pt-3">
<Box className="space-y-2 rounded-md bg-gray-50 px-3 py-2">
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600">
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{(() => {
const ds = payload?.data_source;
const os = payload?.observed_source ?? scheme.scheme_detail?.observed_source;
if (ds === "simulation") return "模拟数据";
if (ds === "monitoring") return "监测数据";
if (os === "simulation_scheme_timerange") return "模拟数据";
if (os === "backend_timerange") return "监测数据";
return os || "-";
})()}
</Typography>
</Box>
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600">
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{payload?.scada_window?.start
? `${dayjs(payload.scada_window.start).format("MM-DD HH:mm")} ~ ${dayjs(
payload.scada_window.end,
).format("MM-DD HH:mm")}`
: "-"}
</Typography>
</Box>
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600">
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{scheme.scheme_detail?.algorithm_params?.mu ?? payload?.algorithm_params?.mu ?? "-"}
{scheme.scheme_detail?.algorithm_params?.points_per_day ??
payload?.algorithm_params?.points_per_day ??
"-"}
</Typography>
</Box>
</Box>
<Box className="border-t border-gray-100 pt-2">
<Button
variant="contained"
fullWidth
size="small"
className="bg-blue-600 hover:bg-blue-700"
sx={{ textTransform: "none", fontWeight: 500 }}
onClick={() => handleViewSchemeResult(scheme.scheme_name)}
>
</Button>
</Box>
</Box>
</Collapse>
</CardContent>
</Card>
);
})}
</Box>
)}
</Box>
</Box>
);
};
export default SchemeQuery;