更新爆管定位功能,优化数据处理和展示

This commit is contained in:
JIANG
2026-03-07 13:54:15 +08:00
parent 133880f7fc
commit 2f24ab5d66
4 changed files with 498 additions and 231 deletions
@@ -1,10 +1,9 @@
"use client"; "use client";
import React, { useCallback, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
import { import {
Alert,
Box, Box,
Button, Button,
CircularProgress, CircularProgress,
@@ -70,6 +69,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
const [basicPressure, setBasicPressure] = useState<number>(10); const [basicPressure, setBasicPressure] = useState<number>(10);
const [advancedOpen, setAdvancedOpen] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(false);
const [running, setRunning] = useState(false); const [running, setRunning] = useState(false);
const isSimulationMode = dataSource === "simulation";
const applySchemeTimeRange = useCallback((scheme: SchemeItem) => { const applySchemeTimeRange = useCallback((scheme: SchemeItem) => {
const start = dayjs(scheme.scheme_start_time); const start = dayjs(scheme.scheme_start_time);
@@ -78,10 +78,16 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
setBurstStartTime(start); setBurstStartTime(start);
setBurstEndTime(end); setBurstEndTime(end);
setNormalStartTime(start.subtract(2, "hour")); setNormalStartTime(start);
setNormalEndTime(start.subtract(10, "minute")); setNormalEndTime(end);
}, []); }, []);
useEffect(() => {
if (!isSimulationMode) return;
setNormalStartTime(burstStartTime);
setNormalEndTime(burstEndTime);
}, [burstEndTime, burstStartTime, isSimulationMode]);
const fetchSchemes = useCallback( const fetchSchemes = useCallback(
async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => { async ({ force = false, notify = false }: { force?: boolean; notify?: boolean } = {}) => {
if (schemeLoading || (!force && schemes.length > 0)) return; if (schemeLoading || (!force && schemes.length > 0)) return;
@@ -262,7 +268,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
</FormControl> </FormControl>
</Box> </Box>
{dataSource === "simulation" && ( {isSimulationMode && (
<Box> <Box>
<Typography variant="subtitle2" className="mb-1 font-medium"> <Typography variant="subtitle2" className="mb-1 font-medium">
@@ -323,6 +329,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
value={burstStartTime} value={burstStartTime}
onChange={setBurstStartTime} onChange={setBurstStartTime}
maxDateTime={burstEndTime ?? undefined} maxDateTime={burstEndTime ?? undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm" format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }} slotProps={{ textField: { size: "small", fullWidth: true } }}
/> />
@@ -335,6 +342,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
value={burstEndTime} value={burstEndTime}
onChange={setBurstEndTime} onChange={setBurstEndTime}
minDateTime={burstStartTime ?? undefined} minDateTime={burstStartTime ?? undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm" format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }} slotProps={{ textField: { size: "small", fullWidth: true } }}
/> />
@@ -347,6 +355,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
value={normalStartTime} value={normalStartTime}
onChange={setNormalStartTime} onChange={setNormalStartTime}
maxDateTime={normalEndTime ?? undefined} maxDateTime={normalEndTime ?? undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm" format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }} slotProps={{ textField: { size: "small", fullWidth: true } }}
/> />
@@ -359,6 +368,7 @@ const AnalysisParameters: React.FC<Props> = ({ onResult }) => {
value={normalEndTime} value={normalEndTime}
onChange={setNormalEndTime} onChange={setNormalEndTime}
minDateTime={normalStartTime ?? undefined} minDateTime={normalStartTime ?? undefined}
disabled={isSimulationMode}
format="YYYY-MM-DD HH:mm" format="YYYY-MM-DD HH:mm"
slotProps={{ textField: { size: "small", fullWidth: true } }} slotProps={{ textField: { size: "small", fullWidth: true } }}
/> />
@@ -1,20 +1,22 @@
"use client"; "use client";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { import {
Box, Box,
Button, Typography,
Chip, Chip,
Divider, IconButton,
IconButton, Table,
Paper, TableBody,
Tooltip, TableCell,
Typography TableHead,
TableRow,
Button,
} from "@mui/material"; } from "@mui/material";
import { import {
FormatListBulleted, FormatListBulleted,
LocationOn as LocationOnIcon, LocationOn as LocationOnIcon,
Map as MapIcon Map as MapIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useMap } from "@app/OlMap/MapComponent"; import { useMap } from "@app/OlMap/MapComponent";
@@ -25,24 +27,102 @@ import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector"; import VectorSource from "ol/source/Vector";
import { Stroke, Style, Circle, Fill } from "ol/style"; import { Stroke, Style, Circle, Fill } from "ol/style";
import { bbox, featureCollection } from "@turf/turf"; import { bbox, featureCollection } from "@turf/turf";
import { BurstLocationResult } from "./types"; import { BurstCandidate, BurstLocationResult } from "./types";
import { DMA_FLOW_DISPLAY_UNIT } from "../DMALeakDetection/utils";
interface Props { interface Props {
result: BurstLocationResult | null; result: BurstLocationResult | 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 formatDateTime = (value?: string) =>
value ? dayjs(value).format("MM-DD HH:mm") : "-";
const MetricCard = ({ label, value, hint, tone }: MetricCardProps) => {
const style = toneStyles[tone];
return (
<Box
className={`rounded-lg border bg-gradient-to-br p-3 shadow-sm ${style.bg} ${style.border}`}
>
<Typography
variant="caption"
className={`mb-1 block text-xs font-semibold uppercase tracking-wide ${style.text}`}
>
{label}
</Typography>
<Typography variant="body2" className={`font-bold ${style.darkText}`}>
{value}
</Typography>
{hint ? (
<Typography variant="caption" className={`mt-0.5 block text-xs opacity-80 ${style.text}`}>
{hint}
</Typography>
) : null}
</Box>
);
};
const EmptyState = () => (
<Box className="flex h-full flex-col items-center justify-center bg-gray-50/50 p-6 text-center">
<Box className="mb-4 rounded-full bg-white p-6 shadow-sm">
<MapIcon sx={{ fontSize: 48, color: "#cbd5e1" }} />
</Box>
<Typography variant="h6" className="mb-1 font-bold text-gray-700">
</Typography>
<Typography variant="body2" className="max-w-xs text-gray-500">
</Typography>
</Box>
);
const LocationResults: React.FC<Props> = ({ result }) => { const LocationResults: React.FC<Props> = ({ result }) => {
const map = useMap(); const map = useMap();
const [highlightLayer, setHighlightLayer] = const [highlightLayer, setHighlightLayer] = useState<VectorLayer<VectorSource> | null>(null);
useState<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]); const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const candidatePipes = useMemo(() => { const candidatePipes = useMemo<BurstCandidate[]>(() => {
if (!result) return []; if (!result) return [];
const base = result.top_candidates ?? []; const base = result.top_candidates ?? [];
const hasLocated = base.some((item) => item.pipe_id === result.located_pipe); const hasLocated = base.some((item) => item.pipe_id === result.located_pipe);
if (result.located_pipe && !hasLocated) { if (result.located_pipe && !hasLocated) {
return [{ pipe_id: result.located_pipe, similarity: 1.0 }, ...base]; return [{ pipe_id: result.located_pipe, similarity: 1 }, ...base];
} }
return base; return base;
}, [result]); }, [result]);
@@ -86,133 +166,124 @@ const LocationResults: React.FC<Props> = ({ result }) => {
const locatePipes = async (pipeIds: string[]) => { const locatePipes = async (pipeIds: string[]) => {
if (!pipeIds.length || !map) return; if (!pipeIds.length || !map) return;
try { try {
// 尝试两个图层
let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat"); let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat");
if (features.length === 0) { if (features.length === 0) {
features = await queryFeaturesByIds(pipeIds, "geo_pipes"); features = await queryFeaturesByIds(pipeIds, "geo_pipes");
} }
if (features.length === 0) return; if (features.length === 0) return;
setHighlightFeatures(features); setHighlightFeatures(features);
const geojsonFormat = new GeoJSON(); const geojsonFormat = new GeoJSON();
const geojsonFeatures = features.map((feature) => const geojsonFeatures = features.map((feature) => geojsonFormat.writeFeatureObject(feature));
geojsonFormat.writeFeatureObject(feature), // @ts-ignore turf typing with ol geojson objects
);
// @ts-ignore
const extent = bbox(featureCollection(geojsonFeatures)); const extent = bbox(featureCollection(geojsonFeatures));
map.getView().fit(extent, { map.getView().fit(extent, {
maxZoom: 19, maxZoom: 19,
duration: 1000, duration: 1000,
padding: [100, 100, 100, 100], padding: [100, 100, 100, 100],
}); });
} catch (e) { } catch (error) {
console.error("Locate failed", e); console.error("Locate failed", error);
} }
}; };
if (!result) { if (!result) {
return ( return <EmptyState />;
<Box className="flex flex-col items-center justify-center h-full bg-gray-50/50 p-6 text-center">
<Box className="bg-white p-6 rounded-full shadow-sm mb-4">
<MapIcon sx={{ fontSize: 48, color: "#cbd5e1" }} />
</Box>
<Typography variant="h6" className="text-gray-700 font-bold mb-1"></Typography>
<Typography variant="body2" className="text-gray-500 max-w-xs">
</Typography>
</Box>
);
} }
const burstSamples = result.pressure_samples?.burst ?? 0;
const normalSamples = result.pressure_samples?.normal ?? 0;
const elapsedText =
result.elapsed_seconds && result.elapsed_seconds > 0
? `${result.elapsed_seconds.toFixed(1)} s`
: "-";
const bestSimilarity = candidatePipes[0]?.similarity ?? 0;
const burstTime = result.scada_window?.burst_start
? formatDateTime(result.scada_window.burst_start)
: "-";
return ( return (
<Box className="h-full overflow-y-auto bg-gray-50 p-3 space-y-3"> <Box className="h-full overflow-auto p-1">
{/* 1. 冠军卡片 */} {/* Header & Metrics */}
<Box className="flex items-center justify-between px-1 mb-2"> <Box className="mb-4 space-y-3">
<Box className="flex items-center gap-2"> <Box className="flex items-center justify-between px-1">
<Box className="w-1 h-4 bg-blue-600 rounded-full" />
<Typography
variant="h6"
className="font-bold text-gray-900 truncate"
sx={{ fontSize: "1.1rem" }}
title={result.scheme_name || "Burst Location Result"}
>
{result.scheme_name || "爆管定位结果"}
</Typography>
</Box>
{result.username && (
<Chip
label={result.username}
size="small"
sx={{
height: 24,
backgroundColor: "#f3f4f6",
color: "#4b5563",
border: "none",
fontWeight: 500
}}
/>
)}
</Box>
{/* 2. 统计数据 */}
<Box className="grid grid-cols-2 gap-3 mb-4">
{/* 方案时间/创建时间 */}
<Box className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-3 border border-blue-200 shadow-sm">
<Typography variant="caption" className="text-blue-700 font-semibold block mb-1 text-xs uppercase tracking-wide">
</Typography>
<Typography variant="body2" className="font-bold text-blue-900">
{result.create_time ? dayjs(result.create_time).format("MM-DD HH:mm") : "-"}
</Typography>
</Box>
{/* 漏损量 */}
<Box className="bg-gradient-to-br from-orange-50 to-orange-100 rounded-lg p-3 border border-orange-200 shadow-sm">
<Typography variant="caption" className="text-orange-700 font-semibold block mb-1 text-xs uppercase tracking-wide">
</Typography>
<Typography variant="body2" className="font-bold text-orange-900">
{result.burst_leakage.toFixed(1)} L/s
</Typography>
</Box>
{/* 最佳匹配 */}
<Box className="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-3 border border-purple-200 shadow-sm col-span-2">
<Box className="flex items-center justify-between">
<Box>
<Typography variant="caption" className="text-purple-700 font-semibold block mb-1 text-xs uppercase tracking-wide">
</Typography>
<Typography variant="h6" className="font-bold text-purple-900">
{result.located_pipe}
</Typography>
<Typography variant="caption" className="text-purple-600">
: {(candidatePipes[0]?.similarity * 100 || 0).toFixed(1)}% · : {result.similarity_mode}
</Typography>
</Box>
<Button
size="small"
variant="contained"
color="secondary"
startIcon={<LocationOnIcon />}
onClick={() => locatePipes([result.located_pipe])}
sx={{ backgroundColor: "#9333ea", "&:hover": { backgroundColor: "#7e22ce" } }}
>
</Button>
</Box>
</Box>
</Box>
{/* 3. 候选列表 */}
<Box className="rounded-xl border border-gray-100 bg-white shadow-sm overflow-hidden">
<Box className="px-4 py-3 border-b border-gray-100 flex items-center justify-between bg-white">
<Box className="flex items-center gap-2"> <Box className="flex items-center gap-2">
<FormatListBulleted className="text-blue-600 w-5 h-5" /> <Box className="h-4 w-1 rounded-full bg-blue-600" />
<Typography
variant="h6"
className="truncate font-bold text-gray-900"
sx={{ fontSize: "1.1rem" }}
title={result.scheme_name}
>
{result.scheme_name || "爆管定位结果"}
</Typography>
</Box>
<Box className="flex items-center gap-2">
{result.username ? (
<Chip
label={result.username}
size="small"
sx={{
height: 24,
backgroundColor: "#f3f4f6",
color: "#4b5563",
border: "none",
fontWeight: 500,
}}
/>
) : null}
<Button
size="small"
variant="outlined"
startIcon={<LocationOnIcon />}
onClick={() => locatePipes([result.located_pipe])}
sx={{
height: 24,
minWidth: 0,
padding: "0 8px",
borderColor: "#bfdbfe",
color: "#2563eb",
fontSize: "0.75rem",
"&:hover": { borderColor: "#60a5fa", backgroundColor: "#eff6ff" },
}}
>
</Button>
</Box>
</Box>
<Box className="grid grid-cols-2 gap-3">
<MetricCard
label="定位管段"
value={result.located_pipe || "-"}
tone="blue"
/>
<MetricCard
label="估计漏损量"
value={`${result.burst_leakage.toFixed(2)} ${DMA_FLOW_DISPLAY_UNIT}`}
tone="orange"
/>
<MetricCard
label="最佳相似度"
value={`${(bestSimilarity * 100).toFixed(1)}%`}
tone="purple"
/>
<MetricCard
label="爆管时间"
value={burstTime}
tone="green"
/>
</Box>
</Box>
{/* Candidate List */}
<Box className="overflow-hidden rounded-xl border border-gray-100 bg-white shadow-sm">
<Box className="flex items-center justify-between border-b border-gray-100 bg-white px-4 py-3">
<Box className="flex items-center gap-2">
<FormatListBulleted className="h-5 w-5 text-blue-600" />
<Typography variant="subtitle1" className="font-bold text-gray-800"> <Typography variant="subtitle1" className="font-bold text-gray-800">
</Typography> </Typography>
@@ -230,35 +301,84 @@ const LocationResults: React.FC<Props> = ({ result }) => {
}} }}
/> />
</Box> </Box>
<Box className="max-h-64 overflow-y-auto"> <Table size="small">
{candidatePipes.map((candidate, idx) => ( <TableHead>
<Box <TableRow sx={{ backgroundColor: "#f8fafc" }}>
key={candidate.pipe_id} <TableCell sx={{ fontWeight: 600, color: "#64748b", py: 1.5, pl: 3 }}>
className="flex items-center justify-between px-4 py-3 border-b border-gray-50 last:border-0 hover:bg-gray-50 transition-colors"
> </TableCell>
<Box className="flex items-center gap-3"> <TableCell sx={{ fontWeight: 600, color: "#64748b", py: 1.5 }}>
<Box className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${idx === 0 ? "bg-yellow-100 text-yellow-700" : idx === 1 ? "bg-gray-100 text-gray-600" : idx === 2 ? "bg-orange-50 text-orange-600" : "bg-transparent text-gray-400"}`}> ID
{idx + 1} </TableCell>
</Box> <TableCell align="right" sx={{ fontWeight: 600, color: "#64748b", py: 1.5 }}>
<Box>
<Typography variant="body2" className="font-bold text-gray-700"> </TableCell>
{candidate.pipe_id} <TableCell align="right" sx={{ fontWeight: 600, color: "#64748b", py: 1.5, pr: 3 }}>
</Typography>
<Typography variant="caption" className="text-gray-400"> </TableCell>
: {(candidate.similarity * 100).toFixed(2)}% </TableRow>
</Typography> </TableHead>
</Box> <TableBody>
</Box> {candidatePipes.map((candidate, index) => {
<IconButton const similarityPercent = candidate.similarity * 100;
size="small" const isTop = index === 0;
onClick={() => locatePipes([candidate.pipe_id])} return (
className="text-gray-400 hover:text-blue-600" <TableRow
key={candidate.pipe_id}
hover
sx={{
"&:last-child td, &:last-child th": { border: 0 },
backgroundColor: isTop ? "#eff6ff" : "inherit",
}}
className="transition-colors"
> >
<LocationOnIcon fontSize="small" /> <TableCell sx={{ pl: 3, py: 1.2 }}>
</IconButton> <Box
</Box> className={`flex h-5 w-5 items-center justify-center rounded-full text-xs font-bold ${isTop ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-600"
))} }`}
</Box> >
{index + 1}
</Box>
</TableCell>
<TableCell sx={{ py: 1.2 }}>
<Typography
variant="body2"
className={`font-medium ${isTop ? "text-blue-700" : "text-gray-700"}`}
>
{candidate.pipe_id}
</Typography>
</TableCell>
<TableCell align="right" sx={{ py: 1.2 }}>
<Box className="flex flex-col items-end gap-1">
<Typography
variant="body2"
className={`font-medium ${isTop ? "text-blue-700" : "text-gray-700"}`}
>
{similarityPercent.toFixed(2)}%
</Typography>
<Box className="h-1.5 w-24 overflow-hidden rounded-full bg-gray-100">
<Box
className={`h-full rounded-full ${isTop ? "bg-blue-500" : "bg-gray-400"}`}
style={{ width: `${similarityPercent}%` }}
/>
</Box>
</Box>
</TableCell>
<TableCell align="right" sx={{ pr: 3, py: 1.2 }}>
<IconButton
size="small"
onClick={() => locatePipes([candidate.pipe_id])}
className="text-blue-600 hover:bg-blue-50"
title="定位"
>
<LocationOnIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</Box> </Box>
</Box> </Box>
); );
@@ -23,7 +23,12 @@ import dayjs, { Dayjs } from "dayjs";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { NETWORK_NAME, config } from "@config/config"; import { NETWORK_NAME, config } from "@config/config";
import { BurstLocationResult, BurstSchemeRecord } from "./types"; import {
BurstLocationResult,
BurstLocationSchemeDetail,
BurstSchemeRecord,
} from "./types";
import { DMA_FLOW_DISPLAY_UNIT } from "../DMALeakDetection/utils";
interface Props { interface Props {
onViewResult: (result: BurstLocationResult) => void; onViewResult: (result: BurstLocationResult) => void;
@@ -37,6 +42,39 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [expandedId, setExpandedId] = useState<number | null>(null); const [expandedId, setExpandedId] = useState<number | null>(null);
const buildDisplayResult = (
scheme: Pick<BurstSchemeRecord, "scheme_name" | "username" | "create_time">,
detail?: BurstLocationSchemeDetail,
): BurstLocationResult | null => {
const payload = detail?.result_payload;
const locatedPipe = payload?.located_pipe ?? detail?.result_summary?.located_pipe;
if (!locatedPipe) return null;
return {
located_pipe: locatedPipe,
burst_leakage: payload?.burst_leakage ?? detail?.algorithm_params?.burst_leakage ?? 0,
elapsed_seconds: payload?.elapsed_seconds ?? 0,
min_dpressure: payload?.min_dpressure ?? detail?.algorithm_params?.min_dpressure,
basic_pressure: payload?.basic_pressure ?? detail?.algorithm_params?.basic_pressure,
simulation_times: payload?.simulation_times ?? detail?.result_summary?.simulation_times ?? 0,
top_candidates: payload?.top_candidates ?? [],
similarity_mode:
payload?.similarity_mode ?? detail?.result_summary?.similarity_mode ?? "-",
scheme_name: payload?.scheme_name ?? scheme.scheme_name,
username: payload?.username ?? scheme.username,
network: payload?.network ?? detail?.network,
data_source: payload?.data_source,
observed_source: payload?.observed_source ?? detail?.observed_source,
pressure_scada_ids: payload?.pressure_scada_ids ?? detail?.pressure_scada_ids,
flow_scada_ids: payload?.flow_scada_ids ?? detail?.flow_scada_ids,
create_time: payload?.create_time ?? scheme.create_time,
scada_window: payload?.scada_window ?? detail?.scada_window,
pressure_samples: payload?.pressure_samples,
flow_samples: payload?.flow_samples,
simulation_scheme: payload?.simulation_scheme,
};
};
const handleQuery = async () => { const handleQuery = async () => {
setLoading(true); setLoading(true);
try { try {
@@ -47,7 +85,7 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult }) => {
if (!queryAll && queryDate) { if (!queryAll && queryDate) {
params.query_date = queryDate.startOf("day").toISOString(); params.query_date = queryDate.startOf("day").toISOString();
} }
const response = await api.get(url, { params }); const response = await api.get(url, { params });
setSchemes(response.data); setSchemes(response.data);
open?.({ open?.({
@@ -73,10 +111,23 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult }) => {
`${config.BACKEND_URL}/api/v1/burst-location/schemes/${encodeURIComponent(schemeName)}`, `${config.BACKEND_URL}/api/v1/burst-location/schemes/${encodeURIComponent(schemeName)}`,
{ params: { network: NETWORK_NAME } }, { params: { network: NETWORK_NAME } },
); );
// The backend returns { scheme_detail: ... } inside the response or just the result? const schemeRecord = response.data as BurstSchemeRecord & {
// Based on burst_location.py: get_burst_location_scheme_detail returns the stored detail. result_payload?: BurstLocationResult;
// Let's assume response.data is the BurstLocationResult };
onViewResult(response.data as BurstLocationResult); const normalizedResult =
schemeRecord.result_payload ??
buildDisplayResult(
{
scheme_name: schemeRecord.scheme_name,
username: schemeRecord.username,
create_time: schemeRecord.create_time,
},
schemeRecord.scheme_detail,
);
if (!normalizedResult) {
throw new Error("方案详情缺少定位结果数据");
}
onViewResult(normalizedResult);
open?.({ open?.({
type: "success", type: "success",
message: "方案加载成功", message: "方案加载成功",
@@ -169,80 +220,118 @@ const SchemeQuery: React.FC<Props> = ({ onViewResult }) => {
<Typography variant="caption" className="text-gray-500 px-2"> <Typography variant="caption" className="text-gray-500 px-2">
{schemes.length} {schemes.length}
</Typography> </Typography>
{schemes.map((scheme) => ( {schemes.map((scheme) => {
<Card key={scheme.scheme_id} variant="outlined" className="hover:shadow-md transition-shadow"> const summary = scheme.scheme_detail?.result_summary;
<CardContent className="p-3 pb-2 last:pb-3"> const payload = scheme.scheme_detail?.result_payload;
<Box className="flex items-start justify-between gap-2 mb-2"> const locatedPipe = payload?.located_pipe ?? summary?.located_pipe ?? "-";
<Box className="flex-1 min-w-0"> const leakage =
<Box className="flex items-center gap-2 mb-1"> payload?.burst_leakage ?? scheme.scheme_detail?.algorithm_params?.burst_leakage;
<Typography variant="body2" className="font-medium truncate" title={scheme.scheme_name}>
{scheme.scheme_name} return (
<Card
key={scheme.scheme_id}
variant="outlined"
className="hover:shadow-md transition-shadow"
>
<CardContent className="p-3 pb-2 last:pb-3">
<Box className="flex items-start justify-between gap-2 mb-2">
<Box className="flex-1 min-w-0">
<Box className="flex items-center gap-2 mb-1">
<Typography
variant="body2"
className="font-medium truncate"
title={scheme.scheme_name}
>
{scheme.scheme_name}
</Typography>
<Chip
size="small"
variant="outlined"
color={
payload?.data_source === "simulation" ? "secondary" : "primary"
}
label={
payload?.data_source === "simulation" ? "模拟方案" : "监测数据"
}
className="h-5"
/>
</Box>
{payload?.data_source === "simulation" &&
payload?.simulation_scheme?.name ? (
<Typography
variant="caption"
className="mb-1 block truncate text-xs text-purple-600"
title={payload.simulation_scheme.name}
>
: {payload.simulation_scheme.name}
</Typography>
) : null}
<Typography variant="caption" className="block text-gray-500">
ID: {scheme.scheme_id} · :{" "}
{dayjs(scheme.create_time).format("MM-DD HH:mm")}
</Typography> </Typography>
<Chip size="small" variant="outlined" color="primary" label="爆管定位" className="h-5" />
</Box> </Box>
<Typography variant="caption" className="text-gray-500 block"> <Box className="flex gap-1 ml-2">
ID: {scheme.scheme_id} · : {dayjs(scheme.create_time).format("MM-DD HH:mm")} <Tooltip title={expandedId === scheme.scheme_id ? "收起详情" : "查看详情"}>
</Typography> <IconButton
</Box> size="small"
<Box className="flex gap-1 ml-2"> onClick={() =>
<Tooltip title={expandedId === scheme.scheme_id ? "收起详情" : "查看详情"}> setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id)
<IconButton }
size="small" color="primary"
onClick={() => setExpandedId(expandedId === scheme.scheme_id ? null : scheme.scheme_id)} className="p-1"
color="primary" >
className="p-1" <InfoIcon fontSize="small" />
> </IconButton>
<InfoIcon fontSize="small" /> </Tooltip>
</IconButton>
</Tooltip>
</Box>
</Box>
<Collapse in={expandedId === scheme.scheme_id}>
<Box className="mt-2 pt-3 border-t border-gray-200">
<Box className="mb-3 rounded-md bg-gray-50 px-3 py-2 space-y-2">
{/* Summary details */}
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600">
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{scheme.scheme_detail?.located_pipe || "-"}
</Typography>
</Box>
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600">
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{scheme.scheme_detail?.burst_leakage ? `${scheme.scheme_detail.burst_leakage} L/s` : "-"}
</Typography>
</Box>
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600">
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{scheme.username || "-"}
</Typography>
</Box>
</Box>
<Box className="pt-2 border-t border-gray-100">
<Button
variant="contained"
fullWidth
size="small"
className="bg-blue-600 hover:bg-blue-700"
sx={{ textTransform: "none", fontWeight: 500 }}
onClick={() => handleViewSchemeResult(scheme.scheme_name)}
>
</Button>
</Box> </Box>
</Box> </Box>
</Collapse> <Collapse in={expandedId === scheme.scheme_id}>
</CardContent> <Box className="mt-2 pt-3 border-t border-gray-200">
</Card> <Box className="mb-3 rounded-md bg-gray-50 px-3 py-2 space-y-2">
))} <Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600">
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{locatedPipe}
</Typography>
</Box>
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600">
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{typeof leakage === "number" ? `${leakage} ${DMA_FLOW_DISPLAY_UNIT}` : "-"}
</Typography>
</Box>
<Box className="grid grid-cols-[78px_1fr] items-center gap-x-2">
<Typography variant="caption" className="text-gray-600">
:
</Typography>
<Typography variant="caption" className="font-medium text-gray-900">
{scheme.username || "-"}
</Typography>
</Box>
</Box>
<Box className="pt-2 border-t border-gray-100">
<Button
variant="contained"
fullWidth
size="small"
className="bg-blue-600 hover:bg-blue-700"
sx={{ textTransform: "none", fontWeight: 500 }}
onClick={() => handleViewSchemeResult(scheme.scheme_name)}
>
</Button>
</Box>
</Box>
</Collapse>
</CardContent>
</Card>
);
})}
</Box> </Box>
)} )}
</Box> </Box>
+49 -1
View File
@@ -13,15 +13,63 @@ export interface BurstLocationResult {
scheme_name?: string; scheme_name?: string;
username?: string; username?: string;
observed_source?: string; observed_source?: string;
network?: string;
data_source?: string;
min_dpressure?: number;
basic_pressure?: number;
pressure_scada_ids?: string[]; pressure_scada_ids?: string[];
flow_scada_ids?: string[]; flow_scada_ids?: string[];
create_time?: string; create_time?: string;
scada_window?: {
burst_start?: string;
burst_end?: string;
normal_start?: string;
normal_end?: string;
};
pressure_samples?: {
burst?: number;
normal?: number;
};
flow_samples?: {
burst?: number;
normal?: number;
};
simulation_scheme?: {
name?: string;
type?: string;
};
}
export interface BurstLocationSchemeDetail {
network?: string;
pressure_scada_ids?: string[];
flow_scada_ids?: string[];
observed_source?: string;
algorithm_params?: {
burst_leakage?: number;
min_dpressure?: number;
basic_pressure?: number;
};
scada_window?: {
burst_start?: string;
burst_end?: string;
normal_start?: string;
normal_end?: string;
};
result_summary?: {
located_pipe?: string;
simulation_times?: number;
similarity_mode?: string;
};
result_payload?: BurstLocationResult;
} }
export interface BurstSchemeRecord { export interface BurstSchemeRecord {
scheme_id: number; scheme_id: number;
scheme_name: string; scheme_name: string;
scheme_type?: string;
create_time: string; create_time: string;
scheme_start_time?: string;
username?: string; username?: string;
scheme_detail?: BurstLocationResult; scheme_detail?: BurstLocationSchemeDetail;
} }