From 6f0ef342e99efae5ae4edee2950e5642157a90cf Mon Sep 17 00:00:00 2001 From: JIANG Date: Tue, 3 Feb 2026 10:56:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=85=B3=E9=98=80=E5=88=86?= =?UTF-8?q?=E6=9E=90=E9=A1=B5=E9=9D=A2=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BurstPipeAnalysisPanel.tsx | 30 +- .../BurstPipeAnalysis/LocationResults.tsx | 117 ++- .../olmap/BurstPipeAnalysis/SchemeQuery.tsx | 2 +- .../BurstPipeAnalysis/ValveIsolation.test.tsx | 98 --- .../BurstPipeAnalysis/ValveIsolation.tsx | 802 ++++++++++++++++-- .../olmap/BurstPipeAnalysis/types.ts | 3 +- .../ContaminantSimulation/SchemeQuery.tsx | 2 +- src/components/olmap/SCADADeviceList.tsx | 32 +- 8 files changed, 846 insertions(+), 240 deletions(-) delete mode 100644 src/components/olmap/BurstPipeAnalysis/ValveIsolation.test.tsx diff --git a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx index 4ee0e18..4288cb3 100644 --- a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx +++ b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx @@ -29,7 +29,7 @@ import axios from "axios"; import { config } from "@config/config"; import { useNotification } from "@refinedev/core"; import { useData } from "@app/OlMap/MapComponent"; -import { LocationResult, SchemeRecord } from "./types"; +import { LocationResult, SchemeRecord, ValveIsolationResult } from "./types"; interface TabPanelProps { children?: React.ReactNode; @@ -75,6 +75,13 @@ const BurstPipeAnalysisPanel: React.FC = ({ const [schemes, setSchemes] = useState([]); // 定位结果数据 const [locationResults, setLocationResults] = useState([]); + // 选中的管段ID数组 + const [selectedPipeIds, setSelectedPipeIds] = useState([]); + // 关阀分析状态提升到父组件 + const [valveAnalysisTriggered, setValveAnalysisTriggered] = useState(false); + // 关阀分析结果和加载状态 + const [valveAnalysisLoading, setValveAnalysisLoading] = useState(false); + const [valveAnalysisResult, setValveAnalysisResult] = useState(null); const { open } = useNotification(); @@ -109,6 +116,12 @@ const BurstPipeAnalysisPanel: React.FC = ({ } }; + const handleAnalyzePipe = (pipeIds: string[]) => { + setSelectedPipeIds(pipeIds); + setValveAnalysisTriggered(true); + setCurrentTab(3); + }; + const drawerWidth = 520; const isBurstMode = panelMode === "burst"; const panelTitle = isBurstMode ? "爆管分析" : "水质模拟"; @@ -283,7 +296,10 @@ const BurstPipeAnalysisPanel: React.FC = ({ {isBurstMode ? ( - + ) : ( )} @@ -291,7 +307,15 @@ const BurstPipeAnalysisPanel: React.FC = ({ {isBurstMode && ( - + setValveAnalysisTriggered(false)} + loading={valveAnalysisLoading} + result={valveAnalysisResult} + onLoadingChange={setValveAnalysisLoading} + onResultChange={setValveAnalysisResult} + /> )} diff --git a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx index 047b730..cc5b9d3 100644 --- a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx +++ b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx @@ -9,7 +9,10 @@ import { Tooltip, Link, } from "@mui/material"; -import { LocationOn as LocationIcon } from "@mui/icons-material"; +import { + LocationOn as LocationIcon, + Handyman as HandymanIcon, +} from "@mui/icons-material"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { useMap } from "@app/OlMap/MapComponent"; import { GeoJSON } from "ol/format"; @@ -33,9 +36,13 @@ import { LocationResult } from "./types"; interface LocationResultsProps { results?: LocationResult[]; + onAnalyze?: (pipeIds: string[]) => void; } -const LocationResults: React.FC = ({ results = [] }) => { +const LocationResults: React.FC = ({ + results = [], + onAnalyze, +}) => { const [highlightLayer, setHighlightLayer] = useState | null>(null); const [highlightFeatures, setHighlightFeatures] = useState([]); @@ -55,7 +62,7 @@ const LocationResults: React.FC = ({ results = [] }) => { // 将 OpenLayers Feature 转换为 GeoJSON Feature const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => - geojsonFormat.writeFeatureObject(feature) + geojsonFormat.writeFeatureObject(feature), ); const extent = bbox(featureCollection(geojsonFeatures as any)); @@ -95,7 +102,7 @@ const LocationResults: React.FC = ({ results = [] }) => { width: 3, lineDash: [15, 10], }), - }) + }), ); const geometry = feature.getGeometry(); const lineCoords = @@ -122,7 +129,7 @@ const LocationResults: React.FC = ({ results = [] }) => { scale: 0.2, anchor: [0.5, 1], }), - }) + }), ); } return styles; @@ -243,7 +250,9 @@ const LocationResults: React.FC = ({ results = [] }) => { {result.burst_incident} = ({ results = [] }) => { > 管段列表 - - handleLocatePipes(result.locate_result!)} - color="primary" - sx={{ - backgroundColor: "rgba(37, 125, 212, 0.1)", - "&:hover": { - backgroundColor: "rgba(37, 125, 212, 0.2)", - }, - }} - > - - - + + {onAnalyze && ( + + onAnalyze(result.locate_result!)} + color="secondary" + sx={{ + backgroundColor: "rgba(156, 39, 176, 0.1)", + "&:hover": { + backgroundColor: "rgba(156, 39, 176, 0.2)", + }, + }} + > + + + + )} + + 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) => ( @@ -361,6 +389,12 @@ const LocationResults: React.FC = ({ results = [] }) => { key={idx} className="bg-gradient-to-r from-blue-50 to-white rounded-lg px-3 py-2 border border-blue-200 hover:border-blue-400 hover:shadow-md transition-all cursor-pointer group" onClick={() => handleLocatePipes([pipeId])} + sx={{ + "&:active": { + transform: "scale(0.98)", + boxShadow: "0 1px 2px rgba(25, 118, 210, 0.2)", + }, + }} > = ({ results = [] }) => { > {pipeId} - + + {onAnalyze && ( + + { + e.stopPropagation(); + onAnalyze([pipeId]); + }} + className="text-blue-400 hover:text-blue-600" + sx={{ + "&:hover": { + backgroundColor: "rgba(37, 125, 212, 0.1)", + }, + }} + > + + + + )} + {/* + { + e.stopPropagation(); + handleLocatePipes([pipeId]); + }} + sx={{ + "&:hover": { + backgroundColor: "rgba(37, 125, 212, 0.1)", + }, + }} + > + + + */} + ))} diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx index f32ffc1..34dfcf3 100644 --- a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx @@ -464,7 +464,7 @@ const SchemeQuery: React.FC = ({ - + onLocate?.(scheme)} diff --git a/src/components/olmap/BurstPipeAnalysis/ValveIsolation.test.tsx b/src/components/olmap/BurstPipeAnalysis/ValveIsolation.test.tsx deleted file mode 100644 index 0389aef..0000000 --- a/src/components/olmap/BurstPipeAnalysis/ValveIsolation.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React from "react"; -import { render, screen, fireEvent, waitFor } from "@testing-library/react"; -import ValveIsolation from "./ValveIsolation"; -import axios from "axios"; -import "@testing-library/jest-dom"; - -// Mock dependencies -jest.mock("axios"); -const mockedAxios = axios as jest.Mocked; - -jest.mock("@refinedev/core", () => ({ - useNotification: () => ({ - open: jest.fn(), - }), -})); - -// Mock config -jest.mock("@config/config", () => ({ - config: { - BACKEND_URL: "http://test-api.com", - }, - NETWORK_NAME: "test-network", - // If config is a default export or named export, adjust accordingly. - // Based on usage: import { config, NETWORK_NAME } from '@config/config'; - // The mock above covers named exports. -})); - -describe("ValveIsolation Component", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test("renders input and analyze button", () => { - render(); - expect(screen.getByLabelText(/爆管管段ID/i)).toBeInTheDocument(); - expect(screen.getByText(/开始分析/i)).toBeInTheDocument(); - }); - - test("calls API with correct parameters when analyze is clicked", async () => { - mockedAxios.get.mockResolvedValueOnce({ - data: { - accident_element: "pipe1", - accident_type: "Burst", - affected_nodes: ["node1", "node2"], - must_close_valves: ["valve1"], - optional_valves: [], - isolatable: true, - }, - }); - - render(); - - const input = screen.getByLabelText(/爆管管段ID/i); - fireEvent.change(input, { target: { value: "pipe1" } }); - - const button = screen.getByText(/开始分析/i); - fireEvent.click(button); - - await waitFor(() => { - expect(mockedAxios.get).toHaveBeenCalledWith( - "http://test-api.com/api/v1/valve_isolation_analysis", - { - params: { - network: "test-network", - accident_element: "pipe1", - }, - }, - ); - }); - }); - - test("displays results after successful analysis", async () => { - mockedAxios.get.mockResolvedValueOnce({ - data: { - accident_element: "pipe1", - accident_type: "Burst", - affected_nodes: ["nodeA"], - must_close_valves: ["valveA", "valveB"], - optional_valves: [], - isolatable: true, - }, - }); - - render(); - - fireEvent.change(screen.getByLabelText(/爆管管段ID/i), { - target: { value: "pipe1" }, - }); - fireEvent.click(screen.getByText(/开始分析/i)); - - await waitFor(() => { - expect(screen.getByText("可隔离")).toBeInTheDocument(); - expect(screen.getByText("valveA")).toBeInTheDocument(); - expect(screen.getByText("valveB")).toBeInTheDocument(); - expect(screen.getByText("nodeA")).toBeInTheDocument(); - }); - }); -}); diff --git a/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx b/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx index 129fb50..f9781ca 100644 --- a/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx +++ b/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx @@ -1,117 +1,731 @@ "use client"; -import React, { useState } from 'react'; -import { Box, TextField, Button, Typography, Card, CardContent, Chip } from '@mui/material'; -import axios from 'axios'; -import { config, NETWORK_NAME } from '@config/config'; -import { ValveIsolationResult } from './types'; +import React, { useState, useEffect, useCallback, useRef } from "react"; +import { + Box, + Typography, + Chip, + CircularProgress, + IconButton, + Tooltip, +} from "@mui/material"; +import { LocationOn as LocationIcon } from "@mui/icons-material"; +import axios from "axios"; +import { config, NETWORK_NAME } from "@config/config"; +import { ValveIsolationResult } from "./types"; import { useNotification } from "@refinedev/core"; +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 { Circle as CircleStyle, Fill, Stroke, Style, Icon } from "ol/style"; +import Feature, { FeatureLike } from "ol/Feature"; +import { + bbox, + featureCollection, + along, + lineString, + length, + toMercator, +} from "@turf/turf"; +import { Point } from "ol/geom"; +import { toLonLat } from "ol/proj"; -const ValveIsolation: React.FC = () => { - const [pipeId, setPipeId] = useState(''); - const [loading, setLoading] = useState(false); - const [result, setResult] = useState(null); +interface ValveIsolationProps { + initialPipeIds?: string[]; + shouldFetch?: boolean; + onFetchComplete?: () => void; + loading?: boolean; + result?: ValveIsolationResult | null; + onLoadingChange?: (loading: boolean) => void; + onResultChange?: (result: ValveIsolationResult | null) => void; +} + +const ValveIsolation: React.FC = ({ + initialPipeIds, + shouldFetch = false, + onFetchComplete, + loading: externalLoading, + result: externalResult, + onLoadingChange, + onResultChange, +}) => { + const [internalLoading, setInternalLoading] = useState(false); + const [internalResult, setInternalResult] = + useState(null); + + // 使用外部状态或内部状态 + const loading = + externalLoading !== undefined ? externalLoading : internalLoading; + const result = externalResult !== undefined ? externalResult : internalResult; + const setLoading = onLoadingChange || setInternalLoading; + const setResult = onResultChange || setInternalResult; + const [highlightLayer, setHighlightLayer] = + useState | null>(null); + const [highlightFeatures, setHighlightFeatures] = useState([]); + const [highlightType, setHighlightType] = useState< + "must_close" | "optional" | "affected_node" | "pipe" + >("affected_node"); const { open } = useNotification(); + const lastPipeIdsRef = useRef(""); + const map = useMap(); - const handleAnalyze = async () => { - if (!pipeId.trim()) { - open?.({ type: 'error', message: '请输入管段ID' }); - return; - } - setLoading(true); - setResult(null); - try { - const response = await axios.get(`${config.BACKEND_URL}/api/v1/valve_isolation_analysis`, { - params: { - network: NETWORK_NAME, - accident_element: pipeId + const handleLocatePipes = (pipeIds: string[]) => { + if (pipeIds.length > 0) { + queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => { + if (features.length > 0) { + // 设置高亮类型为管段 + setHighlightType("pipe"); + // 设置高亮要素 + 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 }); + } } }); - setResult(response.data); - open?.({ type: 'success', message: '分析成功' }); - } catch (error) { - console.error(error); - open?.({ type: 'error', message: '分析失败', description: '无法获取关阀分析结果' }); - } finally { - setLoading(false); } }; + const handleLocateNodes = (nodeIds: string[]) => { + if (nodeIds.length > 0) { + queryFeaturesByIds(nodeIds, "geo_junctions").then((features) => { + if (features.length > 0) { + // 设置高亮类型为受影响节点 + setHighlightType("affected_node"); + // 设置高亮要素 + 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 handleLocateMustCloseValves = (valveIds: string[]) => { + if (valveIds.length > 0) { + queryFeaturesByIds(valveIds, "geo_valves").then((features) => { + if (features.length > 0) { + // 设置高亮类型为必关阀门 + setHighlightType("must_close"); + // 设置高亮要素 + 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 handleLocateOptionalValves = (valveIds: string[]) => { + if (valveIds.length > 0) { + queryFeaturesByIds(valveIds, "geo_valves").then((features) => { + if (features.length > 0) { + // 设置高亮类型为可选阀门 + setHighlightType("optional"); + // 设置高亮要素 + 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 fetchAnalysis = useCallback( + async (ids: string[]) => { + if (!ids || ids.length === 0) { + open?.({ type: "error", message: "请提供管段ID" }); + return; + } + setLoading(true); + setResult(null); + try { + const response = await axios.get( + `${config.BACKEND_URL}/api/v1/valve_isolation_analysis/`, + { + params: { + network: NETWORK_NAME, + accident_element: ids, + }, + paramsSerializer: { + indexes: null, // 生成格式: accident_element=P1&accident_element=P2 + }, + }, + ); + setResult(response.data); + open?.({ type: "success", message: "分析成功" }); + } catch (error) { + console.error(error); + open?.({ + type: "error", + message: "分析失败", + description: "无法获取关阀分析结果", + }); + } finally { + setLoading(false); + onFetchComplete?.(); + } + }, + [open, onFetchComplete], + ); + + useEffect(() => { + // 只有在明确要求获取数据时才调用 API + if (shouldFetch && initialPipeIds && initialPipeIds.length > 0) { + // 使用排序后的字符串作为唯一标识,避免数组引用变化导致重复调用 + const pipeIdsKey = [...initialPipeIds].sort().join(","); + + // 只有当 pipeIds 真正改变时才调用 API + if (pipeIdsKey !== lastPipeIdsRef.current) { + lastPipeIdsRef.current = pipeIdsKey; + fetchAnalysis(initialPipeIds); + } else { + // 如果 pipeIds 相同,直接调用完成回调 + onFetchComplete?.(); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shouldFetch, initialPipeIds]); + + // 初始化高亮图层 + useEffect(() => { + if (!map) return; + + // 动态样式函数,根据 highlightType 返回不同的样式 + const getHighlightStyle = (feature: FeatureLike) => { + if (highlightType === "pipe") { + // 管段 - 多层红色线条样式 + 中点图标 + 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; + } + + // 阀门和节点的样式 + let color: string; + let strokeColor: string; + let radius: number; + + switch (highlightType) { + case "must_close": + // 必关阀门 - 深红色 + color = "rgba(211, 47, 47, 0.6)"; + strokeColor = "rgba(211, 47, 47, 1)"; + radius = 10; + break; + case "optional": + // 可选阀门 - 橙色 + color = "rgba(237, 108, 2, 0.6)"; + strokeColor = "rgba(237, 108, 2, 1)"; + radius = 10; + break; + case "affected_node": + default: + // 受影响节点 - 蓝色 + color = "rgba(25, 118, 210, 0.6)"; + strokeColor = "rgba(25, 118, 210, 1)"; + radius = 8; + break; + } + + return new Style({ + image: new CircleStyle({ + radius: radius, + fill: new Fill({ + color: color, + }), + stroke: new Stroke({ + color: strokeColor, + width: 3, + }), + }), + }); + }; + + // 创建高亮图层 + const highlightLayer = new VectorLayer({ + source: new VectorSource(), + style: getHighlightStyle, + maxZoom: 24, + minZoom: 12, + properties: { + name: "阀门节点高亮", + value: "valve_node_highlight", + }, + }); + + map.addLayer(highlightLayer); + setHighlightLayer(highlightLayer); + + return () => { + map.removeLayer(highlightLayer); + }; + }, [map, highlightType]); + + // 高亮要素的函数 + 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]); + return ( - - {/* Input Section */} - - setPipeId(e.target.value)} - placeholder="请输入管段ID" - /> - - - + {/* Results Section */} - {result && ( - - - - - 分析结果 - - - 事故类型: {result.accident_type} - 受影响节点数: {result.affected_nodes?.length || 0} - - - - - 必须关闭阀门 ({result.must_close_valves?.length || 0}) - {result.must_close_valves?.length > 0 ? ( - - {result.must_close_valves.map(v => ( - - ))} - - ) : } + + {loading ? ( + + + 正在分析... + + ) : result ? ( + + {/* 头部:状态信息 */} + + + + 关阀分析结果 + + + + + + + + + 爆管管段 + + + {result.accident_elements && + result.accident_elements.length > 0 && ( + + + handleLocatePipes(result.accident_elements!) + } + sx={{ + backgroundColor: "rgba(255, 0, 0, 0.1)", + "&:hover": { + backgroundColor: "rgba(255, 0, 0, 0.2)", + }, + }} + > + + + + )} + + + {result.accident_elements?.map( + (pipeId: string, idx: number) => ( + handleLocatePipes([pipeId])} + sx={{ + backgroundColor: "rgba(255, 255, 255, 0.9)", + border: "1.5px solid rgb(248, 113, 113)", + color: "rgb(185, 28, 28)", + fontWeight: 600, + fontSize: "0.8rem", + cursor: "pointer", + transition: "all 0.2s", + "&:hover": { + backgroundColor: "rgb(254, 226, 226)", + borderColor: "rgb(220, 38, 38)", + transform: "translateY(-1px)", + boxShadow: "0 2px 4px rgba(220, 38, 38, 0.2)", + }, + }} + /> + ), + )} + + - - 可选关闭阀门 ({result.optional_valves?.length || 0}) - {result.optional_valves?.length > 0 ? ( - - {result.optional_valves.map(v => ( - - ))} - - ) : } + {/* 主要信息:三栏卡片布局 */} + + {/* 必关阀门卡片 */} + + + + + 必关阀门 + + + + {result.must_close_valves?.length || 0} 个 + + + + {/* 可选阀门卡片 */} + + + + + 可选阀门 + + + + {result.optional_valves?.length || 0} 个 + + + + {/* 受影响节点卡片 */} + + + + + 受影响节点 + + + + {result.affected_nodes?.length || 0} 个 + + - - 受影响节点 ({result.affected_nodes?.length || 0}) - {result.affected_nodes?.length > 0 ? ( - - - {result.affected_nodes.join(', ')} + {/* 必须关闭阀门详细列表 */} + {result.must_close_valves && + result.must_close_valves.length > 0 && ( + + + + 必须关闭阀门 + + + + handleLocateMustCloseValves(result.must_close_valves!) + } + color="error" + sx={{ + backgroundColor: "rgba(211, 47, 47, 0.1)", + "&:hover": { + backgroundColor: "rgba(211, 47, 47, 0.2)", + }, + }} + > + + + + + + {result.must_close_valves.map((valveId, idx) => ( + handleLocateMustCloseValves([valveId])} + sx={{ + "&:active": { + transform: "scale(0.98)", + boxShadow: "0 1px 2px rgba(211, 47, 47, 0.2)", + }, + }} + > + + {valveId} + + ))} + + + )} + + {/* 可选关闭阀门详细列表 */} + {result.optional_valves && result.optional_valves.length > 0 && ( + + + + 可选关闭阀门 + + + + handleLocateOptionalValves(result.optional_valves!) + } + color="warning" + sx={{ + backgroundColor: "rgba(237, 108, 2, 0.1)", + "&:hover": { + backgroundColor: "rgba(237, 108, 2, 0.2)", + }, + }} + > + + + + + + {result.optional_valves.map((valveId, idx) => ( + handleLocateOptionalValves([valveId])} + sx={{ + "&:active": { + transform: "scale(0.98)", + boxShadow: "0 1px 2px rgba(237, 108, 2, 0.2)", + }, + }} + > + + {valveId} + - ) : } + ))} + + + )} + + {/* 受影响节点详细列表 */} + {result.affected_nodes && result.affected_nodes.length > 0 && ( + + + + 受影响节点 + + + handleLocateNodes(result.affected_nodes!)} + color="primary" + sx={{ + backgroundColor: "rgba(37, 125, 212, 0.1)", + "&:hover": { + backgroundColor: "rgba(37, 125, 212, 0.2)", + }, + }} + > + + + + + + {result.affected_nodes.map((nodeId, idx) => ( + handleLocateNodes([nodeId])} + sx={{ + "&:active": { + transform: "scale(0.98)", + boxShadow: "0 1px 2px rgba(25, 118, 210, 0.2)", + }, + }} + > + + {nodeId} + + + ))} + + + )} + + ) : ( + + + + + + + + - - )} + 暂无关阀分析结果 + + 请先查看定位结果 + + + )} + ); }; diff --git a/src/components/olmap/BurstPipeAnalysis/types.ts b/src/components/olmap/BurstPipeAnalysis/types.ts index 02a4224..9d8f5d7 100644 --- a/src/components/olmap/BurstPipeAnalysis/types.ts +++ b/src/components/olmap/BurstPipeAnalysis/types.ts @@ -38,8 +38,7 @@ export interface LocationResult { } export interface ValveIsolationResult { - accident_element: string; - accident_type: string; + accident_elements: string[]; affected_nodes: string[]; must_close_valves: string[]; optional_valves: string[]; diff --git a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx index 0dc0c87..a7db304 100644 --- a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx +++ b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx @@ -599,7 +599,7 @@ const SchemeQuery: React.FC = ({ fontWeight: 500, }} > - 定位污染源 + 定位全部污染源