Files
TJWaterFrontend_Refine/src/components/olmap/BurstSimulation/ValveIsolation.tsx
T

1257 lines
45 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback } from "react";
import {
Box,
Typography,
Chip,
CircularProgress,
IconButton,
Tooltip,
Button,
Stack,
Divider,
Stepper,
Step,
StepLabel,
StepContent,
Paper,
Alert,
} from "@mui/material";
import {
LocationOn as LocationIcon,
Close as CloseIcon,
CheckCircle as CheckCircleIcon,
Search as SearchIcon,
ExpandMore as ExpandMoreIcon,
CheckBox as CheckBoxIcon,
CheckBoxOutlineBlank as CheckBoxOutlineBlankIcon,
} from "@mui/icons-material";
import { api } from "@/lib/api";
import { config, NETWORK_NAME } from "@config/config";
import { ValveIsolationResult } from "./types";
import { useNotification } from "@refinedev/core";
import {
queryFeaturesByIds,
handleMapClickSelectFeatures,
} from "@/utils/mapQueryService";
import { useMap } from "@components/olmap/core/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";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
import {
getAffectedNodeCount,
isAllowedAccidentPipe,
normalizeValveIsolationResult,
} from "./valveIsolationScope";
interface ValveIsolationProps {
initialPipeIds?: string[];
allowedPipeIds?: string[];
sourceSchemeName?: string;
shouldFetch?: boolean;
onFetchComplete?: () => void;
loading?: boolean;
result?: ValveIsolationResult | null;
onLoadingChange?: (loading: boolean) => void;
onResultChange?: (result: ValveIsolationResult | null) => void;
state?: ValveIsolationState;
onStateChange?: (state: ValveIsolationState) => void;
}
export interface ValveIsolationState {
selectedPipeId: string | null;
activeStep: number;
expandedResult: boolean;
disabledValves: string[];
}
export const createValveIsolationState = (): ValveIsolationState => ({
selectedPipeId: null,
activeStep: 0,
expandedResult: true,
disabledValves: [],
});
const ValveIsolation: React.FC<ValveIsolationProps> = ({
initialPipeIds = [],
allowedPipeIds,
sourceSchemeName,
shouldFetch = false,
onFetchComplete,
loading: externalLoading,
result: externalResult,
onLoadingChange,
onResultChange,
state,
onStateChange,
}) => {
const [internalLoading, setInternalLoading] = useState(false);
const [internalResult, setInternalResult] =
useState<ValveIsolationResult | null>(null);
// 使用外部状态或内部状态
const loading =
externalLoading !== undefined ? externalLoading : internalLoading;
const result = externalResult !== undefined ? externalResult : internalResult;
const setLoading = onLoadingChange || setInternalLoading;
const setResult = onResultChange || setInternalResult;
const [flowState, setFlowState, setFlowField] = useControllableObjectState(
state,
onStateChange,
createValveIsolationState(),
);
const { selectedPipeId, activeStep, expandedResult, disabledValves } =
flowState;
const [highlightFeature, setHighlightFeature] = useState<Feature | null>(null);
const [isSelecting, setIsSelecting] = useState(false);
const setSelectedPipeId = useCallback(
(value: string | null) => setFlowField("selectedPipeId", value),
[setFlowField],
);
const setActiveStep = useCallback(
(value: number) => setFlowField("activeStep", value),
[setFlowField],
);
const setExpandedResult = useCallback(
(value: boolean) => setFlowField("expandedResult", value),
[setFlowField],
);
const setDisabledValves = useCallback(
(next: string[] | ((previous: string[]) => string[])) => {
setFlowState((previous) => ({
...previous,
disabledValves:
typeof next === "function" ? next(previous.disabledValves) : next,
}));
},
[setFlowState],
);
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;
}
const normalizedPipeId = String(pipeId);
if (!isAllowedAccidentPipe(normalizedPipeId, allowedPipeIds)) {
open?.({
type: "error",
message: "请选择当前爆管方案中的事故管段",
});
return;
}
setSelectedPipeId(normalizedPipeId);
setHighlightFeature(feature);
setIsSelecting(false);
setResult(null); // 清除旧结果
}
}
},
[
allowedPipeIds,
isSelecting,
map,
open,
setResult,
setSelectedPipeId,
],
);
useEffect(() => {
if (!map) return;
if (isSelecting) {
map.on("click", handleMapClick);
} else {
map.un("click", handleMapClick);
}
return () => {
map.un("click", handleMapClick);
};
}, [map, isSelecting, handleMapClick]);
useEffect(() => {
if (!selectedPipeId || highlightFeature) return;
queryFeaturesByIds([selectedPipeId], "geo_pipes_mat").then((features) => {
setHighlightFeature(features[0] ?? null);
});
}, [highlightFeature, selectedPipeId]);
const clearSelectedPipe = () => {
setSelectedPipeId(null);
setHighlightFeature(null);
setHighlightFeatures([]);
setResult?.(null);
setActiveStep(0);
setExpandedResult(false);
setDisabledValves([]);
};
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [highlightType, setHighlightType] = useState<
"must_close" | "optional" | "affected_node" | "pipe"
>("affected_node");
const selectSchemeAccidentPipe = (pipeId: string) => {
if (!isAllowedAccidentPipe(pipeId, allowedPipeIds)) return;
setSelectedPipeId(pipeId);
setHighlightFeature(null);
setHighlightFeatures([]);
setResult(null);
setActiveStep(0);
setExpandedResult(true);
setDisabledValves([]);
};
const handleLocatePipes = (pipeIds: string[], highlight: boolean = true) => {
if (pipeIds.length > 0) {
queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => {
if (features.length > 0) {
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) {
map?.getView().fit(extent, { maxZoom: 18, duration: 1000 });
}
}
});
}
};
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[], disabled: string[] = []) => {
if (!ids || ids.length === 0) {
open?.({ type: "error", message: "请在地图上选择要分析的管段" });
return;
}
if (ids.some((pipeId) => !isAllowedAccidentPipe(pipeId, allowedPipeIds))) {
open?.({
type: "error",
message: "关阀分析仅限当前爆管方案中的事故管段",
});
return;
}
setLoading(true);
const isExpandSearch = disabled.length > 0;
if (!isExpandSearch) {
setResult(null);
setDisabledValves([]);
}
try {
const params: any = {
accident_element: ids,
};
if (disabled.length > 0) {
params.disabled_valves = disabled;
}
const response = await api.post<ValveIsolationResult>(
`${config.BACKEND_URL}/api/v1/valve-isolation-analyses`,
undefined,
{
params,
paramsSerializer: {
indexes: null, // 生成格式: accident_element=P1&accident_element=P2&disabled_valves=V1&disabled_valves=V2
},
},
);
setResult(normalizeValveIsolationResult(response.data));
if (!isExpandSearch) {
setActiveStep(1);
} else {
setActiveStep(2);
}
open?.({ type: "success", message: isExpandSearch ? "扩大搜索成功" : "分析成功" });
} catch (error) {
console.error(error);
open?.({
type: "error",
message: "分析失败",
description: "无法获取关阀分析结果",
});
} finally {
setLoading(false);
}
},
[
allowedPipeIds,
open,
setActiveStep,
setDisabledValves,
setLoading,
setResult,
],
);
// 监听外部传入的分析请求
useEffect(() => {
if (shouldFetch && initialPipeIds.length > 0) {
// 这里简单地取第一个作为 selectedPipeId,实际 fetchAnalysis 支持数组
setSelectedPipeId(initialPipeIds[0]);
// 尝试获取Feature以高亮 (可选)
queryFeaturesByIds(initialPipeIds, "geo_pipes_mat").then((features) => {
if (features && features.length > 0) {
setHighlightFeature(features[0]);
}
});
fetchAnalysis(initialPipeIds);
if (onFetchComplete) {
onFetchComplete();
}
}
}, [shouldFetch, initialPipeIds, fetchAnalysis, onFetchComplete, setSelectedPipeId]);
// 初始化高亮图层
useEffect(() => {
if (!map) return;
// 动态样式函数,根据 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 = [];
// 线条样式(底层发光,主线条,内层高亮线)
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",
queryable: false,
},
});
map.addLayer(highlightLayer);
setHighlightLayer(highlightLayer);
return () => {
map.removeLayer(highlightLayer);
};
}, [map, highlightType, highlightFeature]);
// 高亮要素的函数
useEffect(() => {
if (!highlightLayer) {
return;
}
const source = highlightLayer.getSource();
if (!source) {
return;
}
// 清除之前的高亮
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, 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 (
<Box className="space-y-4">
{/* 状态信息 */}
<Paper elevation={0} className="p-4 bg-gradient-to-r from-blue-50 to-purple-50 border border-blue-200">
<Box className="flex items-center justify-between mb-2">
<Box className="flex items-center gap-2">
<Typography variant="subtitle1" className="font-bold text-gray-900">
{isExpanded ? "扩大搜索结果" : "分析结果"}
</Typography>
<Chip
label={result.isolatable ? "可隔离" : "不可隔离"}
size="small"
color={result.isolatable ? "success" : "error"}
variant="filled"
sx={{ fontWeight: 700, borderRadius: "6px" }}
/>
</Box>
</Box>
{/* 事故管段 */}
<Box className="bg-white rounded-lg p-3 border border-red-100 mb-3">
<Box className="flex items-center justify-between mb-2">
<Typography variant="caption" className="text-red-800 font-bold flex items-center gap-1.5">
<Box component="span" className="w-1.5 h-1.5 rounded-full bg-red-600 inline-block" />
目标事故管段
</Typography>
{/* {result.accident_elements && result.accident_elements.length > 0 && (
<Tooltip title="定位全部">
<IconButton
size="small"
onClick={() => handleLocatePipes(result.accident_elements!)}
sx={{ color: "rgb(220, 38, 38)", padding: "2px" }}
>
<LocationIcon fontSize="small" />
</IconButton>
</Tooltip>
)} */}
</Box>
<Box className="flex flex-wrap gap-2">
{result.accident_elements?.map((pipeId, idx) => (
<Chip
key={idx}
label={pipeId}
size="small"
onClick={() => handleLocatePipes([pipeId], false)}
sx={{
backgroundColor: "rgb(254, 242, 242)",
border: "1px solid rgb(252, 165, 165)",
color: "rgb(185, 28, 28)",
fontWeight: 600,
"&:hover": {
backgroundColor: "rgb(254, 226, 226)",
borderColor: "rgb(239, 68, 68)",
},
}}
/>
))}
</Box>
</Box>
{/* 统计概览 */}
<Box className="grid grid-cols-3 gap-2">
{[
{ 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: getAffectedNodeCount(result), color: "blue", bgInfo: "from-blue-50 to-blue-100", textInfo: "text-blue-700" },
].map((item, index) => (
<Box
key={index}
className={`bg-gradient-to-br ${item.bgInfo} rounded-lg p-2 border border-${item.color}-200 text-center`}
>
<Typography variant="h6" className={`font-black ${item.textInfo}`}>
{item.value}
</Typography>
<Typography variant="caption" className={`font-semibold opacity-75 ${item.textInfo}`}>
{item.label}
</Typography>
</Box>
))}
</Box>
</Paper>
{/* 必关阀门选择提示 - 只在流程2显示 */}
{allowSelectDisabled && result.must_close_valves && result.must_close_valves.length > 0 && (
<Alert severity="info" variant="outlined">
<Typography variant="body2" className="font-semibold mb-1">
可选择无法关闭的阀门
</Typography>
<Typography variant="caption" className="text-gray-600">
点击下方必关阀门列表中的阀门进行选择,已选择的阀门将在扩大搜索时作为不可用阀门处理
</Typography>
</Alert>
)}
{/* 详细列表 - 可折叠 */}
<Box>
<Button
fullWidth
variant="outlined"
onClick={() => setExpandedResult(!expandedResult)}
endIcon={<ExpandMoreIcon sx={{ transform: expandedResult ? 'rotate(180deg)' : 'rotate(0)', transition: 'transform 0.3s' }} />}
sx={{ mb: 2 }}
>
{expandedResult ? "收起详细信息" : "查看详细信息"}
</Button>
{expandedResult && (
<Box className="flex flex-col gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
{/* 必关阀门 */}
{result.must_close_valves && result.must_close_valves.length > 0 && (
<Box>
<Box className="flex items-center justify-between mb-2">
<Typography variant="caption" className="text-gray-800 font-bold border-l-4 border-red-500 pl-2">
必关阀门列表 ({result.must_close_valves.length})
{allowSelectDisabled && " - 点击勾选不可用阀门"}
</Typography>
<Tooltip title="定位全部">
<IconButton
size="small"
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)",
},
}}
>
<LocationIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
<Box className="grid grid-cols-2 gap-2">
{result.must_close_valves.map((valveId, idx) => {
const isSelected = disabledValves.includes(valveId);
return (
<Box
key={idx}
className={`bg-gradient-to-r rounded-lg px-3 py-2 border transition-all cursor-pointer group ${allowSelectDisabled && isSelected
? "from-orange-50 to-white border-orange-500 hover:shadow-md"
: "from-red-50 to-white border-red-200 hover:border-red-400 hover:shadow-md"
}`}
onClick={() => handleLocateMustCloseValves([valveId])}
sx={{
"&:active": {
transform: "scale(0.98)",
},
}}
>
<Box className="flex items-center justify-between">
<Typography
variant="body2"
className={`font-semibold ${allowSelectDisabled && isSelected
? "text-orange-700 group-hover:text-orange-900"
: "text-red-700 group-hover:text-red-900"
}`}
>
{valveId}
</Typography>
{allowSelectDisabled && (
<Box
onClick={(e) => {
e.stopPropagation();
toggleDisabledValve(valveId);
}}
className="cursor-pointer"
>
{isSelected ? (
<CheckBoxIcon fontSize="small" className="text-orange-600" />
) : (
<CheckBoxOutlineBlankIcon fontSize="small" className="text-gray-400" />
)}
</Box>
)}
</Box>
</Box>
);
})}
</Box>
</Box>
)}
{/* 可选阀门 */}
{result.optional_valves && result.optional_valves.length > 0 && (
<Box>
<Box className="flex items-center justify-between mb-2">
<Typography variant="caption" className="text-gray-800 font-bold border-l-4 border-orange-500 pl-2">
可选阀门列表 ({result.optional_valves.length})
</Typography>
<Tooltip title="定位全部">
<IconButton
size="small"
onClick={() => handleLocateOptionalValves(result.optional_valves!)}
sx={{
color: "rgb(237, 108, 2)",
backgroundColor: "rgba(237, 108, 2, 0.1)",
"&:hover": {
backgroundColor: "rgba(237, 108, 2, 0.2)",
},
}}
>
<LocationIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
<Box className="grid grid-cols-2 gap-2">
{result.optional_valves.map((valveId, idx) => (
<Box
key={idx}
className="bg-gradient-to-r from-orange-50 to-white rounded-lg px-3 py-2 border border-orange-200 hover:border-orange-400 hover:shadow-md transition-all cursor-pointer group"
onClick={() => handleLocateOptionalValves([valveId])}
sx={{
"&:active": {
transform: "scale(0.98)",
},
}}
>
<Box className="flex items-center justify-between">
<Typography
variant="body2"
className="font-semibold text-orange-700 group-hover:text-orange-900"
>
{valveId}
</Typography>
</Box>
</Box>
))}
</Box>
</Box>
)}
{!result.isolatable && (
<Alert severity="info" variant="outlined">
不可隔离,未生成受影响节点清单。
</Alert>
)}
{/* 受影响节点 */}
{result.isolatable && result.affected_nodes && result.affected_nodes.length > 0 && (
<Box>
<Box className="flex items-center justify-between mb-2">
<Typography variant="caption" className="text-gray-800 font-bold border-l-4 border-blue-500 pl-2">
受影响节点 ({result.affected_nodes.length})
</Typography>
<Tooltip title="定位全部">
<IconButton
size="small"
onClick={() => 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)",
},
}}
>
<LocationIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
<Box className="grid grid-cols-2 gap-2">
{result.affected_nodes.map((nodeId, idx) => (
<Box
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={() => handleLocateNodes([nodeId])}
sx={{
"&:active": {
transform: "scale(0.98)",
},
}}
>
<Box className="flex items-center justify-between">
<Typography
variant="body2"
className="font-semibold text-blue-700 group-hover:text-blue-900"
>
{nodeId}
</Typography>
</Box>
</Box>
))}
</Box>
</Box>
)}
</Box>
)}
</Box>
</Box>
);
};
return (
<Box className="flex flex-col h-full bg-gray-50">
{/* 流程区域 */}
<Box className="flex-1 overflow-auto p-4">
<Stepper activeStep={activeStep} orientation="vertical">
{/* 流程1:选择管段并分析 */}
<Step>
<StepLabel
StepIconComponent={() => (
<Box
className={`w-8 h-8 rounded-full flex items-center justify-center font-bold ${activeStep >= 0
? "bg-blue-600 text-white"
: "bg-gray-300 text-gray-600"
}`}
>
1
</Box>
)}
>
<Typography variant="subtitle1" className="font-bold">
选择管段并进行分析
</Typography>
</StepLabel>
<StepContent>
<Box className="ml-4 pl-4 border-l-2 border-gray-200 space-y-3">
{allowedPipeIds !== undefined ? (
<Alert severity="info" variant="outlined">
已绑定爆管方案“{sourceSchemeName || "未命名方案"}”。关阀分析仅限该方案中的事故管段,分析结果将写入对应报告。
</Alert>
) : (
<Alert severity="warning" variant="outlined">
当前为独立关阀分析,结果不会自动写入爆管分析报告。请先从“方案查询”打开对应分析报告以建立关联。
</Alert>
)}
{/* 选择管段 */}
<Paper elevation={0} className="p-3 bg-white border border-gray-200">
<Box className="flex items-center justify-between mb-2">
<Typography variant="subtitle2" className="font-medium text-gray-700">
选择爆管管段
</Typography>
{!isSelecting ? (
<Button
variant="outlined"
size="small"
onClick={() => setIsSelecting(true)}
className="border-blue-500 text-blue-600 hover:bg-blue-50"
startIcon={
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
/>
</svg>
}
>
开始选择
</Button>
) : (
<Button
variant="contained"
size="small"
onClick={() => setIsSelecting(false)}
className="bg-red-500 hover:bg-red-600"
startIcon={<CloseIcon />}
>
结束选择
</Button>
)}
</Box>
{allowedPipeIds !== undefined && (
<Box className="mb-3 rounded border border-blue-100 bg-blue-50 p-2">
<Box className="mb-2 flex flex-wrap gap-1.5">
{allowedPipeIds.map((pipeId) => (
<Chip
key={pipeId}
label={pipeId}
size="small"
color={selectedPipeId === pipeId ? "primary" : "default"}
variant={selectedPipeId === pipeId ? "filled" : "outlined"}
onClick={() => selectSchemeAccidentPipe(pipeId)}
/>
))}
</Box>
<Button
variant="outlined"
size="small"
fullWidth
disabled={allowedPipeIds.length === 0}
onClick={() =>
allowedPipeIds[0] &&
selectSchemeAccidentPipe(allowedPipeIds[0])
}
startIcon={<CheckCircleIcon />}
>
选择当前爆管管段
</Button>
</Box>
)}
{isSelecting && (
<Box className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
💡 点击地图上的管道添加爆管点
</Box>
)}
{selectedPipeId ? (
<Box className="flex items-center gap-2 p-2 bg-green-50 rounded border border-green-200">
<CheckCircleIcon className="text-green-600" fontSize="small" />
<Typography className="flex-1 text-sm font-medium pl-1 text-gray-700">
{selectedPipeId}
</Typography>
<Typography className="text-xs text-gray-500">
已选择
</Typography>
<IconButton
size="small"
onClick={clearSelectedPipe}
>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
) : (
<Box className="p-3 rounded border border-dashed border-gray-300 text-sm text-gray-400 text-center">
暂未选择管段
</Box>
)}
</Paper>
{/* 操作按钮 */}
<Box className="flex gap-2 pt-2">
<Button
variant="contained"
size="medium"
color="primary"
disabled={loading || !selectedPipeId}
onClick={() => selectedPipeId && fetchAnalysis([selectedPipeId], [])}
className="flex-1"
startIcon={loading ? <CircularProgress size={16} /> : <SearchIcon />}
>
{loading ? "正在分析..." : "开始分析"}
</Button>
</Box>
</Box>
</StepContent>
</Step>
{/* 流程2:查看分析结果 */}
<Step>
<StepLabel
StepIconComponent={() => (
<Box
className={`w-8 h-8 rounded-full flex items-center justify-center font-bold ${activeStep >= 1
? "bg-blue-600 text-white"
: "bg-gray-300 text-gray-600"
}`}
>
2
</Box>
)}
>
<Typography variant="subtitle1" className="font-bold">
查看分析结果
</Typography>
</StepLabel>
<StepContent>
<Box className="ml-4 pl-4 border-l-2 border-gray-200 space-y-3">
{loading ? (
<Box className="flex flex-col items-center justify-center py-8 text-gray-500">
<CircularProgress size={40} className="mb-4 text-blue-600" />
<Typography variant="body2" className="text-gray-600 font-medium">
分析计算中,请稍候...
</Typography>
</Box>
) : result ? (
<>
{renderResultCard(false, true)}
{/* 操作按钮 */}
<Box className="flex gap-2 pt-2">
<Button
variant="outlined"
size="medium"
onClick={() => {
clearSelectedPipe();
}}
className="flex-1"
>
重新选择管段
</Button>
<Button
variant="contained"
size="medium"
color="warning"
disabled={loading || disabledValves.length === 0}
onClick={() => selectedPipeId && fetchAnalysis([selectedPipeId], disabledValves)}
className="flex-1"
startIcon={loading ? <CircularProgress size={16} /> : <SearchIcon />}
>
扩大搜索 {disabledValves.length > 0 && `(${disabledValves.length})`}
</Button>
</Box>
</>
) : (
<Alert severity="info" variant="outlined">
请先完成流程1的分析操作
</Alert>
)}
</Box>
</StepContent>
</Step>
{/* 流程3:扩大搜索结果 */}
<Step>
<StepLabel
StepIconComponent={() => (
<Box
className={`w-8 h-8 rounded-full flex items-center justify-center font-bold ${activeStep >= 2
? "bg-blue-600 text-white"
: "bg-gray-300 text-gray-600"
}`}
>
3
</Box>
)}
>
<Typography variant="subtitle1" className="font-bold">
扩大搜索结果(可选)
</Typography>
</StepLabel>
<StepContent>
<Box className="ml-4 pl-4 border-l-2 border-gray-200 space-y-3">
{loading ? (
<Box className="flex flex-col items-center justify-center py-8 text-gray-500">
<CircularProgress size={40} className="mb-4 text-orange-600" />
<Typography variant="body2" className="text-gray-600 font-medium">
扩大搜索中,请稍候...
</Typography>
</Box>
) : activeStep >= 2 && result ? (
<>
{renderResultCard(true, false)}
{/* 最终结果提示 */}
{result.isolatable ? (
<Alert severity="success" variant="outlined">
<Typography variant="body2" className="font-semibold mb-1">
扩大搜索成功,找到可行的隔离方案
</Typography>
<Typography variant="caption" className="text-gray-600">
已标记 {disabledValves.length} 个阀门为不可用状态,可以按照上述新的阀门配置进行隔离操作
</Typography>
</Alert>
) : (
<Alert severity="error" variant="outlined">
<Typography variant="body2" className="font-semibold mb-1">
扩大搜索后仍无法完全隔离
</Typography>
<Typography variant="caption" className="text-gray-600">
即使排除了 {disabledValves.length} 个不可用阀门,仍无法找到有效隔离方案。建议检查管网拓扑结构或阀门配置
</Typography>
</Alert>
)}
{/* 操作按钮 */}
<Box className="flex gap-2 pt-2">
<Button
variant="outlined"
size="medium"
onClick={() => {
setActiveStep(1);
setDisabledValves([]);
}}
className="flex-1"
>
返回上一步
</Button>
<Button
variant="contained"
size="medium"
onClick={() => {
clearSelectedPipe();
}}
className="flex-1"
>
重新开始
</Button>
</Box>
</>
) : (
<Alert severity="info" variant="outlined">
请先在流程2中选择不可用阀门,然后点击“扩大搜索”按钮
</Alert>
)}
</Box>
</StepContent>
</Step>
</Stepper>
</Box>
</Box>
);
};
export default ValveIsolation;