diff --git a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx index 929fa3d..4b00334 100644 --- a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx +++ b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useState } from "react"; import { Box, Drawer, @@ -65,9 +65,6 @@ const BurstPipeAnalysisPanel: React.FC = ({ const [internalOpen, setInternalOpen] = useState(true); const [currentTab, setCurrentTab] = useState(0); const [panelMode, setPanelMode] = useState("burst"); - const previousMapText = useRef<{ junction?: string; pipe?: string } | null>( - null, - ); const data = useData(); @@ -75,10 +72,6 @@ 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); @@ -126,12 +119,6 @@ const BurstPipeAnalysisPanel: React.FC = ({ } }; - const handleAnalyzePipe = (pipeIds: string[]) => { - setSelectedPipeIds(pipeIds); - setValveAnalysisTriggered(true); - setCurrentTab(3); - }; - const drawerWidth = 520; const isBurstMode = panelMode === "burst"; const panelTitle = isBurstMode ? "爆管分析" : "水质模拟"; @@ -308,7 +295,6 @@ const BurstPipeAnalysisPanel: React.FC = ({ {isBurstMode ? ( ) : ( @@ -318,9 +304,6 @@ const BurstPipeAnalysisPanel: React.FC = ({ {isBurstMode && ( setValveAnalysisTriggered(false)} loading={valveAnalysisLoading} result={valveAnalysisResult} onLoadingChange={setValveAnalysisLoading} diff --git a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx index cc5b9d3..e3c2dc7 100644 --- a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx +++ b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx @@ -11,7 +11,6 @@ import { } from "@mui/material"; import { LocationOn as LocationIcon, - Handyman as HandymanIcon, } from "@mui/icons-material"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { useMap } from "@app/OlMap/MapComponent"; @@ -36,12 +35,10 @@ import { LocationResult } from "./types"; interface LocationResultsProps { results?: LocationResult[]; - onAnalyze?: (pipeIds: string[]) => void; } const LocationResults: React.FC = ({ results = [], - onAnalyze, }) => { const [highlightLayer, setHighlightLayer] = useState | null>(null); @@ -349,23 +346,6 @@ const LocationResults: React.FC = ({ 管段列表 - {onAnalyze && ( - - onAnalyze(result.locate_result!)} - color="secondary" - sx={{ - backgroundColor: "rgba(156, 39, 176, 0.1)", - "&:hover": { - backgroundColor: "rgba(156, 39, 176, 0.2)", - }, - }} - > - - - - )} = ({ {pipeId} - {onAnalyze && ( - - { - e.stopPropagation(); - onAnalyze([pipeId]); - }} - className="text-blue-400 hover:text-blue-600" - sx={{ - "&:hover": { - backgroundColor: "rgba(37, 125, 212, 0.1)", - }, - }} - > - - - - )} {/* = ({ - initialPipeIds, + initialPipeIds = [], shouldFetch = false, onFetchComplete, loading: externalLoading, @@ -61,30 +81,93 @@ const ValveIsolation: React.FC = ({ const result = externalResult !== undefined ? externalResult : internalResult; const setLoading = onLoadingChange || setInternalLoading; const setResult = onResultChange || setInternalResult; + + const [selectedPipeId, setSelectedPipeId] = useState(null); + const [highlightFeature, setHighlightFeature] = useState(null); + const [isSelecting, setIsSelecting] = useState(false); + const [activeStep, setActiveStep] = useState(0); + const [expandedResult, setExpandedResult] = useState(true); + const [disabledValves, setDisabledValves] = useState([]); + + const { open } = useNotification(); + const map = useMap(); + + const handleMapClick = useCallback( + async (event: any) => { + if (!isSelecting || !map) return; + + const feature = await handleMapClickSelectFeatures(event, map); + if (feature) { + const pipeId = feature.get("id"); + if (pipeId) { + // 确保是管道 + const layerId = feature.getId()?.toString().split(".")[0] || ""; + const isPipe = layerId.includes("pipe") || layerId.includes("Pipe"); + + if (!isPipe) { + open?.({ + type: "error", + message: "请选择管道类型要素", + }); + return; + } + + setSelectedPipeId(pipeId); + setHighlightFeature(feature); + setIsSelecting(false); + setResult(null); // 清除旧结果 + } + } + }, + [isSelecting, map, open, setResult], + ); + + useEffect(() => { + if (!map) return; + if (isSelecting) { + map.on("click", handleMapClick); + } else { + map.un("click", handleMapClick); + } + + return () => { + map.un("click", handleMapClick); + }; + }, [map, isSelecting, handleMapClick]); + + const clearSelectedPipe = () => { + setSelectedPipeId(null); + setHighlightFeature(null); + setHighlightFeatures([]); + setResult?.(null); + setActiveStep(0); + setExpandedResult(false); + setDisabledValves([]); + }; + 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 handleLocatePipes = (pipeIds: string[]) => { + + const handleLocatePipes = (pipeIds: string[], highlight: boolean = true) => { if (pipeIds.length > 0) { queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => { if (features.length > 0) { - // 设置高亮类型为管段 - setHighlightType("pipe"); - // 设置高亮要素 - setHighlightFeatures(features); + if (highlight) { + // 设置高亮类型为管段 + 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) { @@ -168,28 +251,41 @@ const ValveIsolation: React.FC = ({ }; const fetchAnalysis = useCallback( - async (ids: string[]) => { + async (ids: string[], disabled: string[] = []) => { if (!ids || ids.length === 0) { - open?.({ type: "error", message: "请提供管段ID" }); + open?.({ type: "error", message: "请在地图上选择要分析的管段" }); return; } setLoading(true); - setResult(null); + const isExpandSearch = disabled.length > 0; + if (!isExpandSearch) { + setResult(null); + setDisabledValves([]); + } try { + const params: any = { + network: NETWORK_NAME, + accident_element: ids, + }; + if (disabled.length > 0) { + params.disabled_valves = disabled; + } const response = await axios.get( `${config.BACKEND_URL}/api/v1/valve_isolation_analysis/`, { - params: { - network: NETWORK_NAME, - accident_element: ids, - }, + params, paramsSerializer: { - indexes: null, // 生成格式: accident_element=P1&accident_element=P2 + indexes: null, // 生成格式: accident_element=P1&accident_element=P2&disabled_valves=V1&disabled_valves=V2 }, }, ); setResult(response.data); - open?.({ type: "success", message: "分析成功" }); + if (!isExpandSearch) { + setActiveStep(1); + } else { + setActiveStep(2); + } + open?.({ type: "success", message: isExpandSearch ? "扩大搜索成功" : "分析成功" }); } catch (error) { console.error(error); open?.({ @@ -199,36 +295,95 @@ const ValveIsolation: React.FC = ({ }); } finally { setLoading(false); - onFetchComplete?.(); } }, - [open, onFetchComplete], + [open, setLoading, setResult], ); + // 监听外部传入的分析请求 useEffect(() => { - // 只有在明确要求获取数据时才调用 API - if (shouldFetch && initialPipeIds && initialPipeIds.length > 0) { - // 使用排序后的字符串作为唯一标识,避免数组引用变化导致重复调用 - const pipeIdsKey = [...initialPipeIds].sort().join(","); + if (shouldFetch && initialPipeIds.length > 0) { + // 这里简单地取第一个作为 selectedPipeId,实际 fetchAnalysis 支持数组 + setSelectedPipeId(initialPipeIds[0]); - // 只有当 pipeIds 真正改变时才调用 API - if (pipeIdsKey !== lastPipeIdsRef.current) { - lastPipeIdsRef.current = pipeIdsKey; - fetchAnalysis(initialPipeIds); - } else { - // 如果 pipeIds 相同,直接调用完成回调 - onFetchComplete?.(); + // 尝试获取Feature以高亮 (可选) + queryFeaturesByIds(initialPipeIds, "geo_pipes_mat").then((features) => { + if (features && features.length > 0) { + setHighlightFeature(features[0]); + } + }); + + fetchAnalysis(initialPipeIds); + + if (onFetchComplete) { + onFetchComplete(); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldFetch, initialPipeIds]); + }, [shouldFetch, initialPipeIds, fetchAnalysis, onFetchComplete]); // 初始化高亮图层 useEffect(() => { if (!map) return; - // 动态样式函数,根据 highlightType 返回不同的样式 + // 动态样式函数,根据 highlightType 和 selectedPipeId 返回不同的样式 const getHighlightStyle = (feature: FeatureLike) => { + // 如果是当前选择的爆管点feature + if (highlightFeature && feature === highlightFeature) { + const styles = []; + // 线条样式(底层发光,主线条,内层高亮线) + styles.push( + new Style({ + stroke: new Stroke({ + color: "rgba(255, 0, 0, 0.3)", + width: 14, + }), + }), + new Style({ + stroke: new Stroke({ + color: "rgba(255, 0, 0, 1)", + width: 7, + lineDash: [15, 10], + }), + }), + new Style({ + stroke: new Stroke({ + color: "rgba(255, 102, 102, 1)", + width: 4, + 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.25, + anchor: [0.5, 1], + }), + }), + ); + } + return styles; + } + if (highlightType === "pipe") { // 管段 - 多层红色线条样式 + 中点图标 const styles = []; @@ -345,7 +500,7 @@ const ValveIsolation: React.FC = ({ return () => { map.removeLayer(highlightLayer); }; - }, [map, highlightType]); + }, [map, highlightType, highlightFeature]); // 高亮要素的函数 useEffect(() => { @@ -358,373 +513,590 @@ const ValveIsolation: React.FC = ({ } // 清除之前的高亮 source.clear(); - // 添加新的高亮要素 + + // 如果有选中的爆管点(pipe),优先添加到source + if (highlightFeature) { + // 设置一个特殊的属性来区分 + highlightFeature.set('isHighlightPipe', true); + source.addFeature(highlightFeature); + } + + // 添加其他高亮要素 highlightFeatures.forEach((feature) => { if (feature instanceof Feature) { source.addFeature(feature); } }); - }, [highlightFeatures, highlightLayer]); - return ( - - {/* Results Section */} - - {loading ? ( - - - 正在分析... + }, [highlightFeatures, highlightLayer, highlightFeature]); + + // 切换不可用阀门的选择状态 + const toggleDisabledValve = (valveId: string) => { + setDisabledValves((prev) => { + if (prev.includes(valveId)) { + return prev.filter((id) => id !== valveId); + } else { + return [...prev, valveId]; + } + }); + }; + + // 渲染结果卡片 + const renderResultCard = (isExpanded: boolean = false, allowSelectDisabled: boolean = false) => { + if (!result) return null; + + return ( + + {/* 状态信息 */} + + + + + {isExpanded ? "扩大搜索结果" : "分析结果"} + + + - ) : result ? ( - - {/* 头部:状态信息 */} - - - - 关阀分析结果 - + + {/* 事故管段 */} + + + + + 目标事故管段 + + {/* {result.accident_elements && result.accident_elements.length > 0 && ( + + handleLocatePipes(result.accident_elements!)} + sx={{ color: "rgb(220, 38, 38)", padding: "2px" }} + > + + + + )} */} + + + {result.accident_elements?.map((pipeId, idx) => ( handleLocatePipes([pipeId], false)} sx={{ + backgroundColor: "rgb(254, 242, 242)", + border: "1px solid rgb(252, 165, 165)", + color: "rgb(185, 28, 28)", fontWeight: 600, - fontSize: "0.75rem", - height: "24px", + "&:hover": { + backgroundColor: "rgb(254, 226, 226)", + borderColor: "rgb(239, 68, 68)", + }, }} /> - - - - - - - 爆管管段 - - - {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.must_close_valves?.length || 0} 个 + {/* 统计概览 */} + + {[ + { label: "必关阀门", value: result.must_close_valves?.length || 0, color: "red", bgInfo: "from-red-50 to-red-100", textInfo: "text-red-700" }, + { label: "可选阀门", value: result.optional_valves?.length || 0, color: "orange", bgInfo: "from-orange-50 to-orange-100", textInfo: "text-orange-700" }, + { label: "影响节点", value: result.affected_nodes?.length || 0, color: "blue", bgInfo: "from-blue-50 to-blue-100", textInfo: "text-blue-700" }, + ].map((item, index) => ( + + + {item.value} + + + {item.label} + ))} + + - {/* 可选阀门卡片 */} - - - - - 可选阀门 - - - - {result.optional_valves?.length || 0} 个 - - + {/* 必关阀门选择提示 - 只在流程2显示 */} + {allowSelectDisabled && result.must_close_valves && result.must_close_valves.length > 0 && ( + + + 可选择无法关闭的阀门 + + + 点击下方必关阀门列表中的阀门进行选择,已选择的阀门将在扩大搜索时作为不可用阀门处理 + + + )} - {/* 受影响节点卡片 */} - - - - - 受影响节点 - - - - {result.affected_nodes?.length || 0} 个 - - - + {/* 详细列表 - 可折叠 */} + + - {/* 必须关闭阀门详细列表 */} - {result.must_close_valves && - result.must_close_valves.length > 0 && ( - - - - 必须关闭阀门 + {expandedResult && ( + + {/* 必关阀门 */} + {result.must_close_valves && result.must_close_valves.length > 0 && ( + + + + 必关阀门列表 ({result.must_close_valves.length}) + {allowSelectDisabled && " - 点击勾选不可用阀门"} - + - handleLocateMustCloseValves(result.must_close_valves!) - } - color="error" + onClick={() => handleLocateMustCloseValves(result.must_close_valves!)} sx={{ + color: "rgb(211, 47, 47)", backgroundColor: "rgba(211, 47, 47, 0.1)", "&:hover": { backgroundColor: "rgba(211, 47, 47, 0.2)", }, }} > - + - - {result.must_close_valves.map((valveId, idx) => ( - handleLocateMustCloseValves([valveId])} + + {result.must_close_valves.map((valveId, idx) => { + const isSelected = disabledValves.includes(valveId); + return ( + handleLocateMustCloseValves([valveId])} + sx={{ + "&:active": { + transform: "scale(0.98)", + }, + }} + > + + + {valveId} + + {allowSelectDisabled && ( + { + e.stopPropagation(); + toggleDisabledValve(valveId); + }} + className="cursor-pointer" + > + {isSelected ? ( + + ) : ( + + )} + + )} + + + ); + })} + + + )} + + {/* 可选阀门 */} + {result.optional_valves && result.optional_valves.length > 0 && ( + + + + 可选阀门列表 ({result.optional_valves.length}) + + + handleLocateOptionalValves(result.optional_valves!)} sx={{ - "&:active": { - transform: "scale(0.98)", - boxShadow: "0 1px 2px rgba(211, 47, 47, 0.2)", + color: "rgb(237, 108, 2)", + backgroundColor: "rgba(237, 108, 2, 0.1)", + "&:hover": { + backgroundColor: "rgba(237, 108, 2, 0.2)", }, }} > - - {valveId} - + + + + + + {result.optional_valves.map((valveId, idx) => ( + handleLocateOptionalValves([valveId])} + sx={{ + "&:active": { + transform: "scale(0.98)", + }, + }} + > + + + {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)", - }, - }} - > - 0 && ( + + + + 受影响节点 ({result.affected_nodes.length}) + + + handleLocateNodes(result.affected_nodes!)} + sx={{ + color: "rgb(25, 118, 210)", + backgroundColor: "rgba(25, 118, 210, 0.1)", + "&:hover": { + backgroundColor: "rgba(25, 118, 210, 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)", - }, - }} - > - + + + + + {result.affected_nodes.map((nodeId, idx) => ( + handleLocateNodes([nodeId])} + sx={{ + "&:active": { + transform: "scale(0.98)", + }, + }} > - {nodeId} - - - ))} + + + {nodeId} + + + + ))} + - - )} - - ) : ( - - - - - - - - + )} - 暂无关阀分析结果 - - 请先查看定位结果 - - - )} + )} + + + ); + }; + + return ( + + {/* 流程区域 */} + + + {/* 流程1:选择管段并分析 */} + + ( + = 0 + ? "bg-blue-600 text-white" + : "bg-gray-300 text-gray-600" + }`} + > + 1 + + )} + > + + 选择管段并进行分析 + + + + + {/* 选择管段 */} + + + + 选择爆管管段 + + {!isSelecting ? ( + + ) : ( + + )} + + + {isSelecting && ( + + 💡 点击地图上的管道添加爆管点 + + )} + + {selectedPipeId ? ( + + + + {selectedPipeId} + + + 已选择 + + + + + + ) : ( + + 暂未选择管段 + + )} + + + {/* 操作按钮 */} + + + + + + + + {/* 流程2:查看分析结果 */} + + ( + = 1 + ? "bg-blue-600 text-white" + : "bg-gray-300 text-gray-600" + }`} + > + 2 + + )} + > + + 查看分析结果 + + + + + {loading ? ( + + + + 分析计算中,请稍候... + + + ) : result ? ( + <> + {renderResultCard(false, true)} + + {/* 操作按钮 */} + + + + + + ) : ( + + 请先完成流程1的分析操作 + + )} + + + + + {/* 流程3:扩大搜索结果 */} + + ( + = 2 + ? "bg-blue-600 text-white" + : "bg-gray-300 text-gray-600" + }`} + > + 3 + + )} + > + + 扩大搜索结果(可选) + + + + + {loading ? ( + + + + 扩大搜索中,请稍候... + + + ) : activeStep >= 2 && result ? ( + <> + {renderResultCard(true, false)} + + {/* 最终结果提示 */} + {result.isolatable ? ( + + + ✓ 扩大搜索成功,找到可行的隔离方案 + + + 已标记 {disabledValves.length} 个阀门为不可用状态,可以按照上述新的阀门配置进行隔离操作 + + + ) : ( + + + ✗ 扩大搜索后仍无法完全隔离 + + + 即使排除了 {disabledValves.length} 个不可用阀门,仍无法找到有效隔离方案。建议检查管网拓扑结构或阀门配置 + + + )} + + {/* 操作按钮 */} + + + + + + ) : ( + + 请先在流程2中选择不可用阀门,然后点击"扩大搜索"按钮 + + )} + + + + );