"use client"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Button, Card, CardContent, Chip, Collapse, FormControlLabel, Checkbox, IconButton, Tooltip, Typography, Link, } from "@mui/material"; import { Info as InfoIcon, LocationOn as LocationOnIcon, } 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/locale/zh-cn"; import dayjs, { Dayjs } from "dayjs"; import { useNotification } from "@refinedev/core"; import { getAnalysisScheme, listAnalysisSchemes } from "@/lib/analysisRuns"; import { NETWORK_NAME } from "@config/config"; import { useControllableObjectState } from "@components/olmap/core/useControllableState"; import { useMap } from "@components/olmap/core/MapComponent"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { GeoJSON } from "ol/format"; import Feature from "ol/Feature"; import VectorLayer from "ol/layer/Vector"; import VectorSource from "ol/source/Vector"; import { Stroke, Style, Circle, Fill } from "ol/style"; import { bbox, featureCollection } from "@turf/turf"; import { BurstLocationResult, BurstLocationSchemeDetail, BurstSchemeRecord, } from "./types"; import { FLOW_DISPLAY_UNIT, toM3h } from "@utils/units"; import { useSchemeCreatorName } from "@components/olmap/core/useSchemeCreatorName"; import { SchemeQueryEmptyState } from "@components/olmap/common/PanelEmptyState"; interface Props { onViewResult: (result: BurstLocationResult, runId: string) => void; schemes?: BurstSchemeRecord[]; onSchemesChange?: (schemes: BurstSchemeRecord[]) => void; state?: BurstLocationSchemeQueryState; onStateChange?: (state: BurstLocationSchemeQueryState) => void; } export interface BurstLocationSchemeQueryState { queryAll: boolean; queryDate: Dayjs | null; expandedId: string | null; simulationBurstIdsByName: Record; hasQueried: boolean; } export const createBurstLocationSchemeQueryState = (): BurstLocationSchemeQueryState => ({ queryAll: true, queryDate: dayjs(), expandedId: null, simulationBurstIdsByName: {}, hasQueried: false, }); const SchemeQuery: React.FC = ({ onViewResult, schemes: externalSchemes, onSchemesChange, state, onStateChange, }) => { const { open } = useNotification(); const map = useMap(); const highlightLayerRef = useRef | null>(null); const [highlightFeatures, setHighlightFeatures] = useState([]); const [queryState, , setQueryField] = useControllableObjectState( state, onStateChange, createBurstLocationSchemeQueryState(), ); const { queryAll, queryDate, expandedId, hasQueried } = queryState; const simulationBurstIdsByName = queryState.simulationBurstIdsByName ?? {}; const [internalSchemes, setInternalSchemes] = useState([]); const [loading, setLoading] = useState(false); const creatorName = useSchemeCreatorName(); const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const setSchemes = onSchemesChange || setInternalSchemes; const sortedSchemes = useMemo( () => schemes .slice() .sort( (a, b) => dayjs(b.create_time).valueOf() - dayjs(a.create_time).valueOf(), ), [schemes], ); useEffect(() => { if (!map) return; const layer = new VectorLayer({ source: new VectorSource(), style: new Style({ stroke: new Stroke({ color: "#a855f7", width: 6, }), image: new Circle({ radius: 8, fill: new Fill({ color: "#a855f7" }), stroke: new Stroke({ color: "#fff", width: 2 }), }), }), properties: { name: "爆管定位模拟管段高亮", value: "burst_location_simulation_pipe_highlight", queryable: false, }, }); map.addLayer(layer); highlightLayerRef.current = layer; return () => { highlightLayerRef.current = null; map.removeLayer(layer); }; }, [map]); useEffect(() => { const source = highlightLayerRef.current?.getSource(); if (!source) return; source.clear(); highlightFeatures.forEach((feature) => source.addFeature(feature)); }, [highlightFeatures]); const locatePipes = async (pipeIds: string[]) => { const uniquePipeIds = Array.from(new Set(pipeIds.filter(Boolean))); if (!uniquePipeIds.length || !map) return; try { const features = await queryFeaturesByIds(uniquePipeIds, "pipes"); if (features.length === 0) return; setHighlightFeatures(features); const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => geojsonFormat.writeFeatureObject(feature), ); // @ts-ignore turf typing with ol geojson objects const extent = bbox(featureCollection(geojsonFeatures)); map.getView().fit(extent, { maxZoom: 19, duration: 1000, padding: [100, 100, 100, 100], }); } catch (error) { console.error("Locate failed", error); } }; const getSimulationBurstIds = (payload?: BurstLocationResult) => { const directIds = payload?.simulation_scheme?.burst_ids ?? []; if (directIds.length > 0) return directIds; const simulationSchemeName = payload?.simulation_scheme?.name; return simulationSchemeName ? simulationBurstIdsByName[simulationSchemeName] ?? [] : []; }; const enrichResultWithSimulationBurstIds = (payload: BurstLocationResult) => { const simulationBurstIds = getSimulationBurstIds(payload); if (simulationBurstIds.length === 0) return payload; return { ...payload, simulation_scheme: { ...payload.simulation_scheme, burst_ids: simulationBurstIds, }, }; }; const buildDisplayResult = ( scheme: Pick, detail?: BurstLocationSchemeDetail, ): BurstLocationResult | null => { const payload = detail?.result_payload; const locatedPipe = payload?.located_pipe ?? detail?.result_summary?.located_pipe; if (!locatedPipe) return null; return { located_pipe: locatedPipe, burst_leakage: payload?.burst_leakage ?? detail?.algorithm_params?.burst_leakage ?? 0, elapsed_seconds: payload?.elapsed_seconds ?? 0, min_dpressure: payload?.min_dpressure ?? detail?.algorithm_params?.min_dpressure, basic_pressure: payload?.basic_pressure ?? detail?.algorithm_params?.basic_pressure, simulation_times: payload?.simulation_times ?? detail?.result_summary?.simulation_times ?? 0, top_candidates: payload?.top_candidates ?? [], similarity_mode: payload?.similarity_mode ?? detail?.result_summary?.similarity_mode ?? "-", scheme_name: payload?.scheme_name ?? scheme.scheme_name, username: payload?.username ?? scheme.username, network: payload?.network ?? detail?.network, data_source: payload?.data_source, observed_source: payload?.observed_source ?? detail?.observed_source, pressure_scada_ids: payload?.pressure_scada_ids ?? detail?.pressure_scada_ids, flow_scada_ids: payload?.flow_scada_ids ?? detail?.flow_scada_ids, create_time: payload?.create_time ?? scheme.create_time, scada_window: payload?.scada_window ?? detail?.scada_window, pressure_samples: payload?.pressure_samples, flow_samples: payload?.flow_samples, simulation_scheme: payload?.simulation_scheme, }; }; const handleQuery = async () => { setLoading(true); try { const [nextSchemes, simulationSchemes] = await Promise.all([ listAnalysisSchemes({ runType: "burst_location", queryDate: !queryAll && queryDate ? queryDate.format("YYYY-MM-DD") : undefined, }), listAnalysisSchemes({ runType: "burst_analysis" }), ]); const nextSimulationBurstIdsByName = Object.fromEntries( (simulationSchemes as BurstSimulationSchemeItem[]) .map((scheme) => [ scheme.scheme_name, normalizeBurstIds(scheme.scheme_detail?.burst_ID), ]), ); setQueryField("simulationBurstIdsByName", nextSimulationBurstIdsByName); setSchemes( nextSchemes.map((scheme) => enrichSchemeWithSimulationBurstIds( scheme as BurstSchemeRecord, nextSimulationBurstIdsByName, ), ), ); setQueryField("hasQueried", true); open?.({ type: "success", message: "查询成功", description: `共找到 ${nextSchemes.length} 条记录`, }); } catch (error: any) { console.error(error); open?.({ type: "error", message: "查询失败", description: error?.response?.data?.detail ?? "无法获取方案列表", }); } finally { setLoading(false); } }; const handleViewSchemeResult = async (runId: string) => { try { const schemeRecord = (await getAnalysisScheme(runId)) as BurstSchemeRecord & { result_payload?: BurstLocationResult; }; 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(enrichResultWithSimulationBurstIds(normalizedResult), runId); open?.({ type: "success", message: "方案加载成功", description: `已加载方案: ${schemeRecord.scheme_name}`, }); } catch (error: any) { open?.({ type: "error", message: "查看详情失败", description: error?.response?.data?.detail ?? "无法获取方案详情", }); } }; return ( { setQueryField("queryAll", e.target.checked); setQueryField("hasQueried", false); }} /> } label={查询全部} className="m-0" /> { setQueryField("queryDate", value); setQueryField("hasQueried", false); }} disabled={queryAll} format="YYYY-MM-DD" slotProps={{ textField: { size: "small", sx: { width: 200 } } }} /> {sortedSchemes.length === 0 ? ( ) : ( 共 {sortedSchemes.length} 条记录 {sortedSchemes.map((scheme) => { const summary = scheme.scheme_detail?.result_summary; const payload = scheme.scheme_detail?.result_payload; const locatedPipe = payload?.located_pipe ?? summary?.located_pipe ?? "-"; const simulationBurstIds = getSimulationBurstIds(payload); const leakage = payload?.burst_leakage ?? scheme.scheme_detail?.algorithm_params?.burst_leakage; return ( {scheme.scheme_name} {payload?.data_source === "simulation" && payload?.simulation_scheme?.name ? ( 方案: {payload.simulation_scheme.name} ) : null} ID: {scheme.scheme_id} · 日期:{" "} {dayjs(scheme.create_time).format("MM-DD HH:mm")} setQueryField( "expandedId", expandedId === scheme.scheme_id ? null : scheme.scheme_id, ) } color="primary" className="p-1" > 定位管段: {locatedPipe} {simulationBurstIds.length > 0 ? ( 模拟管段: {simulationBurstIds.map((pipeId) => ( locatePipes([pipeId])} title={pipeId} sx={{ maxWidth: 132, color: "#7c3aed", fontSize: "0.75rem", fontWeight: 700, lineHeight: "22px", cursor: "pointer", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textDecoration: "underline", textUnderlineOffset: "2px", "&:hover": { color: "#5b21b6", }, }} > {pipeId} ))} locatePipes(simulationBurstIds)} className="h-6 w-6 p-0" > ) : null} 漏损量: {typeof leakage === "number" ? `${toM3h(leakage, "m³/s")} ${FLOW_DISPLAY_UNIT}` : "-"} 用户: {creatorName(scheme.username)} ); })} )} ); }; export default SchemeQuery; interface BurstSimulationSchemeItem { scheme_name: string; scheme_type: string; scheme_detail?: { burst_ID?: string[] | string; }; } 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)), ); }; const enrichSchemeWithSimulationBurstIds = ( scheme: BurstSchemeRecord, simulationBurstIdsByName: Record, ) => { const payload = scheme.scheme_detail?.result_payload; const simulationSchemeName = payload?.simulation_scheme?.name; const simulationBurstIds = simulationSchemeName ? simulationBurstIdsByName[simulationSchemeName] ?? [] : []; if (!payload || simulationBurstIds.length === 0) return scheme; if (payload.simulation_scheme?.burst_ids?.length) return scheme; return { ...scheme, scheme_detail: { ...scheme.scheme_detail, result_payload: { ...payload, simulation_scheme: { ...payload.simulation_scheme, burst_ids: simulationBurstIds, }, }, }, }; };