"use client"; import React, { useMemo, useState, useCallback } from "react"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import RefreshIcon from "@mui/icons-material/Refresh"; import { Box, Button, CircularProgress, Collapse, FormControl, MenuItem, Select, TextField, Typography, IconButton, } from "@mui/material"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales"; import { useNotification } from "@refinedev/core"; import dayjs, { Dayjs } from "dayjs"; import "dayjs/locale/zh-cn"; import { api } from "@/lib/api"; import { NETWORK_NAME, config } from "@config/config"; import { BurstDetectionResult } from "./types"; interface Props { onResult: (result: BurstDetectionResult) => void; } interface SchemeItem { scheme_id: number; scheme_name: string; scheme_type: string; create_time: string; scheme_start_time: string; scheme_detail?: { modify_total_duration: number; }; } const AnalysisParameters: React.FC = ({ onResult }) => { const { open } = useNotification(); const [schemeName, setSchemeName] = useState(`Burst_Detection_${Date.now()}`); const [dataSource, setDataSource] = useState<"monitoring" | "simulation">("monitoring"); const [schemes, setSchemes] = useState([]); const [selectedSchemeId, setSelectedSchemeId] = useState(""); const [schemeLoading, setSchemeLoading] = useState(false); const [scadaStart, setScadaStart] = useState(dayjs().subtract(3, "day")); const [scadaEnd, setScadaEnd] = useState(dayjs()); const [mu, setMu] = useState(100); const [pointsPerDay, setPointsPerDay] = useState(96); const [nEstimators, setNEstimators] = useState(50); const [contaminationInput, setContaminationInput] = useState("auto"); const [advancedOpen, setAdvancedOpen] = useState(false); const [running, setRunning] = useState(false); const isSimulationMode = dataSource === "simulation"; const applySchemeTimeRange = useCallback((scheme: SchemeItem) => { const start = dayjs(scheme.scheme_start_time); const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600; const end = start.add(durationSeconds, "second"); setScadaStart(start); setScadaEnd(end); }, []); const fetchSchemes = useCallback( async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => { if (schemeLoading || (!force && schemes.length > 0)) return; setSchemeLoading(true); try { const response = await api.get(`${config.BACKEND_URL}/api/v1/getallschemes/`, { params: { network: NETWORK_NAME }, }); const burstSchemes = (response.data as SchemeItem[]).filter( (scheme) => scheme.scheme_type === "burst_analysis", ); setSchemes(burstSchemes); if (selectedSchemeId) { const matchedScheme = burstSchemes.find( (scheme) => scheme.scheme_id === selectedSchemeId, ); if (matchedScheme) { applySchemeTimeRange(matchedScheme); } else { setSelectedSchemeId(""); } } if (notify) { open?.({ type: "success", message: "方案列表已刷新", description: `当前可选爆管分析方案 ${burstSchemes.length} 个`, }); } } catch (error: any) { open?.({ type: "error", message: "刷新方案失败", description: error?.response?.data?.detail ?? error?.message ?? "无法获取爆管分析方案列表", }); } finally { setSchemeLoading(false); } }, [applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId], ); const handleDataSourceChange = (value: "monitoring" | "simulation") => { setDataSource(value); if (value === "simulation") { void fetchSchemes(); } }; const handleSchemeSelect = (schemeId: number) => { setSelectedSchemeId(schemeId); const scheme = schemes.find((item) => item.scheme_id === schemeId); if (scheme) { applySchemeTimeRange(scheme); } }; const timeWindowValid = useMemo(() => { if (!scadaStart || !scadaEnd) return false; return scadaEnd.diff(scadaStart, "day", true) >= 2; }, [scadaEnd, scadaStart]); const contaminationValue = useMemo(() => { const normalized = contaminationInput.trim().toLowerCase(); if (!normalized || normalized === "auto") { return "auto" as const; } const parsed = Number(normalized); if (!Number.isFinite(parsed) || parsed <= 0 || parsed >= 0.5) { return null; } return parsed; }, [contaminationInput]); const isValid = Boolean(scadaStart && scadaEnd) && timeWindowValid && Number.isFinite(mu) && mu > 0 && Number.isFinite(pointsPerDay) && pointsPerDay > 0 && Number.isFinite(nEstimators) && nEstimators > 0 && contaminationValue !== null && (dataSource !== "simulation" || Boolean(selectedSchemeId)); const handleRun = async () => { if (!isValid || !scadaStart || !scadaEnd || contaminationValue === null) { open?.({ type: "error", message: "参数不完整", description: "请检查时间范围(至少2天)和高级参数是否填写正确。", }); return; } setRunning(true); open?.({ key: "burst-detection-analysis-progress", type: "progress", message: "正在执行爆管侦测", description: "正在读取数据并计算异常分数。", undoableTimeout: 3, }); try { const selectedScheme = dataSource === "simulation" ? schemes.find((item) => item.scheme_id === selectedSchemeId) : undefined; const response = await api.post("/api/v1/burst-detection/detect/", { network: NETWORK_NAME, data_source: dataSource, scheme_name: schemeName.trim() || undefined, scada_start: scadaStart.toISOString(), scada_end: scadaEnd.toISOString(), mu, points_per_day: pointsPerDay, iforest_params: { n_estimators: nEstimators, contamination: contaminationValue, }, simulation_scheme_name: selectedScheme?.scheme_name, simulation_scheme_type: selectedScheme?.scheme_type, }); onResult({ ...(response.data as BurstDetectionResult), scheme_name: schemeName.trim() || (response.data as BurstDetectionResult).scheme_name, algorithm_params: { mu, points_per_day: pointsPerDay, iforest_params: { n_estimators: nEstimators, contamination: contaminationValue, }, }, }); open?.({ key: "burst-detection-analysis-success", type: "success", message: "爆管侦测完成", description: `共识别 ${response.data.summary?.anomaly_day_count ?? 0} 个异常日。`, }); } catch (error: any) { open?.({ key: "burst-detection-analysis-error", type: "error", message: "侦测失败", description: error?.response?.data?.detail ?? error?.message ?? "请求失败", }); } finally { setRunning(false); } }; return ( 方案名称 setSchemeName(event.target.value)} placeholder="请输入方案名称" fullWidth size="small" /> 数据来源 {isSimulationMode && ( 选择爆管分析方案 void fetchSchemes({ force: true, notify: true })} disabled={schemeLoading} aria-label="刷新爆管分析方案" sx={{ border: "1px solid", borderColor: "divider", borderRadius: 1, }} > {schemeLoading ? ( ) : ( )} )} 侦测开始时间 侦测结束时间 当前页面为展示版:手动触发一次侦测,展示异常日、最新测点排名和结果表格,不做定时轮询。 setAdvancedOpen((prev) => !prev)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { setAdvancedOpen((prev) => !prev); } }} sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", px: 1.25, py: 0.75, cursor: "pointer", backgroundColor: "transparent", "&:hover": { backgroundColor: "action.hover" }, }} > 高级参数 setMu(Number(event.target.value))} size="small" fullWidth inputProps={{ min: 1 }} /> setPointsPerDay(Number(event.target.value))} size="small" fullWidth inputProps={{ min: 1 }} /> setNEstimators(Number(event.target.value))} size="small" fullWidth inputProps={{ min: 1 }} /> setContaminationInput(event.target.value)} size="small" fullWidth helperText="填写 auto 或 0~0.5 之间的小数。" error={contaminationValue === null} /> ); }; export default AnalysisParameters;