diff --git a/src/app/(main)/hydraulic-simulation/burst-location/loading.tsx b/src/app/(main)/hydraulic-simulation/burst-location/loading.tsx
new file mode 100644
index 0000000..2c57921
--- /dev/null
+++ b/src/app/(main)/hydraulic-simulation/burst-location/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/burst-location/page.tsx b/src/app/(main)/hydraulic-simulation/burst-location/page.tsx
new file mode 100644
index 0000000..f8e01cd
--- /dev/null
+++ b/src/app/(main)/hydraulic-simulation/burst-location/page.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import MapComponent from "@app/OlMap/MapComponent";
+import MapToolbar from "@app/OlMap/Controls/Toolbar";
+import BurstLocationPanel from "@/components/olmap/BurstLocation/BurstLocationPanel";
+
+export default function Home() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/_refine_context.tsx b/src/app/_refine_context.tsx
index 282ea2a..4d75a3b 100644
--- a/src/app/_refine_context.tsx
+++ b/src/app/_refine_context.tsx
@@ -18,12 +18,16 @@ import { ProjectProvider } from "@/contexts/ProjectContext";
import { useAuthStore } from "@/store/authStore";
import { LiaNetworkWiredSolid } from "react-icons/lia";
-import { TbDatabaseEdit } from "react-icons/tb";
+import { TbDatabaseEdit, TbLocationPin } from "react-icons/tb";
import { LuReplace } from "react-icons/lu";
import { AiOutlineSecurityScan } from "react-icons/ai";
-import { TbLocationPin } from "react-icons/tb";
import { AiOutlinePartition } from "react-icons/ai";
import { MdWater, MdOutlineWaterDrop, MdCleaningServices } from "react-icons/md";
+import {
+ Analytics as AnalyticsIcon,
+ MyLocation as MyLocationIcon,
+ Search as SearchIcon,
+} from "@mui/icons-material";
type RefineContextProps = {
defaultMode?: string;
@@ -172,21 +176,30 @@ const App = (props: React.PropsWithChildren) => {
},
},
{
- name: "爆管分析定位",
+ name: "爆管模拟",
list: "/hydraulic-simulation/pipe-burst-analysis",
meta: {
parent: "Hydraulic Simulation",
icon: ,
- label: "爆管分析定位",
+ label: "爆管模拟",
},
},
{
- name: "DMA漏损识别",
+ name: "爆管定位",
+ list: "/hydraulic-simulation/burst-location",
+ meta: {
+ parent: "Hydraulic Simulation",
+ icon: ,
+ label: "爆管定位",
+ },
+ },
+ {
+ name: "DMA 漏损识别",
list: "/hydraulic-simulation/dma-leak-detection",
meta: {
parent: "Hydraulic Simulation",
- icon: ,
- label: "DMA漏损识别",
+ icon: ,
+ label: "DMA 漏损识别",
},
},
{
diff --git a/src/components/olmap/BurstLocation/AnalysisParameters.tsx b/src/components/olmap/BurstLocation/AnalysisParameters.tsx
new file mode 100644
index 0000000..7b47ddc
--- /dev/null
+++ b/src/components/olmap/BurstLocation/AnalysisParameters.tsx
@@ -0,0 +1,486 @@
+"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 { DMA_FLOW_DISPLAY_UNIT, toM3s } from "../DMALeakDetection/utils";
+import { BurstLocationResult } from "./types";
+
+interface Props {
+ onResult: (result: BurstLocationResult) => void;
+}
+
+interface SchemeItem {
+ scheme_id: number;
+ scheme_name: string;
+ scheme_type: string;
+ create_time: string;
+ scheme_start_time: string;
+ scheme_detail?: {
+ modify_total_duration: number;
+ };
+}
+
+type DataSource = "monitoring" | "simulation";
+
+const AnalysisParameters: React.FC = ({ onResult }) => {
+ 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 [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 [normalStartTime, setNormalStartTime] = useState(
+ dayjs().subtract(2, "hour"),
+ );
+ const [normalEndTime, setNormalEndTime] = useState(
+ dayjs().subtract(90, "minute"),
+ );
+ const [minDpressure, setMinDpressure] = useState(2);
+ const [basicPressure, setBasicPressure] = useState(10);
+ const [advancedOpen, setAdvancedOpen] = useState(false);
+ const [running, setRunning] = useState(false);
+
+ 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");
+
+ setBurstStartTime(start);
+ setBurstEndTime(end);
+ setNormalStartTime(start.subtract(2, "hour"));
+ setNormalEndTime(start.subtract(10, "minute"));
+ }, []);
+
+ 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/getallschemes/`, {
+ params: { network: NETWORK_NAME },
+ });
+ const burstSchemes = (response.data as SchemeItem[]).filter(
+ (scheme) => scheme.scheme_type === "burst_analysis",
+ );
+
+ setSchemes(burstSchemes);
+
+ if (selectedSchemeId) {
+ const matchedScheme = burstSchemes.find(
+ (scheme) => scheme.scheme_id === selectedSchemeId,
+ );
+ if (matchedScheme) {
+ applySchemeTimeRange(matchedScheme);
+ } else {
+ setSelectedSchemeId("");
+ }
+ }
+
+ 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],
+ );
+
+ const handleDataSourceChange = (value: DataSource) => {
+ setDataSource(value);
+ if (value === "simulation") {
+ void fetchSchemes();
+ }
+ };
+
+ const handleSchemeSelect = (schemeId: number) => {
+ setSelectedSchemeId(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 || !normalStartTime || !normalEndTime) {
+ return false;
+ }
+ if (dataSource === "simulation" && !selectedSchemeId) {
+ return false;
+ }
+
+ return (
+ burstStartTime.isBefore(burstEndTime) &&
+ normalStartTime.isBefore(normalEndTime)
+ );
+ }, [
+ burstLeakage,
+ burstStartTime,
+ burstEndTime,
+ normalStartTime,
+ normalEndTime,
+ dataSource,
+ selectedSchemeId,
+ ]);
+
+ const handleRun = async () => {
+ if (
+ !isValid ||
+ !burstStartTime ||
+ !burstEndTime ||
+ !normalStartTime ||
+ !normalEndTime
+ ) {
+ open?.({ type: "error", message: "请完善参数并确认时间范围合法" });
+ return;
+ }
+
+ setRunning(true);
+ open?.({
+ key: "burst-location-analysis",
+ 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-location/locate/`,
+ {
+ network: NETWORK_NAME,
+ data_source: dataSource,
+ scheme_name: schemeName.trim() || undefined,
+ burst_leakage: toM3s(burstLeakage, DMA_FLOW_DISPLAY_UNIT),
+ min_dpressure: minDpressure,
+ basic_pressure: basicPressure,
+ scada_burst_start: burstStartTime.toISOString(),
+ scada_burst_end: burstEndTime.toISOString(),
+ scada_normal_start: normalStartTime.toISOString(),
+ scada_normal_end: normalEndTime.toISOString(),
+ use_scada_flow: enableFlow || undefined,
+ simulation_scheme_name: selectedScheme?.scheme_name,
+ simulation_scheme_type: selectedScheme?.scheme_type,
+ },
+ );
+
+ onResult(response.data as BurstLocationResult);
+ open?.({
+ key: "burst-location-analysis",
+ type: "success",
+ message: "爆管定位成功",
+ description: `定位到管段: ${(response.data as BurstLocationResult).located_pipe}`,
+ });
+ } catch (error: any) {
+ open?.({
+ key: "burst-location-analysis",
+ type: "error",
+ message: "提交分析失败",
+ description: error?.response?.data?.detail ?? error?.message ?? "请求失败",
+ });
+ } finally {
+ setRunning(false);
+ }
+ };
+
+ return (
+
+
+
+ 选择模拟方案将自动填充爆管发生时段,监测数据模式下可手动调整时间范围。
+
+
+
+
+ 方案名称
+
+ setSchemeName(e.target.value)}
+ placeholder="请输入方案名称"
+ fullWidth
+ size="small"
+ />
+
+
+
+
+ SCADA 数据来源
+
+
+
+
+
+
+ {dataSource === "simulation" && (
+
+
+ 选择爆管分析方案
+
+
+
+
+
+ void fetchSchemes({ force: true, notify: true })}
+ disabled={schemeLoading}
+ aria-label="刷新爆管分析方案"
+ sx={{
+ border: "1px solid",
+ borderColor: "divider",
+ borderRadius: 1,
+ }}
+ >
+ {schemeLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+
+
+
+
+ 爆管开始时间
+
+
+
+
+
+ 爆管结束时间
+
+
+
+
+
+ 正常开始时间
+
+
+
+
+
+ 正常结束时间
+
+
+
+
+
+
+
+
+ 总漏损流量 ({DMA_FLOW_DISPLAY_UNIT})
+
+ {
+ const value = Number(e.target.value);
+ setBurstLeakage(Number.isNaN(value) ? 1440 : Math.max(0, value));
+ }}
+ fullWidth
+ inputProps={{ min: 0, step: 10 }}
+ />
+
+ 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" },
+ }}
+ >
+
+ 高级选项
+
+
+
+
+
+
+
+
+ 流量校核
+
+
+
+
+
+
+ setMinDpressure(Number(e.target.value))}
+ />
+ setBasicPressure(Number(e.target.value))}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AnalysisParameters;
diff --git a/src/components/olmap/BurstLocation/BurstLocationPanel.tsx b/src/components/olmap/BurstLocation/BurstLocationPanel.tsx
new file mode 100644
index 0000000..fe56b8f
--- /dev/null
+++ b/src/components/olmap/BurstLocation/BurstLocationPanel.tsx
@@ -0,0 +1,162 @@
+"use client";
+
+import React, { useCallback, useState } from "react";
+import { Box, Drawer, IconButton, Tab, Tabs, Tooltip, Typography } from "@mui/material";
+import {
+ Analytics as AnalyticsIcon,
+ ChevronLeft,
+ ChevronRight,
+ FormatListBulleted,
+ Search as SearchIcon,
+} from "@mui/icons-material";
+import AnalysisParameters from "./AnalysisParameters";
+import LocationResults from "./LocationResults";
+import SchemeQuery from "./SchemeQuery";
+import { BurstLocationResult } from "./types";
+
+const TabPanel = ({
+ value,
+ index,
+ children,
+}: {
+ value: number;
+ index: number;
+ children: React.ReactNode;
+}) => (
+
+ {value === index ? {children} : null}
+
+);
+
+const BurstLocationPanel: React.FC = () => {
+ const [open, setOpen] = useState(true);
+ const [tab, setTab] = useState(0);
+ const [result, setResult] = useState(null);
+
+ const drawerWidth = 450;
+ const panelTitle = "爆管定位";
+
+ const handleResult = useCallback((payload: BurstLocationResult) => {
+ setResult(payload);
+ setTab(2);
+ }, []);
+
+ const handleViewResult = useCallback((payload: BurstLocationResult) => {
+ setResult(payload);
+ setTab(2);
+ }, []);
+
+ return (
+ <>
+ {!open && (
+ setOpen(true)}
+ sx={{ zIndex: 1300 }}
+ >
+
+
+
+ {panelTitle}
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ {panelTitle}
+
+
+
+ setOpen(false)}
+ sx={{ color: "primary.contrastText" }}
+ >
+
+
+
+
+
+
+ setTab(value)}
+ 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="定位结果" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default BurstLocationPanel;
diff --git a/src/components/olmap/BurstLocation/LocationResults.tsx b/src/components/olmap/BurstLocation/LocationResults.tsx
new file mode 100644
index 0000000..98d54d7
--- /dev/null
+++ b/src/components/olmap/BurstLocation/LocationResults.tsx
@@ -0,0 +1,267 @@
+"use client";
+
+import React, { useEffect, useMemo, useState } from "react";
+import {
+ Box,
+ Button,
+ Chip,
+ Divider,
+ IconButton,
+ Paper,
+ Tooltip,
+ Typography
+} from "@mui/material";
+import {
+ FormatListBulleted,
+ LocationOn as LocationOnIcon,
+ Map as MapIcon
+} from "@mui/icons-material";
+import dayjs from "dayjs";
+import { useMap } from "@app/OlMap/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 } from "./types";
+
+interface Props {
+ result: BurstLocationResult | null;
+}
+
+const LocationResults: React.FC = ({ result }) => {
+ const map = useMap();
+ const [highlightLayer, setHighlightLayer] =
+ useState | null>(null);
+ const [highlightFeatures, setHighlightFeatures] = useState([]);
+
+ const candidatePipes = useMemo(() => {
+ if (!result) return [];
+ const base = result.top_candidates ?? [];
+ const hasLocated = base.some((item) => item.pipe_id === result.located_pipe);
+ if (result.located_pipe && !hasLocated) {
+ return [{ pipe_id: result.located_pipe, similarity: 1.0 }, ...base];
+ }
+ return base;
+ }, [result]);
+
+ useEffect(() => {
+ if (!map) return;
+
+ const layer = new VectorLayer({
+ source: new VectorSource(),
+ style: new Style({
+ stroke: new Stroke({
+ color: "#ef4444",
+ width: 6,
+ }),
+ image: new Circle({
+ radius: 8,
+ fill: new Fill({ color: "#ef4444" }),
+ stroke: new Stroke({ color: "#fff", width: 2 }),
+ }),
+ zIndex: 999,
+ }),
+ properties: {
+ name: "爆管定位高亮",
+ value: "burst_location_highlight",
+ },
+ });
+ map.addLayer(layer);
+ setHighlightLayer(layer);
+
+ return () => {
+ map.removeLayer(layer);
+ };
+ }, [map]);
+
+ useEffect(() => {
+ const source = highlightLayer?.getSource();
+ if (!source) return;
+ source.clear();
+ highlightFeatures.forEach((feature) => source.addFeature(feature));
+ }, [highlightFeatures, highlightLayer]);
+
+ const locatePipes = async (pipeIds: string[]) => {
+ if (!pipeIds.length || !map) return;
+
+ try {
+ // 尝试两个图层
+ let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat");
+ if (features.length === 0) {
+ features = await queryFeaturesByIds(pipeIds, "geo_pipes");
+ }
+
+ if (features.length === 0) return;
+
+ setHighlightFeatures(features);
+
+ const geojsonFormat = new GeoJSON();
+ const geojsonFeatures = features.map((feature) =>
+ geojsonFormat.writeFeatureObject(feature),
+ );
+ // @ts-ignore
+ const extent = bbox(featureCollection(geojsonFeatures));
+ map.getView().fit(extent, {
+ maxZoom: 19,
+ duration: 1000,
+ padding: [100, 100, 100, 100],
+ });
+ } catch (e) {
+ console.error("Locate failed", e);
+ }
+ };
+
+ if (!result) {
+ return (
+
+
+
+
+ 等待定位
+
+ 请在左侧面板配置传感器参数与时间范围,点击“开始定位”获取结果。
+
+
+ );
+ }
+
+ return (
+
+ {/* 1. 冠军卡片 */}
+
+
+
+
+ {result.scheme_name || "爆管定位结果"}
+
+
+ {result.username && (
+
+ )}
+
+
+ {/* 2. 统计数据 */}
+
+ {/* 方案时间/创建时间 */}
+
+
+ 方案时间
+
+
+ {result.create_time ? dayjs(result.create_time).format("MM-DD HH:mm") : "-"}
+
+
+
+ {/* 漏损量 */}
+
+
+ 漏损量
+
+
+ {result.burst_leakage.toFixed(1)} L/s
+
+
+
+ {/* 最佳匹配 */}
+
+
+
+
+ 最佳匹配管段
+
+
+ {result.located_pipe}
+
+
+ 置信度: {(candidatePipes[0]?.similarity * 100 || 0).toFixed(1)}% · 模式: {result.similarity_mode}
+
+
+ }
+ onClick={() => locatePipes([result.located_pipe])}
+ sx={{ backgroundColor: "#9333ea", "&:hover": { backgroundColor: "#7e22ce" } }}
+ >
+ 定位
+
+
+
+
+
+ {/* 3. 候选列表 */}
+
+
+
+
+
+ 候选管段列表
+
+
+
+
+
+ {candidatePipes.map((candidate, idx) => (
+
+
+
+ {idx + 1}
+
+
+
+ {candidate.pipe_id}
+
+
+ 相似度: {(candidate.similarity * 100).toFixed(2)}%
+
+
+
+ locatePipes([candidate.pipe_id])}
+ className="text-gray-400 hover:text-blue-600"
+ >
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default LocationResults;
diff --git a/src/components/olmap/BurstLocation/SchemeQuery.tsx b/src/components/olmap/BurstLocation/SchemeQuery.tsx
new file mode 100644
index 0000000..f24dbb1
--- /dev/null
+++ b/src/components/olmap/BurstLocation/SchemeQuery.tsx
@@ -0,0 +1,253 @@
+"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 { BurstLocationResult, BurstSchemeRecord } from "./types";
+
+interface Props {
+ onViewResult: (result: BurstLocationResult) => 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 {
+ // API call to fetch schemes
+ // Adjust URL as needed
+ let url = `${config.BACKEND_URL}/api/v1/burst-location/schemes/`;
+ const params: Record = { network: NETWORK_NAME };
+ if (!queryAll && queryDate) {
+ params.query_date = queryDate.startOf("day").toISOString();
+ }
+
+ const response = await api.get(url, { params });
+ setSchemes(response.data);
+ open?.({
+ type: "success",
+ message: "查询成功",
+ description: `共找到 ${response.data.length} 条记录`,
+ });
+ } catch (error: any) {
+ console.error(error);
+ 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/burst-location/schemes/${encodeURIComponent(schemeName)}`,
+ { params: { network: NETWORK_NAME } },
+ );
+ // The backend returns { scheme_detail: ... } inside the response or just the result?
+ // Based on burst_location.py: get_burst_location_scheme_detail returns the stored detail.
+ // Let's assume response.data is the BurstLocationResult
+ onViewResult(response.data as BurstLocationResult);
+ open?.({
+ type: "success",
+ message: "方案加载成功",
+ description: `已加载方案: ${schemeName}`,
+ });
+ } 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 HH:mm")}
+
+
+
+
+ setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id)}
+ color="primary"
+ className="p-1"
+ >
+
+
+
+
+
+
+
+
+ {/* Summary details */}
+
+
+ 定位管段:
+
+
+ {scheme.scheme_detail?.located_pipe || "-"}
+
+
+
+
+ 漏损量:
+
+
+ {scheme.scheme_detail?.burst_leakage ? `${scheme.scheme_detail.burst_leakage} L/s` : "-"}
+
+
+
+
+ 用户:
+
+
+ {scheme.username || "-"}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default SchemeQuery;
diff --git a/src/components/olmap/BurstLocation/types.ts b/src/components/olmap/BurstLocation/types.ts
new file mode 100644
index 0000000..da03b0a
--- /dev/null
+++ b/src/components/olmap/BurstLocation/types.ts
@@ -0,0 +1,27 @@
+export interface BurstCandidate {
+ pipe_id: string;
+ similarity: number;
+}
+
+export interface BurstLocationResult {
+ located_pipe: string;
+ burst_leakage: number;
+ elapsed_seconds: number;
+ simulation_times: number;
+ top_candidates: BurstCandidate[];
+ similarity_mode: string;
+ scheme_name?: string;
+ username?: string;
+ observed_source?: string;
+ pressure_scada_ids?: string[];
+ flow_scada_ids?: string[];
+ create_time?: string;
+}
+
+export interface BurstSchemeRecord {
+ scheme_id: number;
+ scheme_name: string;
+ create_time: string;
+ username?: string;
+ scheme_detail?: BurstLocationResult;
+}
diff --git a/src/components/olmap/DMALeakDetection/AnalysisParameters.tsx b/src/components/olmap/DMALeakDetection/AnalysisParameters.tsx
index 1c45f16..4a6aa7a 100644
--- a/src/components/olmap/DMALeakDetection/AnalysisParameters.tsx
+++ b/src/components/olmap/DMALeakDetection/AnalysisParameters.tsx
@@ -78,7 +78,7 @@ const AnalysisParameters: React.FC = ({ onResult }) => {
key: "dma-leak-analysis",
type: "success",
message: "方案分析成功",
- description: "DMA漏损识别完成,请在方案查询中查看结果。",
+ description: "DMA 漏损识别完成,请在方案查询中查看结果。",
});
} catch (error: any) {
open?.({
diff --git a/src/components/olmap/DMALeakDetection/DMALeakDetectionPanel.tsx b/src/components/olmap/DMALeakDetection/DMALeakDetectionPanel.tsx
index 75cb5a0..24b9e4c 100644
--- a/src/components/olmap/DMALeakDetection/DMALeakDetectionPanel.tsx
+++ b/src/components/olmap/DMALeakDetection/DMALeakDetectionPanel.tsx
@@ -54,7 +54,7 @@ const DMALeakDetectionPanel: React.FC = () => {
const [loadedResult, setLoadedResult] = useState(null);
const drawerWidth = 450;
- const panelTitle = "DMA漏损识别";
+ const panelTitle = "DMA 漏损识别";
const activeAreas = loadedResult?.areas ?? [];
const legendColors = useMemo(
() => activeAreas.map((area) => getAreaColor(area.area_id)),
diff --git a/src/components/olmap/DMALeakDetection/utils.ts b/src/components/olmap/DMALeakDetection/utils.ts
index c38ee6f..2d56f73 100644
--- a/src/components/olmap/DMALeakDetection/utils.ts
+++ b/src/components/olmap/DMALeakDetection/utils.ts
@@ -11,7 +11,7 @@ export const AREA_COLORS = [
"#be123c",
];
-export const DMA_FLOW_DISPLAY_UNIT = "m3/h";
+export const DMA_FLOW_DISPLAY_UNIT = "m³/h";
const M3H_FACTOR = 3600;
export const getAreaColor = (areaId: string | number | undefined) => {
@@ -23,16 +23,16 @@ export const getAreaColor = (areaId: string | number | undefined) => {
return AREA_COLORS[hash % AREA_COLORS.length];
};
-export const toM3h = (value: number, sourceUnit: string = "m3/s") => {
+export const toM3h = (value: number, sourceUnit: string = "m³/s") => {
if (!Number.isFinite(value)) return Number.NaN;
const normalizedUnit = sourceUnit.trim().toLowerCase();
- if (normalizedUnit === "m3/h") return value;
+ if (normalizedUnit === "m³/h") return value;
return value * M3H_FACTOR;
};
-export const toM3s = (value: number, sourceUnit: string = "m3/h") => {
+export const toM3s = (value: number, sourceUnit: string = "m³/h") => {
if (!Number.isFinite(value)) return Number.NaN;
const normalizedUnit = sourceUnit.trim().toLowerCase();
- if (normalizedUnit === "m3/s") return value;
+ if (normalizedUnit === "m³/s") return value;
return value / M3H_FACTOR;
};