前端项目结构调整
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Link,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
LocationOn as LocationIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import { GeoJSON } from "ol/format";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
import { Stroke, Style, Icon } from "ol/style";
|
||||
import Feature, { FeatureLike } from "ol/Feature";
|
||||
import {
|
||||
along,
|
||||
lineString,
|
||||
length,
|
||||
toMercator,
|
||||
bbox,
|
||||
featureCollection,
|
||||
} from "@turf/turf";
|
||||
import { Point } from "ol/geom";
|
||||
import { toLonLat } from "ol/proj";
|
||||
import moment from "moment";
|
||||
import "moment-timezone";
|
||||
import { LocationResult } from "./types";
|
||||
import { FLOW_DISPLAY_UNIT } from "@utils/units";
|
||||
|
||||
interface LocationResultsProps {
|
||||
results?: LocationResult[];
|
||||
}
|
||||
|
||||
const LocationResults: React.FC<LocationResultsProps> = ({
|
||||
results = [],
|
||||
}) => {
|
||||
const [highlightLayer, setHighlightLayer] =
|
||||
useState<VectorLayer<VectorSource> | null>(null);
|
||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
||||
const map = useMap();
|
||||
|
||||
// 格式化时间为 UTC+8
|
||||
const formatTime = (timeStr: string) => {
|
||||
return moment(timeStr).utcOffset(8).format("YYYY-MM-DD HH:mm:ss");
|
||||
};
|
||||
|
||||
const handleLocatePipes = (pipeIds: string[]) => {
|
||||
if (pipeIds.length > 0) {
|
||||
queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => {
|
||||
if (features.length > 0) {
|
||||
// 设置高亮要素
|
||||
setHighlightFeatures(features);
|
||||
// 将 OpenLayers Feature 转换为 GeoJSON Feature
|
||||
const geojsonFormat = new GeoJSON();
|
||||
const geojsonFeatures = features.map((feature) =>
|
||||
geojsonFormat.writeFeatureObject(feature),
|
||||
);
|
||||
|
||||
const extent = bbox(featureCollection(geojsonFeatures as any));
|
||||
|
||||
if (extent) {
|
||||
map?.getView().fit(extent, { maxZoom: 18, duration: 1000 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化管道图层和高亮图层
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const burstPipeStyle = function (feature: FeatureLike) {
|
||||
const styles = [];
|
||||
// 线条样式(底层发光,主线条,内层高亮线)
|
||||
styles.push(
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: "rgba(255, 0, 0, 0.3)",
|
||||
width: 12,
|
||||
}),
|
||||
}),
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: "rgba(255, 0, 0, 1)",
|
||||
width: 6,
|
||||
lineDash: [15, 10],
|
||||
}),
|
||||
}),
|
||||
new Style({
|
||||
stroke: new Stroke({
|
||||
color: "rgba(255, 102, 102, 1)",
|
||||
width: 3,
|
||||
lineDash: [15, 10],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const geometry = feature.getGeometry();
|
||||
const lineCoords =
|
||||
geometry?.getType() === "LineString"
|
||||
? (geometry as any).getCoordinates()
|
||||
: null;
|
||||
if (geometry && lineCoords) {
|
||||
const lineCoordsWGS84 = lineCoords.map((coord: []) => {
|
||||
const [lon, lat] = toLonLat(coord);
|
||||
return [lon, lat];
|
||||
});
|
||||
// 计算中点
|
||||
const lineStringFeature = lineString(lineCoordsWGS84);
|
||||
const lineLength = length(lineStringFeature);
|
||||
const midPoint = along(lineStringFeature, lineLength / 2).geometry
|
||||
.coordinates;
|
||||
// 在中点添加 icon 样式
|
||||
const midPointMercator = toMercator(midPoint);
|
||||
styles.push(
|
||||
new Style({
|
||||
geometry: new Point(midPointMercator),
|
||||
image: new Icon({
|
||||
src: "/icons/burst_pipe.svg",
|
||||
scale: 0.2,
|
||||
anchor: [0.5, 1],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return styles;
|
||||
};
|
||||
// 创建高亮图层
|
||||
const highlightLayer = new VectorLayer({
|
||||
source: new VectorSource(),
|
||||
style: burstPipeStyle,
|
||||
maxZoom: 24,
|
||||
minZoom: 12,
|
||||
properties: {
|
||||
name: "爆管管段高亮",
|
||||
value: "burst_pipe_highlight",
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer(highlightLayer);
|
||||
setHighlightLayer(highlightLayer);
|
||||
|
||||
return () => {
|
||||
map.removeLayer(highlightLayer);
|
||||
};
|
||||
}, [map]);
|
||||
|
||||
// 高亮要素的函数
|
||||
useEffect(() => {
|
||||
if (!highlightLayer) {
|
||||
return;
|
||||
}
|
||||
const source = highlightLayer.getSource();
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
// 清除之前的高亮
|
||||
source.clear();
|
||||
// 添加新的高亮要素
|
||||
highlightFeatures.forEach((feature) => {
|
||||
if (feature instanceof Feature) {
|
||||
source.addFeature(feature);
|
||||
}
|
||||
});
|
||||
}, [highlightFeatures, highlightLayer]);
|
||||
|
||||
// 取第一条记录或空对象
|
||||
const result = results.length > 0 ? results[0] : null;
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col h-full">
|
||||
{/* 结果展示 */}
|
||||
<Box className="flex-1 overflow-auto bg-white rounded border border-gray-200">
|
||||
{!result ? (
|
||||
<Box className="flex flex-col items-center justify-center h-full text-gray-400 p-4">
|
||||
<Box className="mb-4">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
fill="none"
|
||||
className="opacity-40"
|
||||
>
|
||||
<circle
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<circle cx="40" cy="40" r="5" fill="currentColor" />
|
||||
<line
|
||||
x1="40"
|
||||
y1="15"
|
||||
x2="40"
|
||||
y2="25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="40"
|
||||
y1="55"
|
||||
x2="40"
|
||||
y2="65"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="15"
|
||||
y1="40"
|
||||
x2="25"
|
||||
y2="40"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="55"
|
||||
y1="40"
|
||||
x2="65"
|
||||
y2="40"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
<Typography variant="body2">暂无定位结果</Typography>
|
||||
<Typography variant="body2" className="mt-1">
|
||||
请先执行方案分析
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className="p-5 h-full overflow-auto">
|
||||
{/* 头部:标识信息 */}
|
||||
<Box className="mb-5">
|
||||
<Box className="flex items-center gap-2 mb-1">
|
||||
<Typography
|
||||
variant="h6"
|
||||
className="font-bold text-gray-900"
|
||||
title={result.burst_incident}
|
||||
>
|
||||
{result.burst_incident}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={
|
||||
result.type === "burst_analysis" ? "爆管模拟" : result.type
|
||||
}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.75rem",
|
||||
height: "24px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" className="text-gray-500">
|
||||
ID: {result.id}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 主要信息:三栏卡片布局 */}
|
||||
<Box className="grid grid-cols-3 gap-3 mb-5">
|
||||
{/* 检测时间卡片 */}
|
||||
<Box className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-3 border border-blue-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<Box className="flex items-center gap-1.5 mb-2">
|
||||
<Box className="w-1.5 h-1.5 rounded-full bg-blue-600"></Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
className="text-blue-700 font-semibold uppercase tracking-wide"
|
||||
sx={{ fontSize: "0.7rem" }}
|
||||
>
|
||||
检测时间
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="font-bold text-blue-900 leading-tight"
|
||||
sx={{ fontSize: "0.875rem" }}
|
||||
>
|
||||
{formatTime(result.detect_time)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 漏损量卡片 */}
|
||||
<Box className="bg-gradient-to-br from-orange-50 to-orange-100 rounded-lg p-3 border border-orange-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<Box className="flex items-center gap-1.5 mb-2">
|
||||
<Box className="w-1.5 h-1.5 rounded-full bg-orange-600"></Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
className="text-orange-700 font-semibold uppercase tracking-wide"
|
||||
sx={{ fontSize: "0.7rem" }}
|
||||
>
|
||||
漏损量
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="font-bold text-orange-900"
|
||||
sx={{ fontSize: "0.875rem" }}
|
||||
>
|
||||
{result.leakage !== null
|
||||
? `${result.leakage.toFixed(2)} ${FLOW_DISPLAY_UNIT}`
|
||||
: "N/A"}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 定位管段数量卡片 */}
|
||||
<Box className="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-3 border border-green-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
<Box className="flex items-center gap-1.5 mb-2">
|
||||
<Box className="w-1.5 h-1.5 rounded-full bg-green-600"></Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
className="text-green-700 font-semibold uppercase tracking-wide"
|
||||
sx={{ fontSize: "0.7rem" }}
|
||||
>
|
||||
定位管段
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="font-bold text-green-900"
|
||||
sx={{ fontSize: "0.875rem" }}
|
||||
>
|
||||
{result.locate_result ? result.locate_result.length : 0}{" "}
|
||||
个管段
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 定位管段详细列表 */}
|
||||
{result.locate_result && result.locate_result.length > 0 && (
|
||||
<Box className="bg-white rounded-lg p-4 border-2 border-blue-200 shadow-sm">
|
||||
<Box className="flex items-center justify-between mb-3">
|
||||
<Typography
|
||||
variant="body1"
|
||||
className="text-gray-900 font-bold"
|
||||
sx={{ fontSize: "0.95rem" }}
|
||||
>
|
||||
管段列表
|
||||
</Typography>
|
||||
<Box className="flex items-center gap-2">
|
||||
<Tooltip title="定位所有管道">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleLocatePipes(result.locate_result!)}
|
||||
color="primary"
|
||||
sx={{
|
||||
backgroundColor: "rgba(37, 125, 212, 0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(37, 125, 212, 0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LocationIcon sx={{ fontSize: "1.2rem" }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className="grid grid-cols-2 gap-2">
|
||||
{result.locate_result.map((pipeId, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
className="bg-gradient-to-r from-blue-50 to-white rounded-lg px-3 py-2 border border-blue-200 hover:border-blue-400 hover:shadow-md transition-all cursor-pointer group"
|
||||
onClick={() => handleLocatePipes([pipeId])}
|
||||
sx={{
|
||||
"&:active": {
|
||||
transform: "scale(0.98)",
|
||||
boxShadow: "0 1px 2px rgba(25, 118, 210, 0.2)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box className="flex items-center justify-between">
|
||||
<Typography
|
||||
variant="body2"
|
||||
className="font-semibold text-blue-700 group-hover:text-blue-900"
|
||||
>
|
||||
{pipeId}
|
||||
</Typography>
|
||||
<Box className="flex items-center gap-1">
|
||||
{/* <Tooltip title="定位管段">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleLocatePipes([pipeId]);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(37, 125, 212, 0.1)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LocationIcon sx={{ fontSize: "1rem" }} />
|
||||
</IconButton>
|
||||
</Tooltip> */}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationResults;
|
||||
Reference in New Issue
Block a user