更新爆管定位功能,优化数据处理和展示
This commit is contained in:
@@ -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 } }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,18 +3,20 @@
|
|||||||
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,
|
||||||
Paper,
|
Table,
|
||||||
Tooltip,
|
TableBody,
|
||||||
Typography
|
TableCell,
|
||||||
|
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]);
|
||||||
@@ -88,131 +168,122 @@ const LocationResults: React.FC<Props> = ({ result }) => {
|
|||||||
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 {
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user