"use client"; import React, { useEffect, useMemo, useState } from "react"; import { Box, Button, FormControl, MenuItem, Select, TextField, Typography, } 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 } from "@config/config"; import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { useSessionRecoveryDraft } from "@/lib/sessionRecoveryDraft"; import { getApiErrorMessage } from "@/lib/apiError"; import { BurstDetectionResult } from "./types"; import { createSchemeName, isSchemeNameValid, normalizeSchemeName, SCHEME_NAME_MAX_LENGTH, SCHEME_NAME_PREFIXES, } from "@utils/schemeName"; interface Props { onResult: (result: BurstDetectionResult) => void; state?: BurstDetectionAnalysisParametersState; onStateChange?: (state: BurstDetectionAnalysisParametersState) => void; } export interface BurstDetectionAnalysisParametersState { schemeName: string; detectionMode: "latest" | "historical"; targetTime: Dayjs | null; samplingIntervalMinutes: number; samplingIntervalSource: "metadata" | "manual"; } interface ScadaInfoItem { device_type?: string; transmission_frequency?: string | number | null; } interface ScadaInfoResponse { data?: ScadaInfoItem[]; } const currentQuarterHour = () => { const now = dayjs().second(0).millisecond(0); return now.minute(Math.floor(now.minute() / 15) * 15); }; export const createBurstDetectionAnalysisParametersState = (): BurstDetectionAnalysisParametersState => ({ schemeName: createSchemeName(SCHEME_NAME_PREFIXES.burstDetection), detectionMode: "latest", targetTime: currentQuarterHour(), samplingIntervalMinutes: 15, samplingIntervalSource: "metadata", }); export const parseScadaFrequencyMinutes = ( value: string | number | null | undefined, ): number | null => { if (typeof value === "number") { return Number.isInteger(value) && value > 0 ? value : null; } if (!value) return null; const normalized = value.trim(); const dayMatch = normalized.match(/^(\d+)\s+days?,\s*(.+)$/i); const days = dayMatch ? Number(dayMatch[1]) : 0; const timePart = dayMatch ? dayMatch[2] : normalized; const parts = timePart.split(":").map(Number); if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) { return null; } const minutes = days * 1440 + parts[0] * 60 + parts[1] + parts[2] / 60; return Number.isInteger(minutes) && minutes > 0 ? minutes : null; }; export const resolvePressureSamplingInterval = (items: ScadaInfoItem[]) => { const counts = new Map(); items .filter((item) => item.device_type?.toLowerCase() === "pressure") .forEach((item) => { const minutes = parseScadaFrequencyMinutes(item.transmission_frequency); if (minutes && 1440 % minutes === 0) { counts.set(minutes, (counts.get(minutes) ?? 0) + 1); } }); return [...counts.entries()].sort( ([minutesA, countA], [minutesB, countB]) => countB - countA || minutesA - minutesB, )[0]?.[0] ?? 15; }; export const fetchPressureSamplingInterval = async () => { const response = await api.get( "/api/v1/scada-info/database-view", ); return resolvePressureSamplingInterval( Array.isArray(response.data.data) ? response.data.data : [], ); }; export const buildBurstDetectionRequest = ( parameters: BurstDetectionAnalysisParametersState, ) => ({ scheme_name: normalizeSchemeName(parameters.schemeName), sampling_interval_minutes: parameters.samplingIntervalMinutes, ...(parameters.detectionMode === "historical" && parameters.targetTime ? { target_time: parameters.targetTime.toISOString() } : {}), }); const AnalysisParameters: React.FC = ({ onResult, state, onStateChange, }) => { const { open } = useNotification(); const [parametersState, setParametersState, setFormField] = useControllableObjectState( state, onStateChange, createBurstDetectionAnalysisParametersState(), ); const { schemeName, detectionMode, targetTime, samplingIntervalMinutes, samplingIntervalSource, } = parametersState; const [running, setRunning] = useState(false); const [frequencyLoading, setFrequencyLoading] = useState(false); useSessionRecoveryDraft("burst-detection", parametersState, (draft) => setParametersState({ ...draft, targetTime: draft.targetTime ? dayjs(draft.targetTime) : null, }), ); useEffect(() => { if (samplingIntervalSource !== "metadata") return; let active = true; setFrequencyLoading(true); fetchPressureSamplingInterval() .then((interval) => { if (!active) return; setParametersState((previous) => previous.samplingIntervalSource === "metadata" ? { ...previous, samplingIntervalMinutes: interval } : previous, ); }) .catch(() => { // Keep the 15-minute fallback when SCADA metadata is unavailable. }) .finally(() => { if (active) setFrequencyLoading(false); }); return () => { active = false; }; }, [samplingIntervalSource, setParametersState]); const samplingIntervalValid = Number.isInteger(samplingIntervalMinutes) && samplingIntervalMinutes > 0 && 1440 % samplingIntervalMinutes === 0; const isValid = useMemo( () => isSchemeNameValid(schemeName) && samplingIntervalValid && (detectionMode === "latest" || Boolean(targetTime?.isValid())), [detectionMode, samplingIntervalValid, schemeName, targetTime], ); const handleRun = async () => { if (!isValid) { open?.({ type: "error", message: "参数不完整", description: "请输入方案名称,并检查历史目标时间。", }); return; } setRunning(true); open?.({ key: "burst-detection-analysis-progress", type: "progress", message: "正在执行爆管侦测", description: "正在读取目标时刻及前 14 天同刻基线。", undoableTimeout: 3, }); try { const response = await api.post( "/api/v1/burst-detections", buildBurstDetectionRequest(parametersState), ); onResult(response.data as BurstDetectionResult); open?.({ key: "burst-detection-analysis-success", type: "success", message: "爆管侦测完成", description: response.data.summary?.burst_detected ? "目标时刻存在异常信号,请优先复核相关测点。" : "目标时刻未发现爆管异常。", }); } catch (error) { open?.({ key: "burst-detection-analysis-error", type: "error", message: "侦测失败", description: getApiErrorMessage(error, "爆管侦测请求失败"), }); } finally { setRunning(false); } }; return ( 方案名称 setFormField("schemeName", event.target.value)} placeholder="请输入方案名称" error={schemeName.trim().length > SCHEME_NAME_MAX_LENGTH} helperText={ schemeName.trim().length > SCHEME_NAME_MAX_LENGTH ? `方案名称不能超过 ${SCHEME_NAME_MAX_LENGTH} 个字符` : undefined } inputProps={{ maxLength: SCHEME_NAME_MAX_LENGTH }} fullWidth size="small" /> 侦测方式 {detectionMode === "historical" ? ( 目标时刻 setFormField("targetTime", value)} maxDateTime={dayjs()} minutesStep={15} format="YYYY-MM-DD HH:mm" slotProps={{ textField: { size: "small", fullWidth: true } }} /> ) : null} { setParametersState((previous) => ({ ...previous, samplingIntervalMinutes: Number(event.target.value), samplingIntervalSource: "manual", })); }} size="small" fullWidth error={!samplingIntervalValid} inputProps={{ min: 1, max: 1440, step: 1 }} helperText={ samplingIntervalValid ? `${frequencyLoading ? "正在读取 SCADA 频率" : samplingIntervalSource === "metadata" ? "默认取自压力 SCADA 频率" : "已手动设置"},每天 ${1440 / samplingIntervalMinutes} 个采样点` : "请输入能整除 1440 分钟的正整数,例如 1、5、10、15、30 或 60。" } /> {detectionMode === "latest" ? ( 系统自动读取目标时刻及前 14 天同一时刻数据。每天使用截至该时刻的 24 小时压力序列提取扰动特征,仅判定目标时刻是否异常。 ) : null} 当前口径:{samplingIntervalMinutes || "-"} 分钟采样、 {samplingIntervalValid ? 1440 / samplingIntervalMinutes : "-"} 点/天、14 个参考日;缺失数据的测点不会插值,将从本次分析中排除。 ); }; export default AnalysisParameters;