"use client"; import React, { useCallback, useMemo, useState } from "react"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import RefreshIcon from "@mui/icons-material/Refresh"; import { Alert, 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 { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { FLOW_DISPLAY_UNIT, toM3s } from "@utils/units"; import { BurstLocationResult } from "./types"; import { getBurstLocationErrorNotice } from "./burstLocationError"; interface Props { onResult: (result: BurstLocationResult) => void; state?: BurstLocationAnalysisParametersState; onStateChange?: (state: BurstLocationAnalysisParametersState) => void; } export interface SchemeItem { scheme_id: number; scheme_name: string; scheme_type: string; create_time: string; scheme_start_time: string; scheme_detail?: { modify_total_duration: number; burst_ID?: string[] | string; }; } type DataSource = "monitoring" | "simulation"; export interface BurstLocationAnalysisParametersState { schemeName: string; dataSource: DataSource; schemes: SchemeItem[]; selectedSchemeId: number | ""; burstLeakage: number; enableFlow: boolean; burstStartTime: Dayjs | null; burstEndTime: Dayjs | null; minDpressure: number; basicPressure: number; advancedOpen: boolean; } export const createBurstLocationAnalysisParametersState = (): BurstLocationAnalysisParametersState => ({ schemeName: `Burst_Locate_${Date.now()}`, dataSource: "monitoring", schemes: [], selectedSchemeId: "", burstLeakage: 1440, enableFlow: false, burstStartTime: dayjs().subtract(20, "minute"), burstEndTime: dayjs().subtract(5, "minute"), minDpressure: 2, basicPressure: 10, advancedOpen: false, }); const AnalysisParameters: React.FC = ({ onResult, state, onStateChange, }) => { const { open } = useNotification(); const [parametersState, setParametersState, setFormField] = useControllableObjectState( state, onStateChange, createBurstLocationAnalysisParametersState(), ); const { schemeName, dataSource, schemes, selectedSchemeId, burstLeakage, enableFlow, burstStartTime, burstEndTime, minDpressure, basicPressure, advancedOpen, } = parametersState; const [schemeLoading, setSchemeLoading] = 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"); setParametersState((previous) => ({ ...previous, burstStartTime: start, burstEndTime: end, })); }, [setParametersState]); 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/schemes`, { params: { scheme_type: "burst_analysis" }, }); const burstSchemes = (response.data as SchemeItem[]).filter( (scheme) => scheme.scheme_type === "burst_analysis", ).sort( (a, b) => dayjs(b.create_time).valueOf() - dayjs(a.create_time).valueOf(), ); setFormField("schemes", burstSchemes); if (selectedSchemeId) { const matchedScheme = burstSchemes.find( (scheme) => scheme.scheme_id === selectedSchemeId, ); if (matchedScheme) { applySchemeTimeRange(matchedScheme); } else { setFormField("selectedSchemeId", ""); } } 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, setFormField], ); const handleDataSourceChange = (value: DataSource) => { setFormField("dataSource", value); if (value === "simulation") { void fetchSchemes(); } }; const handleSchemeSelect = (schemeId: number) => { setFormField("selectedSchemeId", schemeId); const scheme = schemes.find((item) => item.scheme_id === schemeId); if (scheme) { applySchemeTimeRange(scheme); } }; const isValid = useMemo(() => { if (!Number.isFinite(burstLeakage) || burstLeakage <= 0) return false; if (!burstStartTime || !burstEndTime) { return false; } if (dataSource === "simulation" && !selectedSchemeId) { return false; } return burstStartTime.isBefore(burstEndTime); }, [ burstLeakage, burstStartTime, burstEndTime, dataSource, selectedSchemeId, ]); const handleRun = async () => { if (!isValid || !burstStartTime || !burstEndTime) { open?.({ type: "error", message: "请完善参数并确认时间范围合法" }); return; } setRunning(true); open?.({ key: "burst-location-analysis-progress", type: "progress", message: "方案提交分析中", undoableTimeout: 3, }); try { const selectedScheme = dataSource === "simulation" ? schemes.find((item) => item.scheme_id === selectedSchemeId) : undefined; const response = await api.post( `${config.BACKEND_URL}/api/v1/burst-locations`, { data_source: dataSource, scheme_name: schemeName.trim() || undefined, burst_leakage: toM3s(burstLeakage, FLOW_DISPLAY_UNIT), min_dpressure: minDpressure, basic_pressure: basicPressure, scada_burst_start: burstStartTime.toISOString(), scada_burst_end: burstEndTime.toISOString(), use_scada_flow: enableFlow || undefined, simulation_scheme_name: selectedScheme?.scheme_name, simulation_scheme_type: selectedScheme?.scheme_type, }, ); const resultPayload = response.data as BurstLocationResult; const selectedBurstIds = normalizeBurstIds(selectedScheme?.scheme_detail?.burst_ID); onResult( selectedBurstIds.length > 0 ? { ...resultPayload, simulation_scheme: { ...resultPayload.simulation_scheme, name: resultPayload.simulation_scheme?.name ?? selectedScheme?.scheme_name, type: resultPayload.simulation_scheme?.type ?? selectedScheme?.scheme_type, burst_ids: resultPayload.simulation_scheme?.burst_ids?.length ? resultPayload.simulation_scheme.burst_ids : selectedBurstIds, }, } : resultPayload, ); open?.({ key: "burst-location-analysis-success", type: "success", message: "爆管定位成功", description: `定位到管段: ${(response.data as BurstLocationResult).located_pipe}`, }); } catch (error: any) { const notice = getBurstLocationErrorNotice(error); open?.({ key: "burst-location-analysis-error", type: "error", message: notice.message, description: notice.description, }); } finally { setRunning(false); } }; return ( 方案名称 setFormField("schemeName", e.target.value)} placeholder="请输入方案名称" fullWidth size="small" /> SCADA 数据来源 {isSimulationMode ? "模拟方案分析前置条件" : "数据选择规则"} {isSimulationMode ? "爆管数据取所选方案,正常数据取同一时间窗的实时模拟结果。所选时间窗必须已有实时模拟正常基线,否则无法分析;系统不会自动补数据。" : "当前为监测数据:爆管数据取所选监测时间窗,正常数据默认取前一天同一时段的监测数据。"} {isSimulationMode && ( 选择爆管分析方案 void fetchSchemes({ force: true, notify: true })} disabled={schemeLoading} aria-label="刷新爆管分析方案" sx={{ border: "1px solid", borderColor: "divider", borderRadius: 1, }} > {schemeLoading ? ( ) : ( )} )} 爆管开始时间 setFormField("burstStartTime", value)} maxDateTime={burstEndTime ?? undefined} disabled={isSimulationMode} format="YYYY-MM-DD HH:mm" slotProps={{ textField: { size: "small", fullWidth: true } }} /> 爆管结束时间 setFormField("burstEndTime", value)} minDateTime={burstStartTime ?? undefined} disabled={isSimulationMode} format="YYYY-MM-DD HH:mm" slotProps={{ textField: { size: "small", fullWidth: true } }} /> 爆管漏损流量 ({FLOW_DISPLAY_UNIT}) { const value = Number(e.target.value); setFormField("burstLeakage", Number.isNaN(value) ? 1440 : Math.max(0, value)); }} fullWidth inputProps={{ min: 0, step: 10 }} /> setFormField("advancedOpen", !advancedOpen)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") setFormField("advancedOpen", !advancedOpen); }} sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", px: 1.25, py: 0.75, cursor: "pointer", backgroundColor: "transparent", "&:hover": { backgroundColor: "action.hover" }, }} > 高级选项 流量校核 setFormField("minDpressure", Number(e.target.value))} /> setFormField("basicPressure", Number(e.target.value))} /> ); }; export default AnalysisParameters; const normalizeBurstIds = (value: string[] | string | undefined) => { if (!value) return []; const values = Array.isArray(value) ? value : [value]; return Array.from( new Set(values.map((item) => String(item).trim()).filter(Boolean)), ); };