添加爆管定位功能及相关组件

This commit is contained in:
JIANG
2026-03-07 10:50:07 +08:00
parent 9beba1cf6f
commit 5ed6740a24
11 changed files with 1247 additions and 14 deletions
@@ -0,0 +1,267 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
import {
Box,
Button,
Chip,
Divider,
IconButton,
Paper,
Tooltip,
Typography
} from "@mui/material";
import {
FormatListBulleted,
LocationOn as LocationOnIcon,
Map as MapIcon
} from "@mui/icons-material";
import dayjs from "dayjs";
import { useMap } from "@app/OlMap/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 { Stroke, Style, Circle, Fill } from "ol/style";
import { bbox, featureCollection } from "@turf/turf";
import { BurstLocationResult } from "./types";
interface Props {
result: BurstLocationResult | null;
}
const LocationResults: React.FC<Props> = ({ result }) => {
const map = useMap();
const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const candidatePipes = useMemo(() => {
if (!result) return [];
const base = result.top_candidates ?? [];
const hasLocated = base.some((item) => item.pipe_id === result.located_pipe);
if (result.located_pipe && !hasLocated) {
return [{ pipe_id: result.located_pipe, similarity: 1.0 }, ...base];
}
return base;
}, [result]);
useEffect(() => {
if (!map) return;
const layer = new VectorLayer({
source: new VectorSource(),
style: new Style({
stroke: new Stroke({
color: "#ef4444",
width: 6,
}),
image: new Circle({
radius: 8,
fill: new Fill({ color: "#ef4444" }),
stroke: new Stroke({ color: "#fff", width: 2 }),
}),
zIndex: 999,
}),
properties: {
name: "爆管定位高亮",
value: "burst_location_highlight",
},
});
map.addLayer(layer);
setHighlightLayer(layer);
return () => {
map.removeLayer(layer);
};
}, [map]);
useEffect(() => {
const source = highlightLayer?.getSource();
if (!source) return;
source.clear();
highlightFeatures.forEach((feature) => source.addFeature(feature));
}, [highlightFeatures, highlightLayer]);
const locatePipes = async (pipeIds: string[]) => {
if (!pipeIds.length || !map) return;
try {
// 尝试两个图层
let features = await queryFeaturesByIds(pipeIds, "geo_pipes_mat");
if (features.length === 0) {
features = await queryFeaturesByIds(pipeIds, "geo_pipes");
}
if (features.length === 0) return;
setHighlightFeatures(features);
const geojsonFormat = new GeoJSON();
const geojsonFeatures = features.map((feature) =>
geojsonFormat.writeFeatureObject(feature),
);
// @ts-ignore
const extent = bbox(featureCollection(geojsonFeatures));
map.getView().fit(extent, {
maxZoom: 19,
duration: 1000,
padding: [100, 100, 100, 100],
});
} catch (e) {
console.error("Locate failed", e);
}
};
if (!result) {
return (
<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>
);
}
return (
<Box className="h-full overflow-y-auto bg-gray-50 p-3 space-y-3">
{/* 1. 冠军卡片 */}
<Box className="flex items-center justify-between px-1 mb-2">
<Box className="flex items-center gap-2">
<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">
<FormatListBulleted className="text-blue-600 w-5 h-5" />
<Typography variant="subtitle1" className="font-bold text-gray-800">
</Typography>
</Box>
<Chip
size="small"
label={`${candidatePipes.length}`}
sx={{
height: 22,
backgroundColor: "rgba(37, 99, 235, 0.08)",
color: "#2563eb",
fontWeight: 600,
fontSize: "0.75rem",
border: "none",
}}
/>
</Box>
<Box className="max-h-64 overflow-y-auto">
{candidatePipes.map((candidate, idx) => (
<Box
key={candidate.pipe_id}
className="flex items-center justify-between px-4 py-3 border-b border-gray-50 last:border-0 hover:bg-gray-50 transition-colors"
>
<Box className="flex items-center gap-3">
<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"}`}>
{idx + 1}
</Box>
<Box>
<Typography variant="body2" className="font-bold text-gray-700">
{candidate.pipe_id}
</Typography>
<Typography variant="caption" className="text-gray-400">
: {(candidate.similarity * 100).toFixed(2)}%
</Typography>
</Box>
</Box>
<IconButton
size="small"
onClick={() => locatePipes([candidate.pipe_id])}
className="text-gray-400 hover:text-blue-600"
>
<LocationOnIcon fontSize="small" />
</IconButton>
</Box>
))}
</Box>
</Box>
</Box>
);
};
export default LocationResults;