From 377fc32f4c003b734e989c9719da3613166e340b Mon Sep 17 00:00:00 2001 From: JIANG Date: Fri, 6 Mar 2026 09:59:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0DMA=E6=BC=8F=E6=8D=9F?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E9=9D=A2=E6=9D=BF=E6=95=B4=E4=BD=93=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dma-leak-detection/loading.tsx | 5 + .../dma-leak-detection/page.tsx | 20 + src/app/OlMap/Controls/StyleLegend.tsx | 91 ++-- src/app/_refine_context.tsx | 9 + .../ContaminantSimulation/ResultsPanel.tsx | 30 -- .../WaterQualityPanel.tsx | 5 - .../DMALeakDetection/AnalysisParameters.tsx | 260 +++++++++++ .../DMALeakDetectionPanel.tsx | 432 ++++++++++++++++++ .../olmap/DMALeakDetection/SchemeQuery.tsx | 259 +++++++++++ .../olmap/DMALeakDetection/types.ts | 53 +++ 10 files changed, 1096 insertions(+), 68 deletions(-) create mode 100644 src/app/(main)/hydraulic-simulation/dma-leak-detection/loading.tsx create mode 100644 src/app/(main)/hydraulic-simulation/dma-leak-detection/page.tsx delete mode 100644 src/components/olmap/ContaminantSimulation/ResultsPanel.tsx create mode 100644 src/components/olmap/DMALeakDetection/AnalysisParameters.tsx create mode 100644 src/components/olmap/DMALeakDetection/DMALeakDetectionPanel.tsx create mode 100644 src/components/olmap/DMALeakDetection/SchemeQuery.tsx create mode 100644 src/components/olmap/DMALeakDetection/types.ts diff --git a/src/app/(main)/hydraulic-simulation/dma-leak-detection/loading.tsx b/src/app/(main)/hydraulic-simulation/dma-leak-detection/loading.tsx new file mode 100644 index 0000000..2c57921 --- /dev/null +++ b/src/app/(main)/hydraulic-simulation/dma-leak-detection/loading.tsx @@ -0,0 +1,5 @@ +import { MapSkeleton } from "@components/loading/MapSkeleton"; + +export default function Loading() { + return ; +} diff --git a/src/app/(main)/hydraulic-simulation/dma-leak-detection/page.tsx b/src/app/(main)/hydraulic-simulation/dma-leak-detection/page.tsx new file mode 100644 index 0000000..fc90a12 --- /dev/null +++ b/src/app/(main)/hydraulic-simulation/dma-leak-detection/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import MapComponent from "@app/OlMap/MapComponent"; +import MapToolbar from "@app/OlMap/Controls/Toolbar"; +import DMALeakDetectionPanel from "@/components/olmap/DMALeakDetection/DMALeakDetectionPanel"; + +export default function Home() { + return ( +
+ + + + +
+ ); +} diff --git a/src/app/OlMap/Controls/StyleLegend.tsx b/src/app/OlMap/Controls/StyleLegend.tsx index e374e84..a1bffe8 100644 --- a/src/app/OlMap/Controls/StyleLegend.tsx +++ b/src/app/OlMap/Controls/StyleLegend.tsx @@ -10,6 +10,9 @@ interface LegendStyleConfig { type: string; // 图例类型 dimensions: number[]; // 尺寸大小 breaks: number[]; // 分段值 + labels?: string[]; // 可选标签(用于离散分类) + columns?: number; + itemsPerColumn?: number; } // 图例组件 // 该组件用于显示图层样式的图例,包含属性名称、颜色、尺寸和分段值等信息 @@ -24,6 +27,9 @@ const StyleLegend: React.FC = ({ type, // 图例类型 dimensions, breaks, + labels, + columns = 1, + itemsPerColumn, }) => { return ( = ({ {layerName} - {property} - {[...Array(breaks.length)].map((_, index) => { - const color = colors[index]; // 默认颜色为黑色 - const dimension = dimensions[index]; // 默认尺寸为16 + 0 + ? undefined + : `repeat(${Math.max(1, columns)}, minmax(0, 1fr))`, + gridTemplateRows: + itemsPerColumn && itemsPerColumn > 0 + ? `repeat(${itemsPerColumn}, minmax(0, auto))` + : undefined, + gridAutoFlow: + itemsPerColumn && itemsPerColumn > 0 ? "column" : undefined, + columnGap: 1.5, + rowGap: 0.5, + }} + > + {[...Array(breaks.length)].map((_, index) => { + const color = colors[index]; // 默认颜色为黑色 + const dimension = dimensions[index]; // 默认尺寸为16 // // 处理第一个区间(小于 breaks[0]) // if (index === 0) { @@ -66,37 +89,39 @@ const StyleLegend: React.FC = ({ // } // 处理中间区间(breaks[index] - breaks[index + 1]) - if (index + 1 < breaks.length) { - const prevValue = breaks[index]; - const currentValue = breaks[index + 1]; - return ( - - - - {prevValue?.toFixed(1)} - {currentValue?.toFixed(1)} - - - ); - } + if (index + 1 < breaks.length) { + const prevValue = breaks[index]; + const currentValue = breaks[index + 1]; + return ( + + + + {labels?.[index] ?? + `${prevValue?.toFixed(1)} - ${currentValue?.toFixed(1)}`} + + + ); + } - return null; - })} + return null; + })} + ); }; diff --git a/src/app/_refine_context.tsx b/src/app/_refine_context.tsx index e88653d..282ea2a 100644 --- a/src/app/_refine_context.tsx +++ b/src/app/_refine_context.tsx @@ -180,6 +180,15 @@ const App = (props: React.PropsWithChildren) => { label: "爆管分析定位", }, }, + { + name: "DMA漏损识别", + list: "/hydraulic-simulation/dma-leak-detection", + meta: { + parent: "Hydraulic Simulation", + icon: , + label: "DMA漏损识别", + }, + }, { name: "水质模拟", list: "/hydraulic-simulation/water-quality-simulation", diff --git a/src/components/olmap/ContaminantSimulation/ResultsPanel.tsx b/src/components/olmap/ContaminantSimulation/ResultsPanel.tsx deleted file mode 100644 index 92aae8e..0000000 --- a/src/components/olmap/ContaminantSimulation/ResultsPanel.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import React from "react"; -import { Box, Typography } from "@mui/material"; - -interface ResultsPanelProps { - schemeName?: string; -} - -const ResultsPanel: React.FC = ({ schemeName }) => { - return ( - - - - 水质模拟结果 - - - 请在下方时间轴查看各时刻的水质分布。 - - {schemeName && ( - - 当前方案:{schemeName} - - )} - - - ); -}; - -export default ResultsPanel; diff --git a/src/components/olmap/ContaminantSimulation/WaterQualityPanel.tsx b/src/components/olmap/ContaminantSimulation/WaterQualityPanel.tsx index 9450e9f..43c7023 100644 --- a/src/components/olmap/ContaminantSimulation/WaterQualityPanel.tsx +++ b/src/components/olmap/ContaminantSimulation/WaterQualityPanel.tsx @@ -19,7 +19,6 @@ import { } from "@mui/icons-material"; import ContaminantAnalysisParameters from "./AnalysisParameters"; import ContaminantSchemeQuery from "./SchemeQuery"; -import ContaminantResultsPanel from "./ResultsPanel"; import { useData } from "@app/OlMap/MapComponent"; interface WaterQualityPanelProps { @@ -175,10 +174,6 @@ const WaterQualityPanel: React.FC = ({ setCurrentTab(2)} /> - - - - diff --git a/src/components/olmap/DMALeakDetection/AnalysisParameters.tsx b/src/components/olmap/DMALeakDetection/AnalysisParameters.tsx new file mode 100644 index 0000000..a19bd44 --- /dev/null +++ b/src/components/olmap/DMALeakDetection/AnalysisParameters.tsx @@ -0,0 +1,260 @@ +"use client"; + +import React, { useMemo, useState } from "react"; +import { + Alert, + Box, + Button, + Collapse, + TextField, + Typography, +} from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales"; +import dayjs, { Dayjs } from "dayjs"; +import "dayjs/locale/zh-cn"; +import { useNotification } from "@refinedev/core"; +import { api } from "@/lib/api"; +import { NETWORK_NAME, config } from "@config/config"; +import { LeakageResultDetail } from "./types"; + +interface Props { + onResult: (result: LeakageResultDetail) => void; +} + +const AnalysisParameters: React.FC = ({ onResult }) => { + const { open } = useNotification(); + const [schemeName, setSchemeName] = useState(`DMA_Leak_${Date.now()}`); + const [dmaCount, setDmaCount] = useState(5); + const [startTime, setStartTime] = useState( + dayjs().subtract(2, "hour"), + ); + const [endTime, setEndTime] = useState(dayjs()); + const [popSize, setPopSize] = useState(50); + const [maxGen, setMaxGen] = useState(100); + const [qSum, setQSum] = useState(0.4); + const [advancedOpen, setAdvancedOpen] = useState(false); + const [running, setRunning] = useState(false); + + const isValid = useMemo(() => { + if (!schemeName.trim() || !startTime || !endTime) return false; + return startTime.isBefore(endTime) && qSum >= 0.1; + }, [schemeName, startTime, endTime, qSum]); + + const handleRun = async () => { + if (!isValid || !startTime || !endTime) { + open?.({ type: "error", message: "请完善参数并确认时间范围合法" }); + return; + } + setRunning(true); + open?.({ + key: "dma-leak-analysis", + type: "progress", + message: "方案提交分析中", + undoableTimeout: 3, + }); + try { + const response = await api.post( + `${config.BACKEND_URL}/api/v1/leakage/identify/`, + { + network: NETWORK_NAME, + scheme_name: schemeName.trim(), + dma_count: dmaCount, + scada_start: startTime.toISOString(), + scada_end: endTime.toISOString(), + pop_size: popSize, + max_gen: maxGen, + q_sum: qSum, + q_sum_unit: "m3/s", + output_flow_unit: "m3/s", + }, + ); + onResult(response.data as LeakageResultDetail); + open?.({ + key: "dma-leak-analysis", + type: "success", + message: "方案分析成功", + description: "DMA漏损识别完成,请在方案查询中查看结果。", + }); + } catch (error: any) { + open?.({ + key: "dma-leak-analysis", + type: "error", + message: "提交分析失败", + description: error?.response?.data?.detail ?? "请求失败", + }); + } finally { + setRunning(false); + } + }; + + return ( + + + + 漏损识别耗时较长(DMA 数量越多越慢),建议先用较小 DMA 数量试跑。 + + + + + 方案名称 + + setSchemeName(e.target.value)} + placeholder="请输入方案名称" + fullWidth + size="small" + /> + + + + + DMA 数量 + + { + const value = Number.parseInt(e.target.value, 10); + setDmaCount(Number.isNaN(value) ? 5 : Math.max(3, value)); + }} + fullWidth + size="small" + inputProps={{ min: 3, step: 1 }} + /> + + + + + + SCADA 开始时间 + + + + + + SCADA 结束时间 + + + + + + + + 总漏损流量 (m3/s) + + { + const value = Number(e.target.value); + setQSum(Number.isNaN(value) ? 0.4 : Math.max(0.1, value)); + }} + inputProps={{ min: 0.1, step: 0.1 }} + /> + + setAdvancedOpen((prev) => !prev)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.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" }, + }} + > + + 高级选项 + + + + + + + setPopSize(Number(e.target.value))} + /> + setMaxGen(Number(e.target.value))} + /> + + + + + + + + + + + + ); +}; + +export default AnalysisParameters; diff --git a/src/components/olmap/DMALeakDetection/DMALeakDetectionPanel.tsx b/src/components/olmap/DMALeakDetection/DMALeakDetectionPanel.tsx new file mode 100644 index 0000000..6c1e3de --- /dev/null +++ b/src/components/olmap/DMALeakDetection/DMALeakDetectionPanel.tsx @@ -0,0 +1,432 @@ +"use client"; + +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { + Box, + Drawer, + Tabs, + Tab, + Typography, + IconButton, + Tooltip, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Chip, +} from "@mui/material"; +import { + Analytics as AnalyticsIcon, + Search as SearchIcon, + ChevronLeft, + ChevronRight, + FormatListBulleted, +} from "@mui/icons-material"; +import dayjs from "dayjs"; +import { Circle as CircleStyle, Fill, Stroke, Style } from "ol/style"; +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; +import Feature from "ol/Feature"; +import { queryFeaturesByIds } from "@/utils/mapQueryService"; +import { useMap } from "@app/OlMap/MapComponent"; +import StyleLegend from "@app/OlMap/Controls/StyleLegend"; +import AnalysisParameters from "./AnalysisParameters"; +import SchemeQuery from "./SchemeQuery"; +import { LeakageResultDetail } from "./types"; + +const TabPanel = ({ + value, + index, + children, +}: { + value: number; + index: number; + children: React.ReactNode; +}) => ( + +); + +const AREA_COLORS = [ + "#2563eb", + "#7c3aed", + "#0891b2", + "#16a34a", + "#ca8a04", + "#dc2626", + "#ea580c", + "#0f766e", + "#4338ca", + "#be123c", +]; + +const getAreaColor = (areaId: string | number | undefined) => { + const text = String(areaId ?? ""); + let hash = 0; + for (let i = 0; i < text.length; i += 1) { + hash = (hash * 31 + text.charCodeAt(i)) >>> 0; + } + return AREA_COLORS[hash % AREA_COLORS.length]; +}; + +const DMALeakDetectionPanel: React.FC = () => { + const map = useMap(); + const [open, setOpen] = useState(true); + const [tab, setTab] = useState(0); + const [result, setResult] = useState(null); + const [loadedResult, setLoadedResult] = useState(null); + const [nodeLayer, setNodeLayer] = useState | null>(null); + + const sortedRows = useMemo(() => { + if (!result?.rows) return []; + return [...result.rows].sort( + (a, b) => b.LeakageFlow_m3_per_s - a.LeakageFlow_m3_per_s, + ); + }, [result]); + const drawerWidth = 450; + const panelTitle = "DMA漏损识别"; + const activeAreas = loadedResult?.areas ?? []; + const legendColors = useMemo( + () => activeAreas.map((area) => getAreaColor(area.area_id)), + [activeAreas], + ); + const legendLabels = useMemo( + () => activeAreas.map((area) => `区域 ${area.area_id}`), + [activeAreas], + ); + const legendBreaks = useMemo( + () => Array.from({ length: activeAreas.length + 1 }, (_, i) => i + 1), + [activeAreas.length], + ); + + useEffect(() => { + if (!map) return; + const layer = new VectorLayer({ + source: new VectorSource(), + maxZoom: 24, + minZoom: 12, + properties: { + name: "DMA漏损节点着色", + value: "dma_leak_nodes", + }, + style: (feature) => { + const areaId = feature.get("__areaId"); + return new Style({ + image: new CircleStyle({ + radius: 4.5, + fill: new Fill({ color: getAreaColor(areaId) }), + stroke: new Stroke({ color: "#ffffff", width: 1.2 }), + }), + }); + }, + }); + map.addLayer(layer); + setNodeLayer(layer); + return () => { + map.removeLayer(layer); + }; + }, [map]); + + useEffect(() => { + if (!nodeLayer) return; + const source = nodeLayer.getSource(); + if (!source) return; + source.clear(); + if (!loadedResult) return; + + const nodeAreaMap = loadedResult.node_area_map || {}; + const nodeIds = Object.keys(nodeAreaMap); + if (nodeIds.length === 0) return; + + queryFeaturesByIds(nodeIds, "geo_junctions_mat").then((features) => { + if (!features?.length) return; + features.forEach((feature) => { + const nodeId = String(feature.get("id") ?? ""); + feature.set("__areaId", nodeAreaMap[nodeId] ?? ""); + }); + source.addFeatures(features as Feature[]); + }); + }, [loadedResult, nodeLayer]); + + const handleAnalysisResult = useCallback((res: LeakageResultDetail) => { + setResult(res); + }, []); + + const handleViewResult = useCallback((res: LeakageResultDetail) => { + setResult(res); + setLoadedResult(res); + setTab(2); + }, []); + + return ( + <> + {!open && ( + setOpen(true)} + sx={{ zIndex: 1300 }} + > + + + + {panelTitle} + + + + + )} + + + + + + + {panelTitle} + + + + setOpen(false)} + sx={{ color: "primary.contrastText" }} + > + + + + + + setTab(v)} + variant="fullWidth" + sx={{ + minHeight: 48, + "& .MuiTab-root": { + minHeight: 48, + textTransform: "none", + fontSize: "0.875rem", + fontWeight: 500, + transition: "all 0.2s", + }, + "& .Mui-selected": { + color: "#257DD4", + }, + "& .MuiTabs-indicator": { + backgroundColor: "#257DD4", + }, + }} + > + } iconPosition="start" label="识别参数" /> + } iconPosition="start" label="方案查询" /> + } iconPosition="start" label="识别结果" /> + + + + + + + + + + {!result || !sortedRows.length ? ( + + + + + + + + 暂无识别结果 + + 请先加载方案或执行识别分析 + + + ) : ( + + {/* 方案详情卡片 */} + + + + + + {result.scheme_name || "漏损识别结果"} + + + {result.username && ( + + )} + + + + {/* 方案时间 */} + + + 方案时间 + + + {dayjs(result.scheme_start_time || result.create_time).format("MM-DD HH:mm")} + + + + {/* 总漏损流量 */} + + + 总漏损流量 + + + {(() => { + const val = (result.scheme_detail as any)?.algorithm_params?.q_sum; + const unit = (result.scheme_detail as any)?.algorithm_params?.q_sum_unit || "m3/s"; + return val !== undefined ? `${Number(val).toFixed(3)} ${unit}` : "-"; + })()} + + + + {/* 分区数量 */} + + + 分区数量 + + + {(result.scheme_detail as any)?.result_summary?.area_count ?? result.areas?.length ?? 0} 个 + + + + {/* 最大漏损 */} + + + 最大漏损 + + + {(() => { + const maxL = (result.scheme_detail as any)?.result_summary?.max_leakage; + return maxL !== undefined ? `${Number(maxL).toFixed(3)} m3/s` : "-"; + })()} + + + + + + {/* 漏损列表 */} + + + + + + 区域漏损列表 + + + + + + + + 区域 + 漏损量占比 (%) + 漏损量 (m3/s) + + + + {sortedRows.map((row) => ( + + + + + + {row.Area} + + + + {(row.LeakageRatio * 100).toFixed(3)} + {row.LeakageFlow_m3_per_s.toFixed(3)} + + ))} + +
+
+
+ )} +
+
+
+ + {loadedResult && activeAreas.length > 0 && ( + + + + )} + + ); +}; + +export default DMALeakDetectionPanel; diff --git a/src/components/olmap/DMALeakDetection/SchemeQuery.tsx b/src/components/olmap/DMALeakDetection/SchemeQuery.tsx new file mode 100644 index 0000000..e7c6f91 --- /dev/null +++ b/src/components/olmap/DMALeakDetection/SchemeQuery.tsx @@ -0,0 +1,259 @@ +"use client"; + +import React, { useState } from "react"; +import { + Box, + Button, + Card, + CardContent, + Chip, + Collapse, + FormControlLabel, + Checkbox, + IconButton, + Tooltip, + Typography, +} from "@mui/material"; +import { Info as InfoIcon } 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 { api } from "@/lib/api"; +import { NETWORK_NAME, config } from "@config/config"; +import { LeakageResultDetail, LeakageSchemeRecord } from "./types"; + +interface Props { + onViewResult: (result: LeakageResultDetail) => void; +} + +const SchemeQuery: React.FC = ({ onViewResult }) => { + const { open } = useNotification(); + const [queryAll, setQueryAll] = useState(true); + const [queryDate, setQueryDate] = useState(dayjs()); + const [schemes, setSchemes] = useState([]); + const [loading, setLoading] = useState(false); + const [expandedId, setExpandedId] = useState(null); + + const handleQuery = async () => { + setLoading(true); + try { + const params: Record = { network: NETWORK_NAME }; + if (!queryAll && queryDate) { + params.query_date = queryDate.startOf("day").toISOString(); + } + const response = await api.get(`${config.BACKEND_URL}/api/v1/leakage/schemes/`, { + params, + }); + setSchemes(response.data); + } catch (error: any) { + open?.({ + type: "error", + message: "查询失败", + description: error?.response?.data?.detail ?? "无法获取方案列表", + }); + } finally { + setLoading(false); + } + }; + + const handleViewSchemeResult = async (schemeName: string) => { + try { + const response = await api.get( + `${config.BACKEND_URL}/api/v1/leakage/schemes/${encodeURIComponent(schemeName)}`, + { params: { network: NETWORK_NAME } }, + ); + onViewResult(response.data as LeakageResultDetail); + } catch (error: any) { + open?.({ + type: "error", + message: "查看详情失败", + description: error?.response?.data?.detail ?? "无法获取方案详情", + }); + } + }; + + return ( + + + + + setQueryAll(e.target.checked)} + /> + } + label={查询全部} + className="m-0" + /> + + + + + + + + + {schemes.length === 0 ? ( + + + + + + + + 总共 0 条 + + No data + + + ) : ( + + + 共 {schemes.length} 条记录 + + {schemes.map((scheme) => ( + + + + + + + {scheme.scheme_name} + + + + + ID: {scheme.scheme_id} · 日期: {dayjs(scheme.create_time).format("MM-DD")} + + + + + setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id)} + color="primary" + className="p-1" + > + + + + + + + + + + + 分区数量: + + + {String((scheme.scheme_detail as any)?.result_summary?.area_count ?? "-")} + + + + + 用户: + + + {scheme.username || "-"} + + + + + 最大漏损: + + + {(() => { + const value = Number((scheme.scheme_detail as any)?.result_summary?.max_leakage); + return Number.isFinite(value) ? `${value.toFixed(3)} m3/s` : "-"; + })()} + + + + + 总漏损流量: + + + {(() => { + const value = Number((scheme.scheme_detail as any)?.algorithm_params?.q_sum); + const unit = String((scheme.scheme_detail as any)?.algorithm_params?.q_sum_unit || "m3/s"); + return Number.isFinite(value) ? `${value.toFixed(3)} ${unit}` : "-"; + })()} + + + + + 方案时间: + + + {dayjs(scheme.scheme_start_time || scheme.create_time).format("YYYY-MM-DD HH:mm")} + + + + + + + + + + + ))} + + )} + + + ); +}; + +export default SchemeQuery; diff --git a/src/components/olmap/DMALeakDetection/types.ts b/src/components/olmap/DMALeakDetection/types.ts new file mode 100644 index 0000000..b7bf649 --- /dev/null +++ b/src/components/olmap/DMALeakDetection/types.ts @@ -0,0 +1,53 @@ +export interface LeakageRow { + Area: string; + LeakageRatioRaw: number; + LeakageRatio: number; + LeakageFlow_m3_per_s: number; +} + +export interface LeakageSchemeRecord { + scheme_id: number; + scheme_name: string; + scheme_type: string; + username: string; + create_time: string; + scheme_start_time: string; + scheme_detail: Record; +} + +export interface LeakageResultDetail { + scheme_name?: string; + network?: string; + sensor_nodes: string[]; + rows: LeakageRow[]; + node_area_map: Record; + areas: Array<{ + area_id: string; + node_count: number; + node_ids: string[]; + sensor_nodes: string[]; + }>; + drawing_payload: { + type: "FeatureCollection"; + features: Array>; + }; + node_visual_payload?: { + type: "FeatureCollection"; + features: Array>; + }; + scheme_detail?: { + algorithm_params?: { + q_sum?: number; + q_sum_unit?: string; + [key: string]: unknown; + }; + result_summary?: { + area_count?: number; + max_leakage?: number; + }; + [key: string]: unknown; + }; + scheme_start_time?: string; + create_time?: string; + username?: string; +}