diff --git a/src/components/olmap/HealthRiskAnalysis/HealthRiskContext.tsx b/src/components/olmap/HealthRiskAnalysis/HealthRiskContext.tsx index c44d021..71c2e3b 100644 --- a/src/components/olmap/HealthRiskAnalysis/HealthRiskContext.tsx +++ b/src/components/olmap/HealthRiskAnalysis/HealthRiskContext.tsx @@ -8,6 +8,8 @@ interface HealthRiskContextType { setPredictionResults: Dispatch>; currentYear: number; setCurrentYear: Dispatch>; + analysisQueryTime: string | null; + setAnalysisQueryTime: Dispatch>; } const HealthRiskContext = createContext(undefined); @@ -15,6 +17,7 @@ const HealthRiskContext = createContext(undef export const HealthRiskProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [predictionResults, setPredictionResults] = useState([]); const [currentYear, setCurrentYear] = useState(4); + const [analysisQueryTime, setAnalysisQueryTime] = useState(null); return ( = ({ children setPredictionResults, currentYear, setCurrentYear, + analysisQueryTime, + setAnalysisQueryTime, }} > {children} diff --git a/src/components/olmap/HealthRiskAnalysis/HealthRiskReport.test.tsx b/src/components/olmap/HealthRiskAnalysis/HealthRiskReport.test.tsx new file mode 100644 index 0000000..dc77565 --- /dev/null +++ b/src/components/olmap/HealthRiskAnalysis/HealthRiskReport.test.tsx @@ -0,0 +1,84 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import HealthRiskReport from "./HealthRiskReport"; +import { PredictionResult } from "./types"; + +jest.mock("echarts-for-react", () => ({ + __esModule: true, + default: () =>
, +})); + +const predictionResults: PredictionResult[] = [ + { + link_id: "PIPE-007", + survival_function: { + x: [4, 5], + y: [0.2, 0.15], + a: 0, + b: 0, + }, + }, + { + link_id: "PIPE-021", + survival_function: { + x: [4, 5], + y: [0.8, 0.7], + a: 0, + b: 0, + }, + }, +]; + +describe("HealthRiskReport", () => { + it("renders automatic report identity and priority results", () => { + render( + , + ); + + expect( + screen.getByRole("heading", { name: "管网健康风险体检报告" }), + ).toBeInTheDocument(); + expect(screen.getByText("fengyang")).toBeInTheDocument(); + expect(screen.getByText("第 4 年")).toBeInTheDocument(); + expect(screen.getByText("PIPE-007")).toBeInTheDocument(); + expect(screen.getAllByTestId("report-chart")).toHaveLength(2); + }); + + it("prints the report and restores document state after printing", () => { + const originalTitle = document.title; + const print = jest + .spyOn(window, "print") + .mockImplementation(() => undefined); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "打印" })); + + expect(print).toHaveBeenCalledTimes(1); + expect(document.body).toHaveClass("health-risk-report-printing"); + expect(document.title).toContain("管网健康风险体检报告-fengyang"); + + window.dispatchEvent(new Event("afterprint")); + + expect(document.body).not.toHaveClass("health-risk-report-printing"); + expect(document.title).toBe(originalTitle); + print.mockRestore(); + }); +}); diff --git a/src/components/olmap/HealthRiskAnalysis/HealthRiskReport.tsx b/src/components/olmap/HealthRiskAnalysis/HealthRiskReport.tsx new file mode 100644 index 0000000..0cddbaf --- /dev/null +++ b/src/components/olmap/HealthRiskAnalysis/HealthRiskReport.tsx @@ -0,0 +1,746 @@ +"use client"; + +import React, { useEffect, useMemo, useRef } from "react"; +import ReactECharts from "echarts-for-react"; +import { + Box, + Button, + Dialog, + GlobalStyles, + IconButton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { + Close, + PrintOutlined, + WaterDropOutlined, +} from "@mui/icons-material"; +import { PredictionResult } from "./types"; +import { buildHealthRiskReportData } from "./healthRiskReport"; + +interface HealthRiskReportProps { + open: boolean; + onClose: () => void; + predictionResults: PredictionResult[]; + currentYear: number; + analysisQueryTime: string | null; + networkName: string; + generatedAt: Date; +} + +const REPORT_BLUE = "#0b4f87"; +const REPORT_INK = "#172435"; +const REPORT_MUTED = "#5f6f80"; +const REPORT_LINE = "#d8e0e8"; + +const reportFontFamily = + '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif'; + +const formatDateTime = (value: Date | string | null) => { + 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 formatPercent = (value: number | null, fractionDigits = 1) => + value === null ? "无有效数据" : `${(value * 100).toFixed(fractionDigits)}%`; + +const SectionTitle = ({ children }: { children: React.ReactNode }) => ( + + +); + +const HealthRiskReport: React.FC = ({ + open, + onClose, + predictionResults, + currentYear, + analysisQueryTime, + networkName, + generatedAt, +}) => { + const printStateRef = useRef<{ title: string } | null>(null); + const reportData = useMemo( + () => buildHealthRiskReportData(predictionResults, currentYear), + [currentYear, predictionResults], + ); + + useEffect( + () => () => { + if (printStateRef.current) { + document.title = printStateRef.current.title; + document.body.classList.remove("health-risk-report-printing"); + } + }, + [], + ); + + const restoreAfterPrint = () => { + if (!printStateRef.current) return; + document.title = printStateRef.current.title; + printStateRef.current = null; + document.body.classList.remove("health-risk-report-printing"); + }; + + const handlePrint = () => { + if (printStateRef.current) return; + printStateRef.current = { title: document.title }; + document.title = `管网健康风险体检报告-${networkName || "未命名管网"}-${new Intl.DateTimeFormat( + "zh-CN", + { year: "numeric", month: "2-digit", day: "2-digit" }, + ) + .format(generatedAt) + .replaceAll("/", "-")}`; + document.body.classList.add("health-risk-report-printing"); + window.addEventListener("afterprint", restoreAfterPrint, { once: true }); + window.print(); + }; + + const distributionOption = { + animation: false, + grid: { top: 4, right: 38, bottom: 28, left: 148 }, + xAxis: { + type: "value", + name: "管段数", + minInterval: 1, + axisLine: { lineStyle: { color: "#8b99a8" } }, + axisLabel: { color: REPORT_MUTED, fontSize: 10 }, + splitLine: { lineStyle: { color: "#e7edf2" } }, + nameTextStyle: { color: REPORT_MUTED, fontSize: 10 }, + }, + yAxis: { + type: "category", + inverse: true, + data: reportData.distribution.map((item) => item.label), + axisLine: { show: false }, + axisTick: { show: false }, + axisLabel: { color: REPORT_INK, fontSize: 10 }, + }, + tooltip: { show: false }, + series: [ + { + type: "bar", + barMaxWidth: 14, + data: reportData.distribution.map((item) => ({ + value: item.count, + itemStyle: { color: item.color }, + })), + label: { + show: true, + position: "right", + color: REPORT_INK, + fontSize: 10, + }, + }, + ], + }; + + const trendOption = { + animation: false, + grid: { top: 24, right: 24, bottom: 38, left: 48 }, + xAxis: { + type: "category", + name: "预测年份", + boundaryGap: false, + data: reportData.trend.map((item) => item.year), + axisLine: { lineStyle: { color: "#8b99a8" } }, + axisLabel: { + color: REPORT_MUTED, + fontSize: 10, + interval: Math.max(0, Math.ceil(reportData.trend.length / 9) - 1), + }, + nameTextStyle: { color: REPORT_MUTED, fontSize: 10 }, + }, + yAxis: { + type: "value", + name: "较高及以上风险管段数", + minInterval: 1, + axisLine: { show: false }, + axisLabel: { color: REPORT_MUTED, fontSize: 10 }, + splitLine: { lineStyle: { color: "#e7edf2" } }, + nameTextStyle: { color: REPORT_MUTED, fontSize: 10 }, + }, + tooltip: { show: false }, + series: [ + { + type: "line", + smooth: true, + symbol: "none", + lineStyle: { color: "#c83d3d", width: 2.5 }, + areaStyle: { color: "rgba(200, 61, 61, 0.08)" }, + data: reportData.trend.map((item) => item.count), + }, + ], + }; + + const metricItems = [ + { label: "分析管段", value: reportData.totalCount.toLocaleString("zh-CN") }, + { label: "有效结果", value: reportData.validCount.toLocaleString("zh-CN") }, + { + label: "平均生存概率", + value: formatPercent(reportData.averageProbability), + }, + { + label: "较高及以上风险", + value: `${reportData.highRiskCount.toLocaleString("zh-CN")} 条`, + detail: formatPercent(reportData.highRiskRatio), + danger: reportData.highRiskCount > 0, + }, + ]; + + return ( + <> + *:not(.health-risk-report-dialog)": + { + display: "none !important", + }, + ".health-risk-report-dialog": { + position: "static !important", + display: "block !important", + }, + ".health-risk-report-dialog .MuiBackdrop-root, .health-risk-report-actions": + { + display: "none !important", + }, + ".health-risk-report-dialog .MuiDialog-container": { + display: "block !important", + height: "auto !important", + }, + ".health-risk-report-dialog .MuiDialog-paper": { + width: "210mm !important", + maxWidth: "none !important", + minHeight: "297mm !important", + maxHeight: "none !important", + margin: "0 !important", + overflow: "visible !important", + boxShadow: "none !important", + }, + ".health-risk-report-preview": { + padding: "0 !important", + overflow: "visible !important", + backgroundColor: "#fff !important", + }, + "#health-risk-print-root": { + width: "210mm !important", + minHeight: "297mm !important", + padding: "12mm !important", + boxShadow: "none !important", + printColorAdjust: "exact", + WebkitPrintColorAdjust: "exact", + }, + ".health-risk-report-section, .health-risk-report-metrics": { + breakInside: "avoid", + pageBreakInside: "avoid", + }, + ".health-risk-report-table-row": { + breakInside: "avoid", + pageBreakInside: "avoid", + }, + }, + }} + /> + + + + + + + + + 报告预览 + + + A4 纵向,可在打印窗口另存为 PDF + + + + + + + + + + + + + + + 管网健康风险体检报告 + + + Pipeline health risk assessment + + + + + + + {reportData.highRiskCount > 0 ? "需重点关注" : "持续监测"} + + + + + + + {[ + ["管网项目", networkName || "未命名管网"], + ["预测时点", `第 ${currentYear} 年`], + ["分析数据时间", formatDateTime(analysisQueryTime)], + ["报告生成时间", formatDateTime(generatedAt)], + ].map(([label, value]) => ( + + + {label} + + + {value} + + + ))} + + + + 总体结论 + 0 ? "#fff4f1" : "#eff8f4", + px: 2.25, + py: 1.75, + }} + > + + {reportData.conclusion} + + {reportData.invalidCount > 0 && ( + + 另有 {reportData.invalidCount} 条管段在当前预测年份缺少有效结果,未纳入本期统计。 + + )} + + + + + {metricItems.map((item, index) => ( + + + {item.label} + + + {item.value} + + {item.detail && ( + + 占有效结果 {item.detail} + + )} + + ))} + + + + 当前年份风险分布 + + 生存概率越低,表示模型预测风险越高。统计口径与地图风险图例一致。 + + + + + + 重点风险趋势 + + 展示各预测年份中生存概率不高于 0.3 的管段数量。 + + + + + + 重点风险管段 + + 按当前预测年份的生存概率从低到高排列,最多展示 20 条。 + + {reportData.priorityPipes.length > 0 ? ( + + + + + 序号 + + 管段编号 + + + 生存概率 + + 风险等级 + + + + {reportData.priorityPipes.map((pipe, index) => ( + + {index + 1} + + {pipe.linkId} + + + {formatPercent(pipe.probability, 2)} + + + + + + + ))} + +
+
+ ) : ( + + + 当前预测年份没有较高及以上风险管段。 + + + )} +
+ + + 处置建议 + + {reportData.recommendations.map((recommendation) => ( + + {recommendation} + + ))} + + + + + TJWater 管网分析平台 + + 风险阈值:生存概率 ≤ 0.3 + + +
+
+
+ + ); +}; + +export default HealthRiskReport; diff --git a/src/components/olmap/HealthRiskAnalysis/HealthRiskStatistics.tsx b/src/components/olmap/HealthRiskAnalysis/HealthRiskStatistics.tsx index 2def28a..07da4f0 100644 --- a/src/components/olmap/HealthRiskAnalysis/HealthRiskStatistics.tsx +++ b/src/components/olmap/HealthRiskAnalysis/HealthRiskStatistics.tsx @@ -11,10 +11,18 @@ import { Stack, Slide, Fade, + Button, } from "@mui/material"; -import { ChevronLeft, ChevronRight, BarChart } from "@mui/icons-material"; +import { + ChevronLeft, + ChevronRight, + BarChart, + DescriptionOutlined, +} from "@mui/icons-material"; import { RAINBOW_COLORS, RISK_BREAKS, RISK_LABELS } from "./types"; import { useHealthRisk } from "./HealthRiskContext"; +import { useProject } from "@/contexts/ProjectContext"; +import HealthRiskReport from "./HealthRiskReport"; const SIMPLE_LABELS = [ "0.0 - 0.1", @@ -30,8 +38,13 @@ const SIMPLE_LABELS = [ ]; const HealthRiskStatistics: React.FC = () => { - const { predictionResults, currentYear } = useHealthRisk(); + const { predictionResults, currentYear, analysisQueryTime } = useHealthRisk(); + const project = useProject(); const [isExpanded, setIsExpanded] = React.useState(true); + const [isReportOpen, setIsReportOpen] = React.useState(false); + const [reportGeneratedAt, setReportGeneratedAt] = React.useState( + () => new Date(), + ); const [hoveredYearIndex, setHoveredYearIndex] = React.useState( null ); @@ -278,6 +291,41 @@ const HealthRiskStatistics: React.FC = () => { />
+ 0 + ? "生成健康风险体检报告" + : "请先完成健康风险分析" + } + > + + + + { + setIsReportOpen(false)} + predictionResults={predictionResults} + currentYear={currentYear} + analysisQueryTime={analysisQueryTime} + networkName={project?.networkName || "未命名管网"} + generatedAt={reportGeneratedAt} + /> ); }; diff --git a/src/components/olmap/HealthRiskAnalysis/Timeline.tsx b/src/components/olmap/HealthRiskAnalysis/Timeline.tsx index b0bb85a..daee04f 100644 --- a/src/components/olmap/HealthRiskAnalysis/Timeline.tsx +++ b/src/components/olmap/HealthRiskAnalysis/Timeline.tsx @@ -137,6 +137,7 @@ const Timeline: React.FC = ({ setPredictionResults, currentYear, setCurrentYear, + setAnalysisQueryTime, } = useHealthRisk(); const [selectedDateTime, setSelectedDateTime] = useState(new Date()); @@ -397,6 +398,7 @@ const Timeline: React.FC = ({ if (response.ok) { const results: PredictionResult[] = await response.json(); setPredictionResults(results); + setAnalysisQueryTime(calculationDateTime.toISOString()); open?.({ type: "success", message: `模拟预测完成,获取到 ${results.length} 条管道数据`, @@ -404,7 +406,11 @@ const Timeline: React.FC = ({ } else { // 读取后端 HTTPException 返回的 detail 信息 const errorData = await response.json().catch(() => ({})); - const errorMessage = errorData.detail || "模拟预测失败"; + const errorMessage = + typeof errorData.detail === "string" && + errorData.detail.includes("未找到流速数据") + ? "所选时间暂无可用的管网模拟数据,请确认该时刻已完成模拟计算并生成流速结果后重试。" + : errorData.detail || "模拟预测失败"; open?.({ type: "error", message: errorMessage, diff --git a/src/components/olmap/HealthRiskAnalysis/healthRiskReport.test.ts b/src/components/olmap/HealthRiskAnalysis/healthRiskReport.test.ts new file mode 100644 index 0000000..ace70fb --- /dev/null +++ b/src/components/olmap/HealthRiskAnalysis/healthRiskReport.test.ts @@ -0,0 +1,84 @@ +import { + buildHealthRiskReportData, + getProbabilityAtYear, + getRiskBandIndex, + PRIORITY_PIPE_LIMIT, +} from "./healthRiskReport"; +import { PredictionResult } from "./types"; + +const createResult = ( + linkId: string, + years: number[], + probabilities: number[], +): PredictionResult => ({ + link_id: linkId, + survival_function: { + x: years, + y: probabilities, + a: 0, + b: 0, + }, +}); + +describe("healthRiskReport", () => { + it("matches probability by the exact forecast year", () => { + const result = createResult("P-1", [4, 6], [0.8, 0.2]); + + expect(getProbabilityAtYear(result, 6)).toBe(0.2); + expect(getProbabilityAtYear(result, 5)).toBeNull(); + }); + + it("rejects missing and out-of-range probabilities", () => { + expect( + getProbabilityAtYear(createResult("P-1", [4], [Number.NaN]), 4), + ).toBeNull(); + expect(getProbabilityAtYear(createResult("P-2", [4], [1.1]), 4)).toBeNull(); + }); + + it("keeps risk break boundaries consistent with the existing legend", () => { + expect(getRiskBandIndex(0)).toBe(0); + expect(getRiskBandIndex(0.1)).toBe(0); + expect(getRiskBandIndex(0.10001)).toBe(1); + expect(getRiskBandIndex(1)).toBe(9); + }); + + it("builds the current snapshot and high-risk trend", () => { + const report = buildHealthRiskReportData( + [ + createResult("P-1", [4, 5], [0.2, 0.1]), + createResult("P-2", [4, 5], [0.5, 0.25]), + createResult("P-3", [4, 5], [0.9, 0.8]), + createResult("P-4", [5], [0.4]), + ], + 4, + ); + + expect(report.totalCount).toBe(4); + expect(report.validCount).toBe(3); + expect(report.invalidCount).toBe(1); + expect(report.highRiskCount).toBe(1); + expect(report.highRiskRatio).toBeCloseTo(1 / 3); + expect(report.averageProbability).toBeCloseTo(0.533333); + expect(report.distribution.reduce((sum, item) => sum + item.count, 0)).toBe( + 3, + ); + expect(report.trend).toEqual([ + { year: 4, count: 1, validCount: 3, ratio: 1 / 3 }, + { year: 5, count: 2, validCount: 4, ratio: 0.5 }, + ]); + }); + + it("sorts priority pipes and limits the printed detail", () => { + const results = Array.from({ length: PRIORITY_PIPE_LIMIT + 5 }, (_, index) => + createResult(`P-${PRIORITY_PIPE_LIMIT + 5 - index}`, [4], [0.2]), + ); + + const report = buildHealthRiskReportData(results, 4); + + expect(report.priorityPipes).toHaveLength(PRIORITY_PIPE_LIMIT); + expect(report.priorityPipes[0].linkId).toBe("P-1"); + expect(report.priorityPipes.at(-1)?.linkId).toBe( + `P-${PRIORITY_PIPE_LIMIT}`, + ); + }); +}); diff --git a/src/components/olmap/HealthRiskAnalysis/healthRiskReport.ts b/src/components/olmap/HealthRiskAnalysis/healthRiskReport.ts new file mode 100644 index 0000000..b00125f --- /dev/null +++ b/src/components/olmap/HealthRiskAnalysis/healthRiskReport.ts @@ -0,0 +1,201 @@ +import { + PredictionResult, + RAINBOW_COLORS, + RISK_BREAKS, + RISK_LABELS, +} from "./types"; + +export const HIGH_RISK_MAX_PROBABILITY = 0.3; +export const PRIORITY_PIPE_LIMIT = 20; + +export interface RiskDistributionItem { + label: string; + color: string; + count: number; + ratio: number; +} + +export interface HighRiskTrendPoint { + year: number; + count: number; + validCount: number; + ratio: number; +} + +export interface PriorityPipeItem { + linkId: string; + probability: number; + riskLabel: string; + color: string; +} + +export interface HealthRiskReportData { + totalCount: number; + validCount: number; + invalidCount: number; + averageProbability: number | null; + highRiskCount: number; + highRiskRatio: number; + distribution: RiskDistributionItem[]; + trend: HighRiskTrendPoint[]; + priorityPipes: PriorityPipeItem[]; + conclusion: string; + recommendations: string[]; +} + +const isValidProbability = (value: unknown): value is number => + typeof value === "number" && + Number.isFinite(value) && + value >= 0 && + value <= 1; + +export const getProbabilityAtYear = ( + result: PredictionResult, + year: number, +): number | null => { + const yearIndex = result.survival_function.x.findIndex( + (value) => value === year, + ); + if (yearIndex < 0) return null; + + const probability = result.survival_function.y[yearIndex]; + return isValidProbability(probability) ? probability : null; +}; + +export const getRiskBandIndex = (probability: number): number => { + const index = RISK_BREAKS.findIndex((upperBound) => probability <= upperBound); + return index >= 0 ? index : RISK_BREAKS.length - 1; +}; + +export const buildHealthRiskReportData = ( + results: PredictionResult[], + currentYear: number, +): HealthRiskReportData => { + const currentValues = results.flatMap((result) => { + const probability = getProbabilityAtYear(result, currentYear); + return probability === null ? [] : [{ result, probability }]; + }); + const validCount = currentValues.length; + const highRiskCount = currentValues.filter( + ({ probability }) => probability <= HIGH_RISK_MAX_PROBABILITY, + ).length; + + const distribution = RISK_LABELS.map((label, index) => { + const count = currentValues.filter( + ({ probability }) => getRiskBandIndex(probability) === index, + ).length; + return { + label, + color: RAINBOW_COLORS[index], + count, + ratio: validCount > 0 ? count / validCount : 0, + }; + }); + + const priorityPipes = currentValues + .filter( + ({ probability }) => probability <= HIGH_RISK_MAX_PROBABILITY, + ) + .sort( + (left, right) => + left.probability - right.probability || + left.result.link_id.localeCompare(right.result.link_id, "zh-CN", { + numeric: true, + }), + ) + .slice(0, PRIORITY_PIPE_LIMIT) + .map(({ result, probability }) => { + const bandIndex = getRiskBandIndex(probability); + return { + linkId: result.link_id, + probability, + riskLabel: RISK_LABELS[bandIndex], + color: RAINBOW_COLORS[bandIndex], + }; + }); + + const trendByYear = new Map< + number, + { count: number; validCount: number } + >(); + results.forEach((result) => { + const seenYears = new Set(); + result.survival_function.x.forEach((year, index) => { + if ( + typeof year !== "number" || + !Number.isFinite(year) || + seenYears.has(year) + ) { + return; + } + seenYears.add(year); + const probability = result.survival_function.y[index]; + if (!isValidProbability(probability)) return; + + const point = trendByYear.get(year) ?? { count: 0, validCount: 0 }; + point.validCount += 1; + if (probability <= HIGH_RISK_MAX_PROBABILITY) point.count += 1; + trendByYear.set(year, point); + }); + }); + const trend = Array.from(trendByYear, ([year, point]) => ({ + year, + count: point.count, + validCount: point.validCount, + ratio: point.validCount > 0 ? point.count / point.validCount : 0, + })).sort((left, right) => left.year - right.year); + + const averageProbability = + validCount > 0 + ? currentValues.reduce( + (total, { probability }) => total + probability, + 0, + ) / validCount + : null; + const highRiskRatio = validCount > 0 ? highRiskCount / validCount : 0; + const mediumRiskCount = currentValues.filter( + ({ probability }) => + probability > HIGH_RISK_MAX_PROBABILITY && probability <= 0.6, + ).length; + + const conclusion = + validCount === 0 + ? `预测第 ${currentYear} 年没有可用于报告的有效生存概率数据。` + : highRiskCount > 0 + ? `预测第 ${currentYear} 年发现 ${highRiskCount} 条较高及以上风险管段,占有效结果的 ${(highRiskRatio * 100).toFixed(1)}%。建议优先核查重点管段,并结合现场检测确认处置顺序。` + : `预测第 ${currentYear} 年未发现较高及以上风险管段。当前结果仍需结合巡检记录持续跟踪。`; + + const recommendations: string[] = []; + if (highRiskCount > 0) { + recommendations.push( + "优先核查重点风险管段的运行状态、历史故障和周边施工情况,必要时安排现场检测。", + ); + } + if (mediumRiskCount > 0) { + recommendations.push( + `对其余 ${mediumRiskCount} 条中等风险管段提高监测频率,关注压力波动和风险趋势变化。`, + ); + } + if (highRiskCount === 0 && mediumRiskCount === 0 && validCount > 0) { + recommendations.push( + "保持现有巡检和监测频率,在新的在线模拟数据产生后重新评估。", + ); + } + recommendations.push( + "本报告为模型预测结果,仅用于辅助决策,不能替代现场检测、专业鉴定和安全处置流程。", + ); + + return { + totalCount: results.length, + validCount, + invalidCount: results.length - validCount, + averageProbability, + highRiskCount, + highRiskRatio, + distribution, + trend, + priorityPipes, + conclusion, + recommendations, + }; +}; diff --git a/src/components/olmap/HealthRiskAnalysis/types.ts b/src/components/olmap/HealthRiskAnalysis/types.ts index b44b481..25db29b 100644 --- a/src/components/olmap/HealthRiskAnalysis/types.ts +++ b/src/components/olmap/HealthRiskAnalysis/types.ts @@ -7,9 +7,9 @@ export interface SurvivalFunction { export interface PredictionResult { link_id: string; - diameter: number; - velocity: number; - pressure: number; + diameter?: number; + velocity?: number; + pressure?: number; survival_function: SurvivalFunction; }