新增健康预测数据查看组件
This commit is contained in:
@@ -5,13 +5,18 @@ import Timeline from "@components/olmap/HealthRiskAnalysis/Timeline";
|
|||||||
import MapToolbar from "@app/OlMap/Controls/Toolbar";
|
import MapToolbar from "@app/OlMap/Controls/Toolbar";
|
||||||
import { HealthRiskProvider } from "@components/olmap/HealthRiskAnalysis/HealthRiskContext";
|
import { HealthRiskProvider } from "@components/olmap/HealthRiskAnalysis/HealthRiskContext";
|
||||||
import HealthRiskPieChart from "@components/olmap/HealthRiskAnalysis/HealthRiskPieChart";
|
import HealthRiskPieChart from "@components/olmap/HealthRiskAnalysis/HealthRiskPieChart";
|
||||||
|
import HistoryDataPanel from "@components/olmap/HealthRiskAnalysis/HistoryDataPanel";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full overflow-hidden">
|
<div className="relative w-full h-full overflow-hidden">
|
||||||
<HealthRiskProvider>
|
<HealthRiskProvider>
|
||||||
<MapComponent>
|
<MapComponent>
|
||||||
<MapToolbar queryType="realtime" />
|
<MapToolbar
|
||||||
|
queryType="realtime"
|
||||||
|
hiddenButtons={["style"]}
|
||||||
|
HistoryPanel={HistoryDataPanel}
|
||||||
|
/>
|
||||||
<Timeline />
|
<Timeline />
|
||||||
<HealthRiskPieChart />
|
<HealthRiskPieChart />
|
||||||
</MapComponent>
|
</MapComponent>
|
||||||
|
|||||||
@@ -26,8 +26,13 @@ const backendUrl = config.BACKEND_URL;
|
|||||||
interface ToolbarProps {
|
interface ToolbarProps {
|
||||||
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
|
hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style']
|
||||||
queryType?: string; // 可选的查询类型参数
|
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 map = useMap();
|
||||||
const data = useData();
|
const data = useData();
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
@@ -654,43 +659,80 @@ const Toolbar: React.FC<ToolbarProps> = ({ hiddenButtons, queryType }) => {
|
|||||||
setLayerStyleStates={setLayerStyleStates}
|
setLayerStyleStates={setLayerStyleStates}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showHistoryPanel && (
|
{showHistoryPanel &&
|
||||||
<HistoryDataPanel
|
(HistoryPanel ? (
|
||||||
featureInfos={(() => {
|
<HistoryPanel
|
||||||
if (!highlightFeature || !showHistoryPanel) return [];
|
featureInfos={(() => {
|
||||||
const properties = highlightFeature.getProperties();
|
if (!highlightFeature || !showHistoryPanel) return [];
|
||||||
const id = properties.id;
|
const properties = highlightFeature.getProperties();
|
||||||
if (!id) return [];
|
const id = properties.id;
|
||||||
|
if (!id) return [];
|
||||||
|
|
||||||
// 从图层名称推断类型
|
// 从图层名称推断类型
|
||||||
const layerId =
|
const layerId =
|
||||||
highlightFeature.getId()?.toString().split(".")[0] || "";
|
highlightFeature.getId()?.toString().split(".")[0] || "";
|
||||||
let type = "unknown";
|
let type = "unknown";
|
||||||
|
|
||||||
if (layerId.includes("pipe")) {
|
if (layerId.includes("pipe")) {
|
||||||
type = "pipe";
|
type = "pipe";
|
||||||
} else if (layerId.includes("junction")) {
|
} else if (layerId.includes("junction")) {
|
||||||
type = "junction";
|
type = "junction";
|
||||||
} else if (layerId.includes("tank")) {
|
} else if (layerId.includes("tank")) {
|
||||||
type = "tank";
|
type = "tank";
|
||||||
} else if (layerId.includes("reservoir")) {
|
} else if (layerId.includes("reservoir")) {
|
||||||
type = "reservoir";
|
type = "reservoir";
|
||||||
} else if (layerId.includes("pump")) {
|
} else if (layerId.includes("pump")) {
|
||||||
type = "pump";
|
type = "pump";
|
||||||
} else if (layerId.includes("valve")) {
|
} else if (layerId.includes("valve")) {
|
||||||
type = "valve";
|
type = "valve";
|
||||||
}
|
}
|
||||||
// 仅处理 type 为 pipe 或 junction 的情况
|
// 仅处理 type 为 pipe 或 junction 的情况
|
||||||
if (type !== "pipe" && type !== "junction") {
|
if (type !== "pipe" && type !== "junction") {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [[id, type]];
|
return [[id, type]];
|
||||||
})()}
|
})()}
|
||||||
scheme_type="burst_Analysis"
|
scheme_type="burst_Analysis"
|
||||||
scheme_name={schemeName}
|
scheme_name={schemeName}
|
||||||
type={queryType as "realtime" | "scheme" | "none"}
|
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 && (
|
{activeLegendConfigs.length > 0 && (
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ const HealthRiskPieChart: React.FC = () => {
|
|||||||
</Fade>
|
</Fade>
|
||||||
|
|
||||||
<Slide direction="left" in={isExpanded} mountOnEnter unmountOnExit>
|
<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 justify-between items-center px-5 py-4 bg-[#257DD4] text-white">
|
||||||
<div className="flex items-center gap-2">
|
<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