) => void;
+ }) => {
+ const createScheme = (id: number, schemeName: string) => ({
+ id,
+ schemeName,
+ type: "burst_analysis",
+ user: "operator",
+ create_time: "2026-07-30T08:00:00+08:00",
+ startTime: "2026-07-30T09:00:00+08:00",
+ schemeDetail: {
+ burst_ID: ["P-1", "P-2"],
+ burst_size: [120, 80],
+ },
+ });
+
+ return (
+ <>
+
+
+ >
+ );
+ },
+ createBurstSchemeQueryState: () => ({}),
+}));
+
+jest.mock("./AnalysisReport", () => ({
+ __esModule: true,
+ default: ({
+ scheme,
+ valveResult,
+ }: {
+ scheme: { schemeName: string } | null;
+ valveResult: { accident_elements: string[] } | null;
+ }) => (
+
+ report:{scheme?.schemeName ?? "empty"}:valve:
+ {valveResult?.accident_elements.join(",") ?? "none"}
+
+ ),
+ matchesValveAnalysis: (
+ scheme: { schemeDetail: { burst_ID: string[] } },
+ valveResult: { accident_elements: string[] } | null,
+ ) =>
+ valveResult !== null &&
+ valveResult.accident_elements.every((pipeId) =>
+ scheme.schemeDetail.burst_ID.includes(pipeId),
+ ),
+}));
+
+jest.mock("./ValveIsolation", () => ({
+ __esModule: true,
+ default: ({
+ allowedPipeIds,
+ sourceSchemeName,
+ onResultChange,
+ }: {
+ allowedPipeIds?: string[];
+ sourceSchemeName?: string;
+ onResultChange: (result: Record) => void;
+ }) => (
+ <>
+
+ valve scope:{sourceSchemeName ?? "independent"}:
+ {allowedPipeIds?.join(",") ?? "any"}
+
+
+ >
+ ),
+ createValveIsolationState: () => ({
+ selectedPipeId: null,
+ activeStep: 0,
+ expandedResult: true,
+ disabledValves: [],
+ }),
+}));
+
+describe("BurstPipeAnalysisPanel", () => {
+ it("replaces the obsolete location tab and opens the selected report", () => {
+ render();
+
+ expect(
+ screen.queryByRole("tab", { name: /定位结果/ }),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.getByRole("tab", { name: /分析报告/ }),
+ ).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole("tab", { name: /方案查询/ }));
+ fireEvent.click(
+ screen.getByRole("button", { name: "mock report action" }),
+ );
+
+ expect(
+ screen.getByText("report:selected-scheme:valve:none"),
+ ).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole("tab", { name: /关阀分析/ }));
+ expect(screen.getByTestId("valve-scope")).toHaveTextContent(
+ "valve scope:selected-scheme:P-1,P-2",
+ );
+ });
+
+ it("does not reuse valve results across schemes with the same pipe", () => {
+ render();
+
+ fireEvent.click(screen.getByRole("tab", { name: /方案查询/ }));
+ fireEvent.click(
+ screen.getByRole("button", { name: "mock report action" }),
+ );
+ fireEvent.click(screen.getByRole("tab", { name: /关阀分析/ }));
+ fireEvent.click(
+ screen.getByRole("button", { name: "mock valve result" }),
+ );
+
+ fireEvent.click(screen.getByRole("tab", { name: /方案查询/ }));
+ fireEvent.click(
+ screen.getByRole("button", { name: "mock other report action" }),
+ );
+
+ expect(
+ screen.getByText("report:other-scheme:valve:none"),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/olmap/BurstSimulation/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstSimulation/BurstPipeAnalysisPanel.tsx
index f305a34..3481f95 100644
--- a/src/components/olmap/BurstSimulation/BurstPipeAnalysisPanel.tsx
+++ b/src/components/olmap/BurstSimulation/BurstPipeAnalysisPanel.tsx
@@ -15,7 +15,7 @@ import {
ChevronLeft,
Analytics as AnalyticsIcon,
Search as SearchIcon,
- MyLocation as MyLocationIcon,
+ DescriptionOutlined as ReportIcon,
Handyman as HandymanIcon,
} from "@mui/icons-material";
import AnalysisParameters, {
@@ -26,15 +26,12 @@ import SchemeQuery, {
createBurstSchemeQueryState,
type BurstSchemeQueryState,
} from "./SchemeQuery";
-import LocationResults from "./LocationResults";
+import AnalysisReport, { matchesValveAnalysis } from "./AnalysisReport";
import ValveIsolation, {
createValveIsolationState,
type ValveIsolationState,
} from "./ValveIsolation";
-import { api } from "@/lib/api";
-import { config } from "@config/config";
-import { useNotification } from "@refinedev/core";
-import { LocationResult, SchemeRecord, ValveIsolationResult } from "./types";
+import { SchemeRecord, ValveIsolationResult } from "./types";
interface TabPanelProps {
children?: React.ReactNode;
@@ -75,16 +72,16 @@ const BurstPipeAnalysisPanel: React.FC = ({
// 持久化方案查询结果
const [schemes, setSchemes] = useState([]);
- // 定位结果数据
- const [locationResults, setLocationResults] = useState([]);
+ const [reportScheme, setReportScheme] = useState(null);
+ const [reportGeneratedAt, setReportGeneratedAt] = useState(null);
// 关阀分析结果和加载状态
const [valveAnalysisLoading, setValveAnalysisLoading] = useState(false);
const [valveAnalysisResult, setValveAnalysisResult] = useState(null);
+ const [valveAnalysisSchemeName, setValveAnalysisSchemeName] =
+ useState(null);
const [valveIsolationState, setValveIsolationState] =
useState(createValveIsolationState);
- const { open } = useNotification();
-
// 使用受控或非受控状态
const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen;
const handleToggle = () => {
@@ -99,21 +96,25 @@ const BurstPipeAnalysisPanel: React.FC = ({
setCurrentTab(newValue);
};
- const handleLocateScheme = async (scheme: SchemeRecord) => {
- try {
- const response = await api.get(
- `${config.BACKEND_URL}/api/v1/burst-locate-result/${scheme.schemeName}`,
- );
- setLocationResults(response.data);
- setCurrentTab(2); // 切换到定位结果标签页
- } catch (error) {
- console.error("获取定位结果失败:", error);
- open?.({
- type: "error",
- message: "获取定位结果失败",
- description: "无法从服务器获取该方案的定位结果",
- });
+ const handleViewReport = (scheme: SchemeRecord) => {
+ if (
+ valveAnalysisSchemeName !== scheme.schemeName ||
+ !matchesValveAnalysis(scheme, valveAnalysisResult)
+ ) {
+ setValveAnalysisResult(null);
+ setValveAnalysisSchemeName(null);
+ setValveIsolationState(createValveIsolationState());
}
+ setReportScheme(scheme);
+ setReportGeneratedAt(new Date());
+ setCurrentTab(2);
+ };
+
+ const handleValveAnalysisResultChange = (
+ result: ValveIsolationResult | null,
+ ) => {
+ setValveAnalysisResult(result);
+ setValveAnalysisSchemeName(result ? reportScheme?.schemeName ?? null : null);
};
const drawerWidth = 520;
@@ -226,9 +227,9 @@ const BurstPipeAnalysisPanel: React.FC = ({
label="方案查询"
/>
}
+ icon={}
iconPosition="start"
- label="定位结果"
+ label="分析报告"
/>
}
@@ -250,15 +251,18 @@ const BurstPipeAnalysisPanel: React.FC = ({
-
@@ -267,9 +271,13 @@ const BurstPipeAnalysisPanel: React.FC = ({
loading={valveAnalysisLoading}
result={valveAnalysisResult}
onLoadingChange={setValveAnalysisLoading}
- onResultChange={setValveAnalysisResult}
+ onResultChange={handleValveAnalysisResultChange}
state={valveIsolationState}
onStateChange={setValveIsolationState}
+ allowedPipeIds={
+ reportScheme?.schemeDetail?.burst_ID
+ }
+ sourceSchemeName={reportScheme?.schemeName}
/>
diff --git a/src/components/olmap/BurstSimulation/LocationResults.tsx b/src/components/olmap/BurstSimulation/LocationResults.tsx
deleted file mode 100644
index 9f14001..0000000
--- a/src/components/olmap/BurstSimulation/LocationResults.tsx
+++ /dev/null
@@ -1,416 +0,0 @@
-"use client";
-
-import React, { useState, useEffect, useRef } from "react";
-import {
- Box,
- Typography,
- Chip,
- IconButton,
- Tooltip,
- Link,
-} from "@mui/material";
-import {
- LocationOn as LocationIcon,
-} from "@mui/icons-material";
-import { queryFeaturesByIds } 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 { Stroke, Style, Icon } from "ol/style";
-import Feature, { FeatureLike } from "ol/Feature";
-import {
- along,
- lineString,
- length,
- toMercator,
- bbox,
- featureCollection,
-} from "@turf/turf";
-import { Point } from "ol/geom";
-import { toLonLat } from "ol/proj";
-import moment from "moment";
-import "moment-timezone";
-import { LocationResult } from "./types";
-import { FLOW_DISPLAY_UNIT } from "@utils/units";
-
-interface LocationResultsProps {
- results?: LocationResult[];
-}
-
-const LocationResults: React.FC = ({
- results = [],
-}) => {
- const highlightLayerRef = useRef | null>(null);
- const [highlightFeatures, setHighlightFeatures] = useState([]);
- const map = useMap();
-
- // 格式化时间为 UTC+8
- const formatTime = (timeStr: string) => {
- return moment(timeStr).utcOffset(8).format("YYYY-MM-DD HH:mm:ss");
- };
-
- const handleLocatePipes = (pipeIds: string[]) => {
- if (pipeIds.length > 0) {
- queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => {
- if (features.length > 0) {
- // 设置高亮要素
- setHighlightFeatures(features);
- // 将 OpenLayers Feature 转换为 GeoJSON Feature
- const geojsonFormat = new GeoJSON();
- const geojsonFeatures = features.map((feature) =>
- geojsonFormat.writeFeatureObject(feature),
- );
-
- const extent = bbox(featureCollection(geojsonFeatures as any));
-
- if (extent) {
- map?.getView().fit(extent, { maxZoom: 18, duration: 1000 });
- }
- }
- });
- }
- };
-
- // 初始化管道图层和高亮图层
- useEffect(() => {
- if (!map) return;
-
- const burstPipeStyle = function (feature: FeatureLike) {
- const styles = [];
- // 线条样式(底层发光,主线条,内层高亮线)
- styles.push(
- new Style({
- stroke: new Stroke({
- color: "rgba(255, 0, 0, 0.3)",
- width: 12,
- }),
- }),
- new Style({
- stroke: new Stroke({
- color: "rgba(255, 0, 0, 1)",
- width: 6,
- lineDash: [15, 10],
- }),
- }),
- new Style({
- stroke: new Stroke({
- color: "rgba(255, 102, 102, 1)",
- width: 3,
- lineDash: [15, 10],
- }),
- }),
- );
- const geometry = feature.getGeometry();
- const lineCoords =
- geometry?.getType() === "LineString"
- ? (geometry as any).getCoordinates()
- : null;
- if (geometry && lineCoords) {
- const lineCoordsWGS84 = lineCoords.map((coord: []) => {
- const [lon, lat] = toLonLat(coord);
- return [lon, lat];
- });
- // 计算中点
- const lineStringFeature = lineString(lineCoordsWGS84);
- const lineLength = length(lineStringFeature);
- const midPoint = along(lineStringFeature, lineLength / 2).geometry
- .coordinates;
- // 在中点添加 icon 样式
- const midPointMercator = toMercator(midPoint);
- styles.push(
- new Style({
- geometry: new Point(midPointMercator),
- image: new Icon({
- src: "/icons/burst_pipe.svg",
- scale: 0.2,
- anchor: [0.5, 1],
- }),
- }),
- );
- }
- return styles;
- };
- // 创建高亮图层
- const highlightLayer = new VectorLayer({
- source: new VectorSource(),
- style: burstPipeStyle,
- maxZoom: 24,
- minZoom: 12,
- properties: {
- name: "爆管管段高亮",
- value: "burst_pipe_highlight",
- queryable: false,
- },
- });
-
- map.addLayer(highlightLayer);
- highlightLayerRef.current = highlightLayer;
-
- return () => {
- highlightLayerRef.current = null;
- map.removeLayer(highlightLayer);
- };
- }, [map]);
-
- // 高亮要素的函数
- useEffect(() => {
- const source = highlightLayerRef.current?.getSource();
- if (!source) {
- return;
- }
- // 清除之前的高亮
- source.clear();
- // 添加新的高亮要素
- highlightFeatures.forEach((feature) => {
- if (feature instanceof Feature) {
- source.addFeature(feature);
- }
- });
- }, [highlightFeatures]);
-
- // 取第一条记录或空对象
- const result = results.length > 0 ? results[0] : null;
-
- return (
-
- {/* 结果展示 */}
-
- {!result ? (
-
-
-
-
- 暂无定位结果
-
- 请先执行方案分析
-
-
- ) : (
-
- {/* 头部:标识信息 */}
-
-
-
- {result.burst_incident}
-
-
-
-
- ID: {result.id}
-
-
-
- {/* 主要信息:三栏卡片布局 */}
-
- {/* 检测时间卡片 */}
-
-
-
-
- 检测时间
-
-
-
- {formatTime(result.detect_time)}
-
-
-
- {/* 漏损量卡片 */}
-
-
-
-
- 漏损量
-
-
-
- {result.leakage !== null
- ? `${result.leakage.toFixed(2)} ${FLOW_DISPLAY_UNIT}`
- : "N/A"}
-
-
-
- {/* 定位管段数量卡片 */}
-
-
-
-
- 定位管段
-
-
-
- {result.locate_result ? result.locate_result.length : 0}{" "}
- 个管段
-
-
-
-
- {/* 定位管段详细列表 */}
- {result.locate_result && result.locate_result.length > 0 && (
-
-
-
- 管段列表
-
-
-
- handleLocatePipes(result.locate_result!)}
- color="primary"
- sx={{
- backgroundColor: "rgba(37, 125, 212, 0.1)",
- "&:hover": {
- backgroundColor: "rgba(37, 125, 212, 0.2)",
- },
- }}
- >
-
-
-
-
-
-
- {result.locate_result.map((pipeId, idx) => (
- handleLocatePipes([pipeId])}
- sx={{
- "&:active": {
- transform: "scale(0.98)",
- boxShadow: "0 1px 2px rgba(25, 118, 210, 0.2)",
- },
- }}
- >
-
-
- {pipeId}
-
-
- {/*
- {
- e.stopPropagation();
- handleLocatePipes([pipeId]);
- }}
- sx={{
- "&:hover": {
- backgroundColor: "rgba(37, 125, 212, 0.1)",
- },
- }}
- >
-
-
- */}
-
-
-
- ))}
-
-
- )}
-
- )}
-
-
- );
-};
-
-export default LocationResults;
diff --git a/src/components/olmap/BurstSimulation/SchemeQuery.tsx b/src/components/olmap/BurstSimulation/SchemeQuery.tsx
index fa38b0e..734ddef 100644
--- a/src/components/olmap/BurstSimulation/SchemeQuery.tsx
+++ b/src/components/olmap/BurstSimulation/SchemeQuery.tsx
@@ -18,6 +18,7 @@ import {
Link,
} from "@mui/material";
import {
+ DescriptionOutlined as ReportIcon,
Info as InfoIcon,
LocationOn as LocationIcon,
} from "@mui/icons-material";
@@ -59,7 +60,7 @@ import {
interface SchemeQueryProps {
schemes?: SchemeRecord[];
onSchemesChange?: (schemes: SchemeRecord[]) => void;
- onLocate?: (scheme: SchemeRecord) => void;
+ onViewReport?: (scheme: SchemeRecord) => void;
network?: string;
state?: BurstSchemeQueryState;
onStateChange?: (state: BurstSchemeQueryState) => void;
@@ -88,7 +89,7 @@ export const createBurstSchemeQueryState = (): BurstSchemeQueryState => ({
const SchemeQuery: React.FC = ({
schemes: externalSchemes,
onSchemesChange,
- onLocate,
+ onViewReport,
network = NETWORK_NAME,
state,
onStateChange,
@@ -599,14 +600,14 @@ const SchemeQuery: React.FC = ({
-
+
onLocate?.(scheme)}
+ onClick={() => onViewReport?.(scheme)}
color="primary"
className="p-1"
>
-
+
diff --git a/src/components/olmap/BurstSimulation/ValveIsolation.tsx b/src/components/olmap/BurstSimulation/ValveIsolation.tsx
index 93f80d0..19ff8db 100644
--- a/src/components/olmap/BurstSimulation/ValveIsolation.tsx
+++ b/src/components/olmap/BurstSimulation/ValveIsolation.tsx
@@ -52,9 +52,12 @@ import {
import { Point } from "ol/geom";
import { toLonLat } from "ol/proj";
import { useControllableObjectState } from "@components/olmap/core/useControllableState";
+import { isAllowedAccidentPipe } from "./valveIsolationScope";
interface ValveIsolationProps {
initialPipeIds?: string[];
+ allowedPipeIds?: string[];
+ sourceSchemeName?: string;
shouldFetch?: boolean;
onFetchComplete?: () => void;
loading?: boolean;
@@ -81,6 +84,8 @@ export const createValveIsolationState = (): ValveIsolationState => ({
const ValveIsolation: React.FC = ({
initialPipeIds = [],
+ allowedPipeIds,
+ sourceSchemeName,
shouldFetch = false,
onFetchComplete,
loading: externalLoading,
@@ -160,14 +165,30 @@ const ValveIsolation: React.FC = ({
return;
}
- setSelectedPipeId(pipeId);
+ const normalizedPipeId = String(pipeId);
+ if (!isAllowedAccidentPipe(normalizedPipeId, allowedPipeIds)) {
+ open?.({
+ type: "error",
+ message: "请选择当前爆管方案中的事故管段",
+ });
+ return;
+ }
+
+ setSelectedPipeId(normalizedPipeId);
setHighlightFeature(feature);
setIsSelecting(false);
setResult(null); // 清除旧结果
}
}
},
- [isSelecting, map, open, setResult, setSelectedPipeId],
+ [
+ allowedPipeIds,
+ isSelecting,
+ map,
+ open,
+ setResult,
+ setSelectedPipeId,
+ ],
);
useEffect(() => {
@@ -207,6 +228,16 @@ const ValveIsolation: React.FC = ({
"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) {
@@ -311,6 +342,13 @@ const ValveIsolation: React.FC = ({
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) {
@@ -352,7 +390,14 @@ const ValveIsolation: React.FC = ({
setLoading(false);
}
},
- [open, setActiveStep, setDisabledValves, setLoading, setResult],
+ [
+ allowedPipeIds,
+ open,
+ setActiveStep,
+ setDisabledValves,
+ setLoading,
+ setResult,
+ ],
);
// 监听外部传入的分析请求
@@ -912,6 +957,16 @@ const ValveIsolation: React.FC = ({
+ {allowedPipeIds !== undefined ? (
+
+ 已绑定爆管方案“{sourceSchemeName || "未命名方案"}”。关阀分析仅限该方案中的事故管段,分析结果将写入对应报告。
+
+ ) : (
+
+ 当前为独立关阀分析,结果不会自动写入爆管分析报告。请先从“方案查询”打开对应分析报告以建立关联。
+
+ )}
+
{/* 选择管段 */}
@@ -955,6 +1010,36 @@ const ValveIsolation: React.FC = ({
)}
+ {allowedPipeIds !== undefined && (
+
+
+ {allowedPipeIds.map((pipeId) => (
+ selectSchemeAccidentPipe(pipeId)}
+ />
+ ))}
+
+
+
+ )}
+
{isSelecting && (
💡 点击地图上的管道添加爆管点
diff --git a/src/components/olmap/BurstSimulation/types.ts b/src/components/olmap/BurstSimulation/types.ts
index 9d8f5d7..91abb26 100644
--- a/src/components/olmap/BurstSimulation/types.ts
+++ b/src/components/olmap/BurstSimulation/types.ts
@@ -28,15 +28,6 @@ export interface SchemaItem {
scheme_detail?: SchemeDetail;
}
-export interface LocationResult {
- id: number;
- type: string;
- burst_incident: string;
- leakage: number | null;
- detect_time: string;
- locate_result: string[] | null;
-}
-
export interface ValveIsolationResult {
accident_elements: string[];
affected_nodes: string[];
diff --git a/src/components/olmap/BurstSimulation/valveIsolationScope.ts b/src/components/olmap/BurstSimulation/valveIsolationScope.ts
new file mode 100644
index 0000000..9c8c14c
--- /dev/null
+++ b/src/components/olmap/BurstSimulation/valveIsolationScope.ts
@@ -0,0 +1,4 @@
+export const isAllowedAccidentPipe = (
+ pipeId: string,
+ allowedPipeIds: string[] | undefined,
+) => allowedPipeIds === undefined || allowedPipeIds.includes(pipeId);