diff --git a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx index 0f20257..0ef868f 100644 --- a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx +++ b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx @@ -19,7 +19,10 @@ import { } from "@mui/icons-material"; import AnalysisParameters from "./AnalysisParameters"; import SchemeQuery from "./SchemeQuery"; -import LocationResults from "./LocationResults"; +import LocationResults, { LocationResult } from "./LocationResults"; +import axios from "axios"; +import { config } from "@config/config"; +import { useNotification } from "@refinedev/core"; interface SchemeDetail { burst_ID: string[]; burst_size: number[]; @@ -74,6 +77,10 @@ const BurstPipeAnalysisPanel: React.FC = ({ // 持久化方案查询结果 const [schemes, setSchemes] = useState([]); + // 定位结果数据 + const [locationResults, setLocationResults] = useState([]); + + const { open } = useNotification(); // 使用受控或非受控状态 const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen; @@ -88,6 +95,24 @@ const BurstPipeAnalysisPanel: React.FC = ({ const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { setCurrentTab(newValue); }; + + const handleLocateScheme = async (scheme: SchemeRecord) => { + try { + const response = await axios.get( + `${config.BACKEND_URL}/postgresql/burst-locate-result/${scheme.schemeName}` + ); + setLocationResults(response.data); + setCurrentTab(2); // 切换到定位结果标签页 + } catch (error) { + console.error("获取定位结果失败:", error); + open?.({ + type: "error", + message: "获取定位结果失败", + description: "无法从服务器获取该方案的定位结果", + }); + } + }; + const drawerWidth = 520; return ( @@ -214,19 +239,21 @@ const BurstPipeAnalysisPanel: React.FC = ({ { - console.log("定位方案:", id); - // TODO: 在地图上定位 - }} + onLocate={handleLocateScheme} /> { - console.log("定位到:", coordinates); + results={locationResults} + onLocate={(result) => { + console.log("定位到:", result.locate_result); // TODO: 地图定位到指定坐标 }} + onLocateAll={(results) => { + console.log("定位全部结果:", results); + // TODO: 地图定位到所有结果坐标 + }} onViewDetail={(id) => { console.log("查看节点详情:", id); // TODO: 显示节点详细信息 diff --git a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx index ce564eb..2292ad9 100644 --- a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx +++ b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx @@ -1,132 +1,188 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Box, Typography, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, Chip, IconButton, Tooltip, + Link, } from "@mui/material"; +import { LocationOn as LocationIcon } from "@mui/icons-material"; +import { queryFeaturesByIds } from "@/utils/mapQueryService"; +import { useMap } from "@app/OlMap/MapComponent"; +import { GeoJSON } from "ol/format"; +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; +import { Stroke, Style, Icon } from "ol/style"; +import Feature, { FeatureLike } from "ol/Feature"; import { - LocationOn as LocationIcon, - Visibility as VisibilityIcon, -} from "@mui/icons-material"; + along, + lineString, + length, + toMercator, + bbox, + featureCollection, +} from "@turf/turf"; +import { Point } from "ol/geom"; +import { toLonLat } from "ol/proj"; +import moment from "moment"; +import "moment-timezone"; -interface LocationResult { +export interface LocationResult { id: number; - nodeName: string; - nodeId: string; - pressure: number; - waterLevel: number; - flow: number; - status: "normal" | "warning" | "danger"; - coordinates: [number, number]; + type: string; + burst_incident: string; + leakage: number | null; + detect_time: string; + locate_result: string[] | null; } interface LocationResultsProps { - onLocate?: (coordinates: [number, number]) => void; - onViewDetail?: (id: number) => void; + results?: LocationResult[]; } -const LocationResults: React.FC = ({ - onLocate, - onViewDetail, -}) => { - const [results, setResults] = useState([ - // 示例数据 - // { - // id: 1, - // nodeName: '节点A', - // nodeId: 'N001', - // pressure: 0.35, - // waterLevel: 12.5, - // flow: 85.3, - // status: 'normal', - // coordinates: [120.15, 30.25], - // }, - ]); +const LocationResults: React.FC = ({ results = [] }) => { + const [highlightLayer, setHighlightLayer] = + useState | null>(null); + const [highlightFeatures, setHighlightFeatures] = useState([]); + const map = useMap(); - const getStatusColor = (status: string) => { - switch (status) { - case "normal": - return "success"; - case "warning": - return "warning"; - case "danger": - return "error"; - default: - return "default"; + // 格式化时间为 UTC+8 + const formatTime = (timeStr: string) => { + return moment(timeStr).utcOffset(8).format("YYYY-MM-DD HH:mm:ss"); + }; + + const handleLocatePipes = (pipeIds: string[]) => { + if (pipeIds.length > 0) { + queryFeaturesByIds(pipeIds).then((features) => { + if (features.length > 0) { + // 设置高亮要素 + setHighlightFeatures(features); + // 将 OpenLayers Feature 转换为 GeoJSON Feature + const geojsonFormat = new GeoJSON(); + const geojsonFeatures = features.map((feature) => + geojsonFormat.writeFeatureObject(feature) + ); + + const extent = bbox(featureCollection(geojsonFeatures as any)); + + if (extent) { + map?.getView().fit(extent, { maxZoom: 18, duration: 1000 }); + } + } + }); } }; - const getStatusText = (status: string) => { - switch (status) { - case "normal": - return "正常"; - case "warning": - return "预警"; - case "danger": - return "危险"; - default: - return "未知"; + // 初始化管道图层和高亮图层 + useEffect(() => { + if (!map) return; + + const burstPipeStyle = function (feature: FeatureLike) { + const styles = []; + // 线条样式(底层发光,主线条,内层高亮线) + styles.push( + new Style({ + stroke: new Stroke({ + color: "rgba(255, 0, 0, 0.3)", + width: 12, + }), + }), + new Style({ + stroke: new Stroke({ + color: "rgba(255, 0, 0, 1)", + width: 6, + lineDash: [15, 10], + }), + }), + new Style({ + stroke: new Stroke({ + color: "rgba(255, 102, 102, 1)", + width: 3, + lineDash: [15, 10], + }), + }) + ); + const geometry = feature.getGeometry(); + const lineCoords = + geometry?.getType() === "LineString" + ? (geometry as any).getCoordinates() + : null; + if (geometry && lineCoords) { + const lineCoordsWGS84 = lineCoords.map((coord: []) => { + const [lon, lat] = toLonLat(coord); + return [lon, lat]; + }); + // 计算中点 + const lineStringFeature = lineString(lineCoordsWGS84); + const lineLength = length(lineStringFeature); + const midPoint = along(lineStringFeature, lineLength / 2).geometry + .coordinates; + // 在中点添加 icon 样式 + const midPointMercator = toMercator(midPoint); + styles.push( + new Style({ + geometry: new Point(midPointMercator), + image: new Icon({ + src: "/icons/burst_pipe.svg", + scale: 0.2, + anchor: [0.5, 1], + }), + }) + ); + } + return styles; + }; + // 创建高亮图层 + const highlightLayer = new VectorLayer({ + source: new VectorSource(), + style: burstPipeStyle, + maxZoom: 24, + minZoom: 12, + properties: { + name: "爆管管段高亮", + value: "burst_pipe_highlight", + }, + }); + + map.addLayer(highlightLayer); + setHighlightLayer(highlightLayer); + + return () => { + map.removeLayer(highlightLayer); + }; + }, [map]); + + // 高亮要素的函数 + useEffect(() => { + if (!highlightLayer) { + return; } - }; + const source = highlightLayer.getSource(); + if (!source) { + return; + } + // 清除之前的高亮 + source.clear(); + // 添加新的高亮要素 + highlightFeatures.forEach((feature) => { + if (feature instanceof Feature) { + source.addFeature(feature); + } + }); + }, [highlightFeatures, highlightLayer]); + + // 取第一条记录或空对象 + const result = results.length > 0 ? results[0] : null; return ( - {/* 统计信息 */} - - - 定位结果统计 - - - - - 总数 - - - {results.length} - - - - - 正常 - - - {results.filter((r) => r.status === "normal").length} - - - - - 预警 - - - {results.filter((r) => r.status === "warning").length} - - - - - 危险 - - - {results.filter((r) => r.status === "danger").length} - - - - - - {/* 结果列表 */} - - {results.length === 0 ? ( - + {/* 结果展示 */} + + {!result ? ( + = ({ ) : ( - - - - - 节点名称 - 节点ID - 压力 (MPa) - 水位 (m) - 流量 (m³/h) - 状态 - 操作 - - - - {results.map((result) => ( - - {result.nodeName} - {result.nodeId} - - {result.pressure.toFixed(3)} - - - {result.waterLevel.toFixed(3)} - - - {result.flow.toFixed(1)} - - - - - - - onLocate?.(result.coordinates)} - color="primary" + + {/* 头部:标识信息 */} + + + + {result.burst_incident} + + + + + ID: {result.id} + + + + {/* 主要信息:三栏卡片布局 */} + + {/* 检测时间卡片 */} + + + + + 检测时间 + + + + {formatTime(result.detect_time)} + + + + {/* 漏损量卡片 */} + + + + + 漏损量 + + + + {result.leakage !== null + ? `${result.leakage.toFixed(2)} m³/h` + : "N/A"} + + + + {/* 定位管段数量卡片 */} + + + + + 定位管段 + + + + {result.locate_result ? result.locate_result.length : 0}{" "} + 个管段 + + + + + {/* 定位管段详细列表 */} + {result.locate_result && result.locate_result.length > 0 && ( + + + + 管段列表 + + + handleLocatePipes(result.locate_result!)} + color="primary" + sx={{ + backgroundColor: "rgba(37, 125, 212, 0.1)", + "&:hover": { + backgroundColor: "rgba(37, 125, 212, 0.2)", + }, + }} + > + + + + + + {result.locate_result.map((pipeId, idx) => ( + handleLocatePipes([pipeId])} + > + + - - - - - onViewDetail?.(result.id)} - color="primary" - > - - - - - - ))} - -
-
+ {pipeId} + + +
+
+ ))} +
+
+ )} +
)} diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx index 0c9c110..51c4cb5 100644 --- a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx @@ -83,7 +83,7 @@ interface SchemaItem { interface SchemeQueryProps { schemes?: SchemeRecord[]; onSchemesChange?: (schemes: SchemeRecord[]) => void; - onLocate?: (id: number) => void; + onLocate?: (scheme: SchemeRecord) => void; network?: string; } @@ -486,7 +486,7 @@ const SchemeQuery: React.FC = ({ onLocate?.(scheme.id)} + onClick={() => onLocate?.(scheme)} color="primary" className="p-1" > diff --git a/src/components/olmap/SCADADeviceList.tsx b/src/components/olmap/SCADADeviceList.tsx index 6a9b14e..bdd2ae0 100644 --- a/src/components/olmap/SCADADeviceList.tsx +++ b/src/components/olmap/SCADADeviceList.tsx @@ -605,6 +605,12 @@ const SCADADeviceList: React.FC = ({ setIsCleaning(true); + open?.({ + type: "progress", + message: "正在清洗数据,请稍候...", + undoableTimeout: 3000, + }); + try { const startTime = dayjs(cleanStartTime).toISOString(); const endTime = dayjs(cleanEndTime).toISOString();