From 22280a0df9970c3dbb670b2de2cfe099f7bc1d13 Mon Sep 17 00:00:00 2001 From: JIANG Date: Thu, 18 Dec 2025 18:12:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=81=A5=E5=BA=B7=E9=A2=84?= =?UTF-8?q?=E6=B5=8B=E6=95=B0=E6=8D=AE=E6=9F=A5=E7=9C=8B=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/health-risk-analysis/page.tsx | 7 +- src/app/OlMap/Controls/Toolbar.tsx | 114 +++++-- .../HealthRiskAnalysis/HealthRiskPieChart.tsx | 2 +- .../HealthRiskAnalysis/HistoryDataPanel.tsx | 313 ++++++++++++++++++ 4 files changed, 398 insertions(+), 38 deletions(-) create mode 100644 src/components/olmap/HealthRiskAnalysis/HistoryDataPanel.tsx diff --git a/src/app/(main)/health-risk-analysis/page.tsx b/src/app/(main)/health-risk-analysis/page.tsx index a42c5f2..2d7320f 100644 --- a/src/app/(main)/health-risk-analysis/page.tsx +++ b/src/app/(main)/health-risk-analysis/page.tsx @@ -5,13 +5,18 @@ import Timeline from "@components/olmap/HealthRiskAnalysis/Timeline"; import MapToolbar from "@app/OlMap/Controls/Toolbar"; import { HealthRiskProvider } from "@components/olmap/HealthRiskAnalysis/HealthRiskContext"; import HealthRiskPieChart from "@components/olmap/HealthRiskAnalysis/HealthRiskPieChart"; +import HistoryDataPanel from "@components/olmap/HealthRiskAnalysis/HistoryDataPanel"; export default function Home() { return (
- + diff --git a/src/app/OlMap/Controls/Toolbar.tsx b/src/app/OlMap/Controls/Toolbar.tsx index def5d7b..7a84306 100644 --- a/src/app/OlMap/Controls/Toolbar.tsx +++ b/src/app/OlMap/Controls/Toolbar.tsx @@ -26,8 +26,13 @@ const backendUrl = config.BACKEND_URL; interface ToolbarProps { hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style'] queryType?: string; // 可选的查询类型参数 + HistoryPanel?: React.FC; // 可选的自定义历史数据面板 } -const Toolbar: React.FC = ({ hiddenButtons, queryType }) => { +const Toolbar: React.FC = ({ + hiddenButtons, + queryType, + HistoryPanel, +}) => { const map = useMap(); const data = useData(); if (!data) return null; @@ -654,43 +659,80 @@ const Toolbar: React.FC = ({ hiddenButtons, queryType }) => { setLayerStyleStates={setLayerStyleStates} /> )} - {showHistoryPanel && ( - { - if (!highlightFeature || !showHistoryPanel) return []; - const properties = highlightFeature.getProperties(); - const id = properties.id; - if (!id) return []; + {showHistoryPanel && + (HistoryPanel ? ( + { + if (!highlightFeature || !showHistoryPanel) return []; + const properties = highlightFeature.getProperties(); + const id = properties.id; + if (!id) return []; - // 从图层名称推断类型 - const layerId = - highlightFeature.getId()?.toString().split(".")[0] || ""; - let type = "unknown"; + // 从图层名称推断类型 + const layerId = + highlightFeature.getId()?.toString().split(".")[0] || ""; + let type = "unknown"; - if (layerId.includes("pipe")) { - type = "pipe"; - } else if (layerId.includes("junction")) { - type = "junction"; - } else if (layerId.includes("tank")) { - type = "tank"; - } else if (layerId.includes("reservoir")) { - type = "reservoir"; - } else if (layerId.includes("pump")) { - type = "pump"; - } else if (layerId.includes("valve")) { - type = "valve"; - } - // 仅处理 type 为 pipe 或 junction 的情况 - if (type !== "pipe" && type !== "junction") { - return []; - } - return [[id, type]]; - })()} - scheme_type="burst_Analysis" - scheme_name={schemeName} - type={queryType as "realtime" | "scheme" | "none"} - /> - )} + if (layerId.includes("pipe")) { + type = "pipe"; + } else if (layerId.includes("junction")) { + type = "junction"; + } else if (layerId.includes("tank")) { + type = "tank"; + } else if (layerId.includes("reservoir")) { + type = "reservoir"; + } else if (layerId.includes("pump")) { + type = "pump"; + } else if (layerId.includes("valve")) { + type = "valve"; + } + // 仅处理 type 为 pipe 或 junction 的情况 + if (type !== "pipe" && type !== "junction") { + return []; + } + return [[id, type]]; + })()} + scheme_type="burst_Analysis" + scheme_name={schemeName} + type={queryType as "realtime" | "scheme" | "none"} + /> + ) : ( + { + if (!highlightFeature || !showHistoryPanel) return []; + const properties = highlightFeature.getProperties(); + const id = properties.id; + if (!id) return []; + + // 从图层名称推断类型 + const layerId = + highlightFeature.getId()?.toString().split(".")[0] || ""; + let type = "unknown"; + + if (layerId.includes("pipe")) { + type = "pipe"; + } else if (layerId.includes("junction")) { + type = "junction"; + } else if (layerId.includes("tank")) { + type = "tank"; + } else if (layerId.includes("reservoir")) { + type = "reservoir"; + } else if (layerId.includes("pump")) { + type = "pump"; + } else if (layerId.includes("valve")) { + type = "valve"; + } + // 仅处理 type 为 pipe 或 junction 的情况 + if (type !== "pipe" && type !== "junction") { + return []; + } + return [[id, type]]; + })()} + scheme_type="burst_Analysis" + scheme_name={schemeName} + type={queryType as "realtime" | "scheme" | "none"} + /> + ))} {/* 图例显示 */} {activeLegendConfigs.length > 0 && ( diff --git a/src/components/olmap/HealthRiskAnalysis/HealthRiskPieChart.tsx b/src/components/olmap/HealthRiskAnalysis/HealthRiskPieChart.tsx index abb5a6f..4148bfa 100644 --- a/src/components/olmap/HealthRiskAnalysis/HealthRiskPieChart.tsx +++ b/src/components/olmap/HealthRiskAnalysis/HealthRiskPieChart.tsx @@ -175,7 +175,7 @@ const HealthRiskPieChart: React.FC = () => { -
+
{/* 头部 */}
diff --git a/src/components/olmap/HealthRiskAnalysis/HistoryDataPanel.tsx b/src/components/olmap/HealthRiskAnalysis/HistoryDataPanel.tsx new file mode 100644 index 0000000..7f73e8d --- /dev/null +++ b/src/components/olmap/HealthRiskAnalysis/HistoryDataPanel.tsx @@ -0,0 +1,313 @@ +"use client"; + +import React, { useMemo, useRef, useState } from "react"; +import Draggable from "react-draggable"; + +import { Box, Chip, Stack, Tab, Tabs, Typography } from "@mui/material"; +import { ShowChart, TableChart } from "@mui/icons-material"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import { zhCN } from "@mui/x-data-grid/locales"; +import ReactECharts from "echarts-for-react"; +import "dayjs/locale/zh-cn"; +import { useHealthRisk } from "./HealthRiskContext"; + +export interface HistoryDataPanelProps { + /** 选中的要素信息列表,格式为 [[id, type], [id, type]] */ + featureInfos: [string, string][]; + /** 数据类型: realtime-查询模拟值和监测值, none-仅查询监测值, scheme-查询策略模拟值和监测值 */ + type?: "realtime" | "scheme" | "none"; + /** 策略类型 */ + scheme_type?: string; + /** 策略名称 */ + scheme_name?: string; + /** 默认展示的选项卡 */ + defaultTab?: "chart" | "table"; + /** Y 轴数值的小数位数 */ + fractionDigits?: number; +} + +type PanelTab = "chart" | "table"; + +const HistoryDataPanel: React.FC = ({ + featureInfos, + defaultTab = "chart", + fractionDigits = 4, +}) => { + const { predictionResults } = useHealthRisk(); + const [activeTab, setActiveTab] = useState(defaultTab); + const draggableRef = useRef(null); + + // 提取选中的设备 ID + const selectedIds = useMemo( + () => featureInfos.map(([id]) => id), + [featureInfos] + ); + + // 过滤出选中管道的预测结果 + const filteredResults = useMemo(() => { + return predictionResults.filter((res) => selectedIds.includes(res.link_id)); + }, [predictionResults, selectedIds]); + + const hasData = filteredResults.length > 0; + + // 构建表格和图表所需的数据集 + const dataset = useMemo(() => { + if (filteredResults.length === 0) return []; + + // 获取所有唯一的时间点并排序 + const allX = Array.from( + new Set(filteredResults.flatMap((res) => res.survival_function.x)) + ).sort((a, b) => a - b); + + return allX.map((x) => { + const row: any = { x, label: `${x}年` }; + filteredResults.forEach((res) => { + const index = res.survival_function.x.indexOf(x); + if (index !== -1) { + row[res.link_id] = res.survival_function.y[index]; + } + }); + return row; + }); + }, [filteredResults]); + + const columns: GridColDef[] = useMemo(() => { + const base: GridColDef[] = [ + { + field: "label", + headerName: "预测时长", + minWidth: 120, + flex: 1, + }, + ]; + + const dynamic = filteredResults.map((res) => ({ + field: res.link_id, + headerName: `管道 ${res.link_id}`, + minWidth: 150, + flex: 1, + valueFormatter: (value: any) => { + if (value === null || value === undefined) return "--"; + return Number(value).toFixed(fractionDigits); + }, + })); + + return [...base, ...dynamic]; + }, [filteredResults, fractionDigits]); + + const rows = useMemo( + () => + dataset.map((item, index) => ({ + id: index, + ...item, + })), + [dataset] + ); + + const renderEmpty = () => ( + + + + 暂无预测数据 + + + 请在地图上选择已分析的管道 + + + ); + + const renderChart = () => { + if (!hasData) return renderEmpty(); + + const colors = [ + "#1976d2", + "#dc004e", + "#ff9800", + "#4caf50", + "#9c27b0", + "#00bcd4", + "#f44336", + "#8bc34a", + "#ff5722", + "#3f51b5", + ]; + + const xData = dataset.map((item) => item.x); + + const series = filteredResults.map((res, index) => ({ + name: `管道 ${res.link_id}`, + type: "line", + smooth: true, + symbol: "circle", + symbolSize: 6, + itemStyle: { + color: colors[index % colors.length], + }, + data: res.survival_function.y, + })); + + const option = { + tooltip: { + trigger: "axis", + formatter: (params: any) => { + let res = `${params[0].name}年
`; + params.forEach((item: any) => { + res += `${item.marker} ${item.seriesName}: ${item.value.toFixed( + fractionDigits + )}
`; + }); + return res; + }, + }, + legend: { + top: "top", + type: "scroll", + }, + grid: { + left: "5%", + right: "5%", + bottom: "10%", + containLabel: true, + }, + xAxis: { + type: "category", + name: "年", + boundaryGap: false, + data: xData, + }, + yAxis: { + type: "value", + name: "生存概率", + min: 0, + max: 1, + }, + series, + }; + + return ( + + + + ); + }; + + const renderTable = () => { + if (!hasData) return renderEmpty(); + + return ( + + ); + }; + + return ( + + + {/* Header */} + + + + + + 健康预测曲线 + + + + + + + {/* Tabs */} + + setActiveTab(value)} + variant="fullWidth" + > + } + iconPosition="start" + label="预测曲线" + /> + } + iconPosition="start" + label="数据表格" + /> + + + + {/* Content */} + + {activeTab === "chart" ? renderChart() : renderTable()} + + + + ); +}; + +export default HistoryDataPanel;