新增健康预测数据查看组件
This commit is contained in:
@@ -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 (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<HealthRiskProvider>
|
||||
<MapComponent>
|
||||
<MapToolbar queryType="realtime" />
|
||||
<MapToolbar
|
||||
queryType="realtime"
|
||||
hiddenButtons={["style"]}
|
||||
HistoryPanel={HistoryDataPanel}
|
||||
/>
|
||||
<Timeline />
|
||||
<HealthRiskPieChart />
|
||||
</MapComponent>
|
||||
|
||||
@@ -26,8 +26,13 @@ const backendUrl = config.BACKEND_URL;
|
||||
interface ToolbarProps {
|
||||
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
|
||||
queryType?: string; // 可选的查询类型参数
|
||||
HistoryPanel?: React.FC<any>; // 可选的自定义历史数据面板
|
||||
}
|
||||
const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
|
||||
const Toolbar: React.FC<ToolbarProps> = ({
|
||||
hiddenButtons,
|
||||
queryType,
|
||||
HistoryPanel,
|
||||
}) => {
|
||||
const map = useMap();
|
||||
const data = useData();
|
||||
if (!data) return null;
|
||||
@@ -654,43 +659,80 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
|
||||
setLayerStyleStates={setLayerStyleStates}
|
||||
/>
|
||||
)}
|
||||
{showHistoryPanel && (
|
||||
<HistoryDataPanel
|
||||
featureInfos={(() => {
|
||||
if (!highlightFeature || !showHistoryPanel) return [];
|
||||
const properties = highlightFeature.getProperties();
|
||||
const id = properties.id;
|
||||
if (!id) return [];
|
||||
{showHistoryPanel &&
|
||||
(HistoryPanel ? (
|
||||
<HistoryPanel
|
||||
featureInfos={(() => {
|
||||
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"}
|
||||
/>
|
||||
) : (
|
||||
<HistoryDataPanel
|
||||
featureInfos={(() => {
|
||||
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 && (
|
||||
|
||||
@@ -175,7 +175,7 @@ const HealthRiskPieChart: React.FC = () => {
|
||||
</Fade>
|
||||
|
||||
<Slide direction="left" in={isExpanded} mountOnEnter unmountOnExit>
|
||||
<div className="absolute top-4 right-4 bg-white shadow-2xl rounded-xl overflow-hidden w-[450px] flex flex-col backdrop-blur-sm z-[1000] opacity-95 hover:opacity-100 transition-all duration-300">
|
||||
<div className="absolute top-4 right-4 bg-white shadow-2xl rounded-xl overflow-hidden w-160 flex flex-col backdrop-blur-sm z-[1000] opacity-95 hover:opacity-100 transition-all duration-300">
|
||||
{/* 头部 */}
|
||||
<div className="flex justify-between items-center px-5 py-4 bg-[#257DD4] text-white">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
313
src/components/olmap/HealthRiskAnalysis/HistoryDataPanel.tsx
Normal file
313
src/components/olmap/HealthRiskAnalysis/HistoryDataPanel.tsx
Normal file
@@ -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<HistoryDataPanelProps> = ({
|
||||
featureInfos,
|
||||
defaultTab = "chart",
|
||||
fractionDigits = 4,
|
||||
}) => {
|
||||
const { predictionResults } = useHealthRisk();
|
||||
const [activeTab, setActiveTab] = useState<PanelTab>(defaultTab);
|
||||
const draggableRef = useRef<HTMLDivElement>(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 = () => (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 8,
|
||||
color: "text.secondary",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<ShowChart sx={{ fontSize: 64, mb: 2, opacity: 0.3 }} />
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 500 }}>
|
||||
暂无预测数据
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
请在地图上选择已分析的管道
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
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}年<br/>`;
|
||||
params.forEach((item: any) => {
|
||||
res += `${item.marker} ${item.seriesName}: ${item.value.toFixed(
|
||||
fractionDigits
|
||||
)}<br/>`;
|
||||
});
|
||||
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 (
|
||||
<Box sx={{ width: "100%", height: "100%" }}>
|
||||
<ReactECharts
|
||||
option={option}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
notMerge={true}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTable = () => {
|
||||
if (!hasData) return renderEmpty();
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
localeText={zhCN.components.MuiDataGrid.defaultProps.localeText}
|
||||
initialState={{
|
||||
pagination: {
|
||||
paginationModel: { pageSize: 10, page: 0 },
|
||||
},
|
||||
}}
|
||||
pageSizeOptions={[10, 20, 50]}
|
||||
sx={{
|
||||
border: "none",
|
||||
height: "100%",
|
||||
}}
|
||||
disableRowSelectionOnClick
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Draggable nodeRef={draggableRef}>
|
||||
<Box
|
||||
ref={draggableRef}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "1rem",
|
||||
top: "1rem",
|
||||
width: "min(800px, calc(100vw - 2rem))",
|
||||
height: "600px",
|
||||
borderRadius: "12px",
|
||||
boxShadow: 3,
|
||||
backdropFilter: "blur(8px)",
|
||||
zIndex: 1300,
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
backgroundColor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<ShowChart fontSize="small" />
|
||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
||||
健康预测曲线
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`${filteredResults.length}`}
|
||||
sx={{
|
||||
backgroundColor: "rgba(255,255,255,0.2)",
|
||||
color: "primary.contrastText",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Tabs */}
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, value: PanelTab) => setActiveTab(value)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab
|
||||
value="chart"
|
||||
icon={<ShowChart fontSize="small" />}
|
||||
iconPosition="start"
|
||||
label="预测曲线"
|
||||
/>
|
||||
<Tab
|
||||
value="table"
|
||||
icon={<TableChart fontSize="small" />}
|
||||
iconPosition="start"
|
||||
label="数据表格"
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{ flex: 1, p: 2, overflow: "hidden" }}>
|
||||
{activeTab === "chart" ? renderChart() : renderTable()}
|
||||
</Box>
|
||||
</Box>
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryDataPanel;
|
||||
Reference in New Issue
Block a user