"use client"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { Box, Button, Chip, Tooltip, Typography } from "@mui/material"; import { DataGrid, GridColDef } from "@mui/x-data-grid"; import { zhCN } from "@mui/x-data-grid/locales"; import { FormatListBulleted, InfoOutlined as InfoOutlinedIcon, Room as RoomIcon, ShowChart as ShowChartIcon, CheckCircleOutline as CheckCircleIcon, ErrorOutline as ErrorOutlineIcon, } from "@mui/icons-material"; import ReactECharts from "echarts-for-react"; import dayjs from "dayjs"; import { useMap } from "@components/olmap/core/MapComponent"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { GeoJSON } from "ol/format"; import Feature from "ol/Feature"; import VectorLayer from "ol/layer/Vector"; import VectorSource from "ol/source/Vector"; import { Circle, Fill, Stroke, Style } from "ol/style"; import { bbox, featureCollection } from "@turf/turf"; import { BurstDetectionResult, BurstDetectionRow } from "./types"; interface Props { result: BurstDetectionResult | null; } interface MetricCardProps { label: string; value: string; hint?: string; tone: "blue" | "orange" | "purple" | "green"; } const toneStyles: Record< MetricCardProps["tone"], { bg: string; border: string; text: string; darkText: string } > = { blue: { bg: "from-blue-50 to-blue-100", border: "border-blue-200", text: "text-blue-700", darkText: "text-blue-900", }, orange: { bg: "from-orange-50 to-orange-100", border: "border-orange-200", text: "text-orange-700", darkText: "text-orange-900", }, purple: { bg: "from-purple-50 to-purple-100", border: "border-purple-200", text: "text-purple-700", darkText: "text-purple-900", }, green: { bg: "from-green-50 to-green-100", border: "border-green-200", text: "text-green-700", darkText: "text-green-900", }, }; const MetricCard = ({ label, value, hint, tone }: MetricCardProps) => { const style = toneStyles[tone]; return ( {label} {value} {hint ? ( {hint} ) : null} ); }; const EmptyState = () => ( 等待侦测结果 提交一次爆管侦测后,这里会展示异常天数、分数趋势、最新测点排名和结果表格。 ); const getScoreLevel = (score: number) => { if (score <= -0.6) return { label: "高风险", color: "error" as const }; if (score <= -0.2) return { label: "需关注", color: "warning" as const }; return { label: "正常", color: "success" as const }; }; const formatDateTime = (value?: string) => (value ? dayjs(value).format("YYYY-MM-DD HH:mm") : "-"); const DetectionResults: React.FC = ({ result }) => { const map = useMap(); const highlightLayerRef = useRef | null>(null); const [highlightFeatures, setHighlightFeatures] = useState([]); const [selectedDay, setSelectedDay] = useState(null); useEffect(() => { if (!map) return; const layer = new VectorLayer({ source: new VectorSource(), style: new Style({ stroke: new Stroke({ color: "#ef4444", width: 4 }), image: new Circle({ radius: 7, fill: new Fill({ color: "#ef4444" }), stroke: new Stroke({ color: "#fff", width: 2 }), }), zIndex: 999, }), properties: { name: "爆管侦测高亮", value: "burst_detection_highlight", }, }); map.addLayer(layer); highlightLayerRef.current = layer; return () => { highlightLayerRef.current = null; map.removeLayer(layer); }; }, [map]); useEffect(() => { const source = highlightLayerRef.current?.getSource(); if (!source) return; source.clear(); highlightFeatures.forEach((feature) => source.addFeature(feature)); }, [highlightFeatures]); const defaultSelectedDay = useMemo( () => result?.summary?.most_anomalous_day ?? result?.summary?.latest_day?.Day ?? result?.rows[0]?.Day ?? null, [result], ); const activeSelectedDay = selectedDay ?? defaultSelectedDay; const selectedRow = useMemo(() => { if (!result || activeSelectedDay === null) return null; return result.rows.find((row) => row.Day === activeSelectedDay) ?? null; }, [activeSelectedDay, result]); const scoreSeries = useMemo( () => result?.rows.map((row) => ({ value: [row.Day, Number(row.Score.toFixed(4))], itemStyle: { color: row.IsBurst ? "#ef4444" : row.Score <= -0.2 ? "#f59e0b" : "#10b981", }, })) ?? [], [result], ); const rankingSeries = useMemo( () => [...(result?.summary?.latest_sensor_rankings ?? [])] .sort((a, b) => a.latest_high_frequency_value - b.latest_high_frequency_value) .map((item) => ({ name: item.sensor_node, value: Number(item.latest_high_frequency_value.toFixed(4)), })), [result], ); const locateSensors = async (sensorIds: string[]) => { if (!map || sensorIds.length === 0) return; let features = await queryFeaturesByIds(sensorIds, "geo_junctions_mat"); if (features.length === 0) { features = await queryFeaturesByIds(sensorIds, "geo_junctions"); } if (features.length === 0) return; setHighlightFeatures(features); const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => geojsonFormat.writeFeatureObject(feature)); // @ts-ignore turf typing with ol geojson objects const extent = bbox(featureCollection(geojsonFeatures)); map.getView().fit(extent, { maxZoom: 18, duration: 1000, padding: [100, 100, 100, 100], }); }; if (!result) { return ; } const latestDay = result.summary?.latest_day; const latestLevel = latestDay ? getScoreLevel(latestDay.Score) : getScoreLevel(0); const mostAnomalousRow = result.rows.find((row) => row.Day === result.summary?.most_anomalous_day) ?? null; const mostAnomalousLevel = getScoreLevel(mostAnomalousRow?.Score ?? 0); const isBurstDetected = result.summary.burst_detected; const chartOption = { tooltip: { trigger: "axis", formatter: (params: Array<{ data: { value: [number, number] } }>) => { const point = params[0]?.data?.value; if (!point) return "-"; return `侦测日第 ${point[0]} 天
异常分数:${point[1]}`; }, }, grid: { top: 30, left: 40, right: 20, bottom: 35 }, xAxis: { type: "category", name: "侦测日", data: result.rows.map((row) => row.Day), axisLabel: { fontSize: 10 }, }, yAxis: { type: "value", name: "异常分数", axisLabel: { fontSize: 10 }, }, series: [ { type: "line", smooth: true, symbolSize: 8, data: scoreSeries, lineStyle: { color: "#2563eb", width: 2 }, markLine: { symbol: "none", lineStyle: { type: "dashed", color: "#94a3b8" }, data: [{ yAxis: 0 }], }, }, ], }; const rankingOption = { tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, }, grid: { top: 20, left: 70, right: 20, bottom: 20 }, xAxis: { type: "value", axisLabel: { fontSize: 10 } }, yAxis: { type: "category", data: rankingSeries.map((item) => item.name), axisLabel: { fontSize: 10 }, }, series: [ { type: "bar", data: rankingSeries.map((item) => ({ value: item.value, itemStyle: { color: item.value <= -0.6 ? "#ef4444" : item.value <= -0.2 ? "#f59e0b" : "#10b981", }, })), barWidth: 14, }, ], }; const columns: GridColDef[] = [ { field: "Day", headerName: "侦测日", width: 96, valueFormatter: (value?: number) => (typeof value === "number" ? `第 ${value} 天` : "-"), }, { field: "Score", headerName: "异常分数", width: 120, valueFormatter: (value?: number) => (typeof value === "number" ? value.toFixed(4) : "-"), }, { field: "IsBurst", headerName: "判定结果", width: 120, renderCell: ({ value }) => { const level = value ? { label: "爆管异常", color: "error" as const } : { label: "正常", color: "success" as const }; return ; }, }, ]; const rows = result.rows.map((row) => ({ id: row.Day, ...row })); return ( {/* Status Banner */} {isBurstDetected ? ( ) : ( )} {isBurstDetected ? `侦测到异常信号 (共 ${result.summary.anomaly_day_count} 天)` : "未侦测到爆管异常"} {isBurstDetected ? "建议检查异常日期的压力波动情况" : "当前时间窗口内数据特征平稳,符合历史模式"} {/* Header */} {result.scheme_name || "爆管侦测结果"} {result.username ? ( ) : null} {/* Configuration Summary */} 时间窗口: {formatDateTime(result.scada_window?.start)} ~ {formatDateTime(result.scada_window?.end)} 数据来源: {(() => { const ds = result.data_source; const os = result.observed_source; if (ds === "simulation") return "模拟数据"; if (ds === "monitoring") return "监测数据"; if (os === "simulation_scheme_timerange") return "模拟数据"; if (os === "backend_timerange") return "监测数据"; return os || "-"; })()} {/* Metrics Grid */} 0 ? "orange" : "green"} /> {/* Score Trend Chart */} 异常分数趋势 { const day = params?.data?.value?.[0]; if (typeof day === "number") { setSelectedDay(day); } }, }} /> {/* Selected Day Interpretation */} {/* 选中日解读 {selectedRow ? ( ) : null} {selectedRow ? ( 异常分数:{selectedRow.Score.toFixed(4)} 模型判定:{selectedRow.IsBurst ? "异常日(Prediction = -1)" : "正常日(Prediction = 1)"} 解读建议: {selectedRow.Score <= -0.6 ? "高风险异常,建议优先复核对应测点的原始压力曲线与现场工况。" : selectedRow.Score <= -0.2 ? "存在可疑波动,建议结合相邻测点和调度记录进一步确认。" : "未见明显异常,可作为基线日参考。"} ) : ( 请在趋势图或表格中选择一天查看详细解释。 )} */} {/* Latest Sensor Rankings */} {/* 最新测点高频特征排名 仅展示最新一天 {result.summary.latest_sensor_rankings.slice(0, 5).map((item) => ( ))} */} {/* Results Table */} 结果表格 setSelectedDay(Number(params.row.Day))} /> ); }; export default DetectionResults;