From 5ed6740a24a6199491f2219c3482cfc5e723871f Mon Sep 17 00:00:00 2001 From: JIANG Date: Sat, 7 Mar 2026 10:50:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=88=86=E7=AE=A1=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../burst-location/loading.tsx | 5 + .../burst-location/page.tsx | 20 + src/app/_refine_context.tsx | 27 +- .../BurstLocation/AnalysisParameters.tsx | 486 ++++++++++++++++++ .../BurstLocation/BurstLocationPanel.tsx | 162 ++++++ .../olmap/BurstLocation/LocationResults.tsx | 267 ++++++++++ .../olmap/BurstLocation/SchemeQuery.tsx | 253 +++++++++ src/components/olmap/BurstLocation/types.ts | 27 + .../DMALeakDetection/AnalysisParameters.tsx | 2 +- .../DMALeakDetectionPanel.tsx | 2 +- .../olmap/DMALeakDetection/utils.ts | 10 +- 11 files changed, 1247 insertions(+), 14 deletions(-) create mode 100644 src/app/(main)/hydraulic-simulation/burst-location/loading.tsx create mode 100644 src/app/(main)/hydraulic-simulation/burst-location/page.tsx create mode 100644 src/components/olmap/BurstLocation/AnalysisParameters.tsx create mode 100644 src/components/olmap/BurstLocation/BurstLocationPanel.tsx create mode 100644 src/components/olmap/BurstLocation/LocationResults.tsx create mode 100644 src/components/olmap/BurstLocation/SchemeQuery.tsx create mode 100644 src/components/olmap/BurstLocation/types.ts 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; +}) => ( + +); + +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} + + + + + + + + {/* 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; };