diff --git a/src/components/olmap/BurstDetection/AnalysisParameters.tsx b/src/components/olmap/BurstDetection/AnalysisParameters.tsx index 2b0efba..864b3e0 100644 --- a/src/components/olmap/BurstDetection/AnalysisParameters.tsx +++ b/src/components/olmap/BurstDetection/AnalysisParameters.tsx @@ -24,13 +24,16 @@ 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 { BurstDetectionResult } from "./types"; interface Props { onResult: (result: BurstDetectionResult) => void; + state?: BurstDetectionAnalysisParametersState; + onStateChange?: (state: BurstDetectionAnalysisParametersState) => void; } -interface SchemeItem { +export interface SchemeItem { scheme_id: number; scheme_name: string; scheme_type: string; @@ -41,20 +44,60 @@ interface SchemeItem { }; } -const AnalysisParameters: React.FC = ({ onResult }) => { +export interface BurstDetectionAnalysisParametersState { + schemeName: string; + dataSource: "monitoring" | "simulation"; + schemes: SchemeItem[]; + selectedSchemeId: number | ""; + scadaStart: Dayjs | null; + scadaEnd: Dayjs | null; + mu: number; + pointsPerDay: number; + nEstimators: number; + contaminationInput: string; + advancedOpen: boolean; +} + +export const createBurstDetectionAnalysisParametersState = + (): BurstDetectionAnalysisParametersState => ({ + schemeName: `Burst_Detection_${Date.now()}`, + dataSource: "monitoring", + schemes: [], + selectedSchemeId: "", + scadaStart: dayjs().subtract(3, "day"), + scadaEnd: dayjs(), + mu: 100, + pointsPerDay: 96, + nEstimators: 50, + contaminationInput: "auto", + advancedOpen: false, + }); + +const AnalysisParameters: React.FC = ({ + onResult, + state, + onStateChange, +}) => { 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 [parametersState, setParametersState, setFormField] = useControllableObjectState( + state, + onStateChange, + createBurstDetectionAnalysisParametersState(), + ); + const { + schemeName, + dataSource, + schemes, + selectedSchemeId, + scadaStart, + scadaEnd, + mu, + pointsPerDay, + nEstimators, + contaminationInput, + advancedOpen, + } = parametersState; 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"; @@ -63,9 +106,12 @@ const AnalysisParameters: React.FC = ({ onResult }) => { const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600; const end = start.add(durationSeconds, "second"); - setScadaStart(start); - setScadaEnd(end); - }, []); + setParametersState((previous) => ({ + ...previous, + scadaStart: start, + scadaEnd: end, + })); + }, [setParametersState]); const fetchSchemes = useCallback( async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => { @@ -80,7 +126,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { (scheme) => scheme.scheme_type === "burst_analysis", ); - setSchemes(burstSchemes); + setFormField("schemes", burstSchemes); if (selectedSchemeId) { const matchedScheme = burstSchemes.find( @@ -89,7 +135,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { if (matchedScheme) { applySchemeTimeRange(matchedScheme); } else { - setSelectedSchemeId(""); + setFormField("selectedSchemeId", ""); } } @@ -111,18 +157,18 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setSchemeLoading(false); } }, - [applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId], + [applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId, setFormField], ); const handleDataSourceChange = (value: "monitoring" | "simulation") => { - setDataSource(value); + setFormField("dataSource", value); if (value === "simulation") { void fetchSchemes(); } }; const handleSchemeSelect = (schemeId: number) => { - setSelectedSchemeId(schemeId); + setFormField("selectedSchemeId", schemeId); const scheme = schemes.find((item) => item.scheme_id === schemeId); if (scheme) { applySchemeTimeRange(scheme); @@ -239,7 +285,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setSchemeName(event.target.value)} + onChange={(event) => setFormField("schemeName", event.target.value)} placeholder="请输入方案名称" fullWidth size="small" @@ -318,7 +364,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setFormField("scadaStart", value)} maxDateTime={scadaEnd ? scadaEnd.subtract(2, "day") : undefined} disabled={isSimulationMode} format="YYYY-MM-DD HH:mm" @@ -331,7 +377,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setFormField("scadaEnd", value)} minDateTime={scadaStart ? scadaStart.add(2, "day") : undefined} disabled={isSimulationMode} format="YYYY-MM-DD HH:mm" @@ -356,10 +402,10 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setAdvancedOpen((prev) => !prev)} + onClick={() => setFormField("advancedOpen", !advancedOpen)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { - setAdvancedOpen((prev) => !prev); + setFormField("advancedOpen", !advancedOpen); } }} sx={{ @@ -397,7 +443,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { type="number" label="频域截断系数" value={mu} - onChange={(event) => setMu(Number(event.target.value))} + onChange={(event) => setFormField("mu", Number(event.target.value))} size="small" fullWidth inputProps={{ min: 1 }} @@ -406,7 +452,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { type="number" label="每日采样点数" value={pointsPerDay} - onChange={(event) => setPointsPerDay(Number(event.target.value))} + onChange={(event) => setFormField("pointsPerDay", Number(event.target.value))} size="small" fullWidth inputProps={{ min: 1 }} @@ -415,7 +461,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { type="number" label="孤立森林树数量" value={nEstimators} - onChange={(event) => setNEstimators(Number(event.target.value))} + onChange={(event) => setFormField("nEstimators", Number(event.target.value))} size="small" fullWidth inputProps={{ min: 1 }} @@ -423,7 +469,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setContaminationInput(event.target.value)} + onChange={(event) => setFormField("contaminationInput", event.target.value)} size="small" fullWidth helperText="填写 auto 或 0~0.5 之间的小数。" @@ -442,13 +488,16 @@ const AnalysisParameters: React.FC = ({ onResult }) => { disabled={running} sx={{ textTransform: "none", fontWeight: 500 }} onClick={() => { - setSchemeName(`Burst_Detection_${Date.now()}`); - setScadaStart(dayjs().subtract(3, "day")); - setScadaEnd(dayjs()); - setMu(100); - setPointsPerDay(96); - setNEstimators(50); - setContaminationInput("auto"); + setParametersState((previous) => ({ + ...previous, + schemeName: `Burst_Detection_${Date.now()}`, + scadaStart: dayjs().subtract(3, "day"), + scadaEnd: dayjs(), + mu: 100, + pointsPerDay: 96, + nEstimators: 50, + contaminationInput: "auto", + })); }} > 重置 diff --git a/src/components/olmap/BurstDetection/BurstDetectionPanel.tsx b/src/components/olmap/BurstDetection/BurstDetectionPanel.tsx index 54a6f21..d4ab172 100644 --- a/src/components/olmap/BurstDetection/BurstDetectionPanel.tsx +++ b/src/components/olmap/BurstDetection/BurstDetectionPanel.tsx @@ -9,9 +9,18 @@ import { FormatListBulleted, Search as SearchIcon, } from "@mui/icons-material"; -import AnalysisParameters from "./AnalysisParameters"; -import DetectionResults from "./DetectionResults"; -import SchemeQuery from "./SchemeQuery"; +import AnalysisParameters, { + createBurstDetectionAnalysisParametersState, + type BurstDetectionAnalysisParametersState, +} from "./AnalysisParameters"; +import DetectionResults, { + createBurstDetectionResultsState, + type BurstDetectionResultsState, +} from "./DetectionResults"; +import SchemeQuery, { + createBurstDetectionSchemeQueryState, + type BurstDetectionSchemeQueryState, +} from "./SchemeQuery"; import { BurstDetectionResult, BurstDetectionSchemeRecord } from "./types"; const TabPanel = ({ @@ -33,6 +42,18 @@ const BurstDetectionPanel: React.FC = () => { const [tab, setTab] = useState(0); const [result, setResult] = useState(null); const [schemes, setSchemes] = useState([]); + const [analysisState, setAnalysisState] = + useState( + createBurstDetectionAnalysisParametersState, + ); + const [queryState, setQueryState] = + useState( + createBurstDetectionSchemeQueryState, + ); + const [resultsState, setResultsState] = + useState( + createBurstDetectionResultsState, + ); const drawerWidth = 450; const panelTitle = "爆管侦测"; @@ -137,13 +158,27 @@ const BurstDetectionPanel: React.FC = () => { - + - + - + diff --git a/src/components/olmap/BurstDetection/DetectionResults.tsx b/src/components/olmap/BurstDetection/DetectionResults.tsx index d216eee..8102aaa 100644 --- a/src/components/olmap/BurstDetection/DetectionResults.tsx +++ b/src/components/olmap/BurstDetection/DetectionResults.tsx @@ -24,8 +24,19 @@ import { Circle, Fill, Stroke, Style } from "ol/style"; import { bbox, featureCollection } from "@turf/turf"; import { BurstDetectionResult, BurstDetectionRow } from "./types"; +export interface BurstDetectionResultsState { + selectedDay: number | null; +} + +export const createBurstDetectionResultsState = + (): BurstDetectionResultsState => ({ + selectedDay: null, + }); + interface Props { result: BurstDetectionResult | null; + state?: BurstDetectionResultsState; + onStateChange?: (state: BurstDetectionResultsState) => void; } interface MetricCardProps { @@ -106,11 +117,25 @@ const getScoreLevel = (score: number) => { const formatDateTime = (value?: string) => (value ? dayjs(value).format("YYYY-MM-DD HH:mm") : "-"); -const DetectionResults: React.FC = ({ result }) => { +const DetectionResults: React.FC = ({ + result, + state, + onStateChange, +}) => { const map = useMap(); const highlightLayerRef = useRef | null>(null); const [highlightFeatures, setHighlightFeatures] = useState([]); - const [selectedDay, setSelectedDay] = useState(null); + const [internalResultsState, setInternalResultsState] = + useState(createBurstDetectionResultsState); + const resultsState = state ?? internalResultsState; + const selectedDay = resultsState.selectedDay; + const setSelectedDay = (value: number | null) => { + const nextState = { ...resultsState, selectedDay: value }; + if (state === undefined) { + setInternalResultsState(nextState); + } + onStateChange?.(nextState); + }; useEffect(() => { if (!map) return; diff --git a/src/components/olmap/BurstDetection/SchemeQuery.tsx b/src/components/olmap/BurstDetection/SchemeQuery.tsx index 3d292e2..c773b0e 100644 --- a/src/components/olmap/BurstDetection/SchemeQuery.tsx +++ b/src/components/olmap/BurstDetection/SchemeQuery.tsx @@ -23,6 +23,7 @@ import "dayjs/locale/zh-cn"; import { useNotification } from "@refinedev/core"; import { api } from "@/lib/api"; import { NETWORK_NAME } from "@config/config"; +import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { BurstDetectionResult, BurstDetectionSchemeDetail, @@ -33,15 +34,39 @@ interface Props { onViewResult: (result: BurstDetectionResult) => void; schemes?: BurstDetectionSchemeRecord[]; onSchemesChange?: (schemes: BurstDetectionSchemeRecord[]) => void; + state?: BurstDetectionSchemeQueryState; + onStateChange?: (state: BurstDetectionSchemeQueryState) => void; } -const SchemeQuery: React.FC = ({ onViewResult, schemes: externalSchemes, onSchemesChange }) => { +export interface BurstDetectionSchemeQueryState { + queryAll: boolean; + queryDate: Dayjs | null; + expandedId: number | null; +} + +export const createBurstDetectionSchemeQueryState = + (): BurstDetectionSchemeQueryState => ({ + queryAll: true, + queryDate: dayjs(), + expandedId: null, + }); + +const SchemeQuery: React.FC = ({ + onViewResult, + schemes: externalSchemes, + onSchemesChange, + state, + onStateChange, +}) => { const { open } = useNotification(); - const [queryAll, setQueryAll] = useState(true); - const [queryDate, setQueryDate] = useState(dayjs()); + const [queryState, , setQueryField] = useControllableObjectState( + state, + onStateChange, + createBurstDetectionSchemeQueryState(), + ); + const { queryAll, queryDate, expandedId } = queryState; const [internalSchemes, setInternalSchemes] = useState([]); const [loading, setLoading] = useState(false); - const [expandedId, setExpandedId] = useState(null); const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const setSchemes = onSchemesChange || setInternalSchemes; @@ -159,7 +184,7 @@ const SchemeQuery: React.FC = ({ onViewResult, schemes: externalSchemes, setQueryAll(event.target.checked)} + onChange={(event) => setQueryField("queryAll", event.target.checked)} /> } label={查询全部} @@ -168,7 +193,7 @@ const SchemeQuery: React.FC = ({ onViewResult, schemes: externalSchemes, setQueryField("queryDate", value)} disabled={queryAll} format="YYYY-MM-DD" slotProps={{ textField: { size: "small", sx: { width: 180 } } }} @@ -241,7 +266,10 @@ const SchemeQuery: React.FC = ({ onViewResult, schemes: externalSchemes, - setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id) + setQueryField( + "expandedId", + expandedId === scheme.scheme_id ? null : scheme.scheme_id, + ) } color="primary" className="p-1" diff --git a/src/components/olmap/BurstLocation/AnalysisParameters.tsx b/src/components/olmap/BurstLocation/AnalysisParameters.tsx index 5d571f0..4a872c2 100644 --- a/src/components/olmap/BurstLocation/AnalysisParameters.tsx +++ b/src/components/olmap/BurstLocation/AnalysisParameters.tsx @@ -25,14 +25,17 @@ 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"; interface Props { onResult: (result: BurstLocationResult) => void; + state?: BurstLocationAnalysisParametersState; + onStateChange?: (state: BurstLocationAnalysisParametersState) => void; } -interface SchemeItem { +export interface SchemeItem { scheme_id: number; scheme_name: string; scheme_type: string; @@ -45,24 +48,60 @@ interface SchemeItem { type DataSource = "monitoring" | "simulation"; -const AnalysisParameters: React.FC = ({ onResult }) => { +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 [schemeName, setSchemeName] = useState(`Burst_Locate_${Date.now()}`); - const [dataSource, setDataSource] = useState("monitoring"); - const [schemes, setSchemes] = useState([]); - const [selectedSchemeId, setSelectedSchemeId] = useState(""); + 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 [burstLeakage, setBurstLeakage] = useState(1440); - const [enableFlow, setEnableFlow] = useState(false); - const [burstStartTime, setBurstStartTime] = useState( - dayjs().subtract(20, "minute"), - ); - const [burstEndTime, setBurstEndTime] = useState( - dayjs().subtract(5, "minute"), - ); - const [minDpressure, setMinDpressure] = useState(2); - const [basicPressure, setBasicPressure] = useState(10); - const [advancedOpen, setAdvancedOpen] = useState(false); const [running, setRunning] = useState(false); const isSimulationMode = dataSource === "simulation"; @@ -71,9 +110,12 @@ const AnalysisParameters: React.FC = ({ onResult }) => { const durationSeconds = scheme.scheme_detail?.modify_total_duration ?? 3600; const end = start.add(durationSeconds, "second"); - setBurstStartTime(start); - setBurstEndTime(end); - }, []); + setParametersState((previous) => ({ + ...previous, + burstStartTime: start, + burstEndTime: end, + })); + }, [setParametersState]); const fetchSchemes = useCallback( async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => { @@ -88,7 +130,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { (scheme) => scheme.scheme_type === "burst_analysis", ); - setSchemes(burstSchemes); + setFormField("schemes", burstSchemes); if (selectedSchemeId) { const matchedScheme = burstSchemes.find( @@ -97,7 +139,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { if (matchedScheme) { applySchemeTimeRange(matchedScheme); } else { - setSelectedSchemeId(""); + setFormField("selectedSchemeId", ""); } } @@ -119,18 +161,18 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setSchemeLoading(false); } }, - [applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId], + [applySchemeTimeRange, open, schemeLoading, schemes.length, selectedSchemeId, setFormField], ); const handleDataSourceChange = (value: DataSource) => { - setDataSource(value); + setFormField("dataSource", value); if (value === "simulation") { void fetchSchemes(); } }; const handleSchemeSelect = (schemeId: number) => { - setSelectedSchemeId(schemeId); + setFormField("selectedSchemeId", schemeId); const scheme = schemes.find((item) => item.scheme_id === schemeId); if (scheme) { applySchemeTimeRange(scheme); @@ -220,7 +262,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setSchemeName(e.target.value)} + onChange={(e) => setFormField("schemeName", e.target.value)} placeholder="请输入方案名称" fullWidth size="small" @@ -321,7 +363,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setFormField("burstStartTime", value)} maxDateTime={burstEndTime ?? undefined} disabled={isSimulationMode} format="YYYY-MM-DD HH:mm" @@ -334,7 +376,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setFormField("burstEndTime", value)} minDateTime={burstStartTime ?? undefined} disabled={isSimulationMode} format="YYYY-MM-DD HH:mm" @@ -354,7 +396,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => { value={burstLeakage} onChange={(e) => { const value = Number(e.target.value); - setBurstLeakage(Number.isNaN(value) ? 1440 : Math.max(0, value)); + setFormField("burstLeakage", Number.isNaN(value) ? 1440 : Math.max(0, value)); }} fullWidth inputProps={{ min: 0, step: 10 }} @@ -370,9 +412,9 @@ const AnalysisParameters: React.FC = ({ onResult }) => { setAdvancedOpen((prev) => !prev)} + onClick={() => setFormField("advancedOpen", !advancedOpen)} onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") setAdvancedOpen((prev) => !prev); + if (e.key === "Enter" || e.key === " ") setFormField("advancedOpen", !advancedOpen); }} sx={{ display: "flex", @@ -412,7 +454,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => {