Files
TJWaterFrontend_Refine/src/components/olmap/BurstSimulation/ValveIsolation.tsx
T
2026-03-10 17:52:00 +08:00

1106 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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";
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<ValveIsolationProps> = ({
initialPipeIds = [],
shouldFetch = false,
onFetchComplete,
loading: externalLoading,
result: externalResult,
onLoadingChange,
onResultChange,
}) => {
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 [selectedPipeId, setSelectedPipeId] = useState<string | null>(null);
const [highlightFeature, setHighlightFeature] = useState<Feature | null>(null);
const [isSelecting, setIsSelecting] = useState(false);
const [activeStep, setActiveStep] = useState(0);
const [expandedResult, setExpandedResult] = useState(true);
const [disabledValves, setDisabledValves] = useState<string[]>([]);
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<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [highlightType, setHighlightType] = useState<
"must_close" | "optional" | "affected_node" | "pipe"
>("affected_node");
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;
}
setLoading(true);
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 api.get(
`${config.BACKEND_URL}/api/v1/valve_isolation_analysis/`,
{
params,
paramsSerializer: {
indexes: null, // 生成格式: accident_element=P1&accident_element=P2&disabled_valves=V1&disabled_valves=V2
},
},
);
setResult(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);
}
},
[open, 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]);
// 初始化高亮图层
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",
},
});
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: result.affected_nodes?.length || 0, 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.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">
{/* 选择管段 */}
<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>
{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;