"use client";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Alert,
Box,
Button,
Chip,
GlobalStyles,
Paper,
Portal,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import {
DescriptionOutlined,
PrintOutlined,
} from "@mui/icons-material";
import { NETWORK_NAME } from "@config/config";
import {
clearPipeDiameterCache,
getCachedPipeDiameters,
getPipeDiameterDisplay,
getPipeDiameterQueryKey,
loadPipeDiameters,
type PipeDiameterMap,
} from "./schemePipeDiameters";
import { SchemeRecord, ValveIsolationResult } from "./types";
import { getAffectedNodeCount } from "./valveIsolationScope";
import PanelEmptyState from "@components/olmap/common/PanelEmptyState";
interface AnalysisReportProps {
scheme: SchemeRecord | null;
valveResult: ValveIsolationResult | null;
disabledValves: string[];
generatedAt: Date | null;
}
interface ReportDocumentProps extends AnalysisReportProps {
diameters: PipeDiameterMap;
loadingDiameters: boolean;
}
const REPORT_BLUE = "#0b4f87";
const REPORT_INK = "#172435";
const REPORT_MUTED = "#5f6f80";
const REPORT_LINE = "#d8e0e8";
export const clearAnalysisReportDiameterCache = clearPipeDiameterCache;
const formatDateTime = (value: Date | string | null | undefined) => {
if (!value) return "未记录";
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) return "未记录";
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}).format(date);
};
const formatDuration = (seconds: number | undefined) => {
if (!Number.isFinite(seconds) || seconds === undefined || seconds < 0) {
return "未记录";
}
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
return [
hours ? `${hours} 小时` : "",
minutes ? `${minutes} 分钟` : "",
remainingSeconds || (!hours && !minutes) ? `${remainingSeconds} 秒` : "",
]
.filter(Boolean)
.join(" ");
};
export const matchesValveAnalysis = (
scheme: SchemeRecord | null,
valveResult: ValveIsolationResult | null,
) => {
const schemePipeIds = new Set(scheme?.schemeDetail?.burst_ID ?? []);
const accidentElements = valveResult?.accident_elements ?? [];
return (
valveResult !== null &&
accidentElements.length > 0 &&
accidentElements.every((pipeId) => schemePipeIds.has(pipeId))
);
};
const SectionTitle = ({ children }: { children: React.ReactNode }) => (
{children}
);
const IdList = ({
values,
emptyText = "无",
}: {
values: string[] | undefined;
emptyText?: string;
}) =>
values?.length ? (
{values.map((value) => (
))}
) : (
{emptyText}
);
const ReportDocument: React.FC = ({
scheme,
valveResult,
disabledValves,
generatedAt,
diameters,
loadingDiameters,
}) => {
if (!scheme) return null;
const pipeIds = scheme.schemeDetail?.burst_ID ?? [];
const burstSizes = scheme.schemeDetail?.burst_size ?? [];
const matchedValveResult = matchesValveAnalysis(scheme, valveResult)
? valveResult
: null;
const duration = scheme.schemeDetail?.modify_total_duration;
const valveDetailRows: Array<[string, string[] | undefined]> =
matchedValveResult
? [
["已分析事故管段", matchedValveResult.accident_elements],
["必关阀门", matchedValveResult.must_close_valves],
["可选阀门", matchedValveResult.optional_valves],
["不可用阀门", disabledValves],
]
: [];
if (matchedValveResult?.isolatable) {
valveDetailRows.push(["受影响节点", matchedValveResult.affected_nodes]);
}
return (
爆管分析报告
报告编号:BA-{scheme.id} 生成时间:{formatDateTime(generatedAt)}
方案概况
{[
["管网", NETWORK_NAME],
["方案名称", scheme.schemeName],
["方案创建人", scheme.username || "未记录"],
["方案创建时间", formatDateTime(scheme.create_time)],
["模拟开始时间", formatDateTime(scheme.startTime)],
["模拟持续时间", formatDuration(duration)],
].map(([label, value]) => (
{label}
{value}
))}
爆管模拟参数
序号
爆管管段
管径
爆管面积
{pipeIds.length ? (
pipeIds.map((pipeId, index) => (
{index + 1}
{pipeId}
{getPipeDiameterDisplay(
[pipeId],
diameters,
loadingDiameters,
)}
{Number.isFinite(burstSizes[index])
? `${burstSizes[index]} cm²`
: "未记录"}
))
) : (
未记录爆管管段
)}
分析结论
本方案记录了 {pipeIds.length} 条爆管管段,模拟持续时间为
{formatDuration(duration)}。
{matchedValveResult
? ` 当前关阀分析判定事故管段${
matchedValveResult.isolatable ? "可以" : "无法"
}有效隔离。`
: " 当前会话尚未对本方案执行匹配的关阀分析。"}
关阀分析
{matchedValveResult ? (
{[
["隔离结论", matchedValveResult.isolatable ? "可隔离" : "不可隔离"],
["必关阀门", `${matchedValveResult.must_close_valves?.length ?? 0} 个`],
["受影响节点", `${getAffectedNodeCount(matchedValveResult)} 个`],
].map(([label, value]) => (
{label}
{value}
))}
{valveDetailRows.map(([label, values]) => (
{label}
))}
{!matchedValveResult.isolatable && (
不可隔离,未生成受影响节点清单。
)}
) : (
本方案尚未执行关阀分析。可在“关阀分析”页签完成分析后重新查看报告。
)}
);
};
const AnalysisReport: React.FC = ({
scheme,
valveResult,
disabledValves,
generatedAt,
}) => {
const pipeIds = useMemo(
() => scheme?.schemeDetail?.burst_ID ?? [],
[scheme],
);
const diameterQueryKey = getPipeDiameterQueryKey(pipeIds);
const cachedDiameters = getCachedPipeDiameters(pipeIds);
const [diameterState, setDiameterState] = useState<{
queryKey: string | null;
values: PipeDiameterMap;
}>({ queryKey: null, values: {} });
const [printReady, setPrintReady] = useState(false);
const printStateRef = useRef<{ title: string } | null>(null);
const diameters = useMemo(
() =>
diameterState.queryKey === diameterQueryKey
? diameterState.values
: cachedDiameters ?? {},
[cachedDiameters, diameterQueryKey, diameterState],
);
const loadingDiameters =
pipeIds.length > 0 &&
diameterState.queryKey !== diameterQueryKey &&
!cachedDiameters;
useEffect(() => {
if (!pipeIds.length) {
return;
}
if (getCachedPipeDiameters(pipeIds)) {
return;
}
let cancelled = false;
loadPipeDiameters(pipeIds)
.then((nextDiameters) => {
if (!cancelled) {
setDiameterState({
queryKey: diameterQueryKey,
values: nextDiameters,
});
}
})
.catch((error) => {
console.error("查询分析报告管径失败:", error);
if (!cancelled) {
setDiameterState({
queryKey: diameterQueryKey,
values: Object.fromEntries(
pipeIds.map((pipeId) => [pipeId, null]),
),
});
}
});
return () => {
cancelled = true;
};
}, [diameterQueryKey, pipeIds]);
const restoreAfterPrint = useCallback(() => {
if (printStateRef.current) {
document.title = printStateRef.current.title;
printStateRef.current = null;
document.body.classList.remove("burst-analysis-report-printing");
}
setPrintReady(false);
}, []);
useEffect(
() => () => {
restoreAfterPrint();
},
[restoreAfterPrint],
);
const handlePrint = () => {
if (!scheme || printStateRef.current) return;
printStateRef.current = { title: document.title };
document.title = `爆管分析报告-${scheme.schemeName}`;
document.body.classList.add("burst-analysis-report-printing");
setPrintReady(true);
};
useEffect(() => {
if (!printReady) return;
window.addEventListener("afterprint", restoreAfterPrint, { once: true });
try {
window.print();
} catch (error) {
console.error("打印爆管分析报告失败:", error);
window.setTimeout(restoreAfterPrint, 0);
}
return () => {
window.removeEventListener("afterprint", restoreAfterPrint);
};
}, [printReady, restoreAfterPrint]);
const reportDocument = useMemo(
() => (
),
[
diameters,
disabledValves,
generatedAt,
loadingDiameters,
scheme,
valveResult,
],
);
if (!scheme) {
return (
}
title="尚未选择分析方案"
description="请在“方案查询”中打开一个方案,再查看分析报告。"
/>
);
}
return (
<>
*:not(.burst-analysis-report-print-root)":
{ display: "none !important" },
".burst-analysis-report-print-root": {
display: "block !important",
width: "210mm !important",
minHeight: "297mm !important",
backgroundColor: "#fff !important",
},
".burst-analysis-report-print-root > div": {
padding: "14mm 16mm !important",
},
},
}}
/>
}
onClick={handlePrint}
>
打印/保存 PDF
{reportDocument}
{printReady && (
{reportDocument}
)}
>
);
};
export default AnalysisReport;