前端项目结构调整
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
IconButton,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import { Close as CloseIcon } from "@mui/icons-material";
|
||||
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
|
||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { zhCN as pickerZhCN } from "@mui/x-date-pickers/locales";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import "dayjs/locale/zh-cn"; // 引入中文包
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { useMap } from "@components/olmap/core/MapComponent";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
import { Style, Stroke, Icon } from "ol/style";
|
||||
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
||||
import Feature, { FeatureLike } from "ol/Feature";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import { api } from "@/lib/api";
|
||||
import { config, NETWORK_NAME } from "@/config/config";
|
||||
import { along, lineString, length, toMercator } from "@turf/turf";
|
||||
import { Point } from "ol/geom";
|
||||
import { toLonLat } from "ol/proj";
|
||||
|
||||
interface PipePoint {
|
||||
id: string;
|
||||
diameter: number;
|
||||
area: number;
|
||||
feature?: any; // 存储管道要素用于高亮
|
||||
}
|
||||
|
||||
const AnalysisParameters: React.FC = () => {
|
||||
const map = useMap();
|
||||
const { open } = useNotification();
|
||||
|
||||
const [pipePoints, setPipePoints] = useState<PipePoint[]>([]);
|
||||
const [startTime, setStartTime] = useState<Dayjs | null>(dayjs(new Date()));
|
||||
const [duration, setDuration] = useState<number>(3600);
|
||||
const [schemeName, setSchemeName] = useState<string>(
|
||||
"FANGAN" + new Date().getTime(),
|
||||
);
|
||||
const [network, setNetwork] = useState<string>(NETWORK_NAME);
|
||||
const [isSelecting, setIsSelecting] = useState<boolean>(false);
|
||||
|
||||
const [highlightLayer, setHighlightLayer] =
|
||||
useState<VectorLayer<VectorSource> | null>(null);
|
||||
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
|
||||
const [analyzing, setAnalyzing] = useState<boolean>(false);
|
||||
|
||||
// 检查是否所有必要参数都已填写
|
||||
const isFormValid =
|
||||
pipePoints.length > 0 &&
|
||||
startTime !== null &&
|
||||
duration > 0 &&
|
||||
schemeName.trim() !== "";
|
||||
|
||||
// 初始化管道图层和高亮图层
|
||||
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) {
|
||||
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,
|
||||
properties: {
|
||||
name: "高亮管道",
|
||||
value: "highlight_pipeline",
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer(highlightLayer);
|
||||
setHighlightLayer(highlightLayer);
|
||||
|
||||
return () => {
|
||||
map.removeLayer(highlightLayer);
|
||||
map.un("click", handleMapClickSelectFeatures);
|
||||
};
|
||||
}, [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]);
|
||||
|
||||
// 同步高亮要素和爆管点信息
|
||||
useEffect(() => {
|
||||
setPipePoints((prevPipes) => {
|
||||
// 移除不在highlightFeatures中的
|
||||
const filtered = prevPipes.filter((pipe) =>
|
||||
highlightFeatures.some(
|
||||
(feature) => feature.getProperties().id === pipe.id,
|
||||
),
|
||||
);
|
||||
// 添加新的
|
||||
const newPipes = highlightFeatures
|
||||
.filter(
|
||||
(feature) =>
|
||||
!filtered.some((p) => p.id === feature.getProperties().id),
|
||||
)
|
||||
.map((feature) => {
|
||||
const properties = feature.getProperties();
|
||||
return {
|
||||
id: properties.id,
|
||||
diameter: properties.diameter || 0,
|
||||
area: 15,
|
||||
feature: feature,
|
||||
};
|
||||
});
|
||||
return [...filtered, ...newPipes];
|
||||
});
|
||||
}, [highlightFeatures]);
|
||||
|
||||
// 地图点击选择要素事件处理函数
|
||||
const handleMapClickSelectFeatures = useCallback(
|
||||
async (event: { coordinate: number[] }) => {
|
||||
if (!map) return;
|
||||
const feature = await mapClickSelectFeatures(event, map);
|
||||
const layer = feature?.getId()?.toString().split(".")[0];
|
||||
|
||||
if (!feature) return;
|
||||
if (
|
||||
feature.getGeometry()?.getType() === "Point" ||
|
||||
(layer !== "geo_pipes_mat" && layer !== "geo_pipes")
|
||||
) {
|
||||
// 点类型几何不处理
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "请选择线类型管道要素。",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const featureId = feature.getProperties().id;
|
||||
setHighlightFeatures((prev) => {
|
||||
const existingIndex = prev.findIndex(
|
||||
(f) => f.getProperties().id === featureId,
|
||||
);
|
||||
if (existingIndex !== -1) {
|
||||
// 如果已存在,移除
|
||||
return prev.filter((_, i) => i !== existingIndex);
|
||||
} else {
|
||||
// 如果不存在,添加
|
||||
return [...prev, feature];
|
||||
}
|
||||
});
|
||||
},
|
||||
[map],
|
||||
);
|
||||
|
||||
// 开始选择管道
|
||||
const handleStartSelection = () => {
|
||||
if (!map) return;
|
||||
setIsSelecting(true);
|
||||
// 注册点击事件
|
||||
map.on("click", handleMapClickSelectFeatures);
|
||||
};
|
||||
|
||||
// 结束选择管道
|
||||
const handleEndSelection = () => {
|
||||
if (!map) return;
|
||||
|
||||
setIsSelecting(false);
|
||||
|
||||
// 移除点击事件
|
||||
map.un("click", handleMapClickSelectFeatures);
|
||||
};
|
||||
|
||||
const handleRemovePipe = (id: string) => {
|
||||
// 从高亮features中移除
|
||||
setHighlightFeatures((prev) =>
|
||||
prev.filter((f) => f.getProperties().id !== id),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAreaChange = (id: string, value: string) => {
|
||||
const numValue = parseFloat(value) || 0;
|
||||
setPipePoints((prev) =>
|
||||
prev.map((pipe) => (pipe.id === id ? { ...pipe, area: numValue } : pipe)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
setAnalyzing(true);
|
||||
|
||||
// 显示处理中的通知
|
||||
open?.({
|
||||
key: "burst-analysis",
|
||||
type: "progress",
|
||||
message: "方案提交分析中",
|
||||
undoableTimeout: 3,
|
||||
});
|
||||
|
||||
const burst_ID = pipePoints.map((pipe) => pipe.id);
|
||||
const burst_size = pipePoints.map((pipe) =>
|
||||
parseInt(pipe.area.toString(), 10),
|
||||
);
|
||||
// 格式化开始时间,去除秒部分
|
||||
const modify_pattern_start_time = startTime
|
||||
? startTime.format("YYYY-MM-DDTHH:mm:00Z")
|
||||
: "";
|
||||
const modify_total_duration = duration;
|
||||
const params = {
|
||||
network: network,
|
||||
modify_pattern_start_time: modify_pattern_start_time,
|
||||
burst_ID: burst_ID,
|
||||
burst_size: burst_size,
|
||||
modify_total_duration: modify_total_duration,
|
||||
scheme_name: schemeName,
|
||||
};
|
||||
|
||||
try {
|
||||
await api.get(`${config.BACKEND_URL}/api/v1/burst_analysis/`, {
|
||||
params,
|
||||
paramsSerializer: {
|
||||
indexes: null, // 移除数组索引,即由 burst_ID[] 变为 burst_ID
|
||||
},
|
||||
});
|
||||
// 更新弹窗为成功状态
|
||||
open?.({
|
||||
key: "burst-analysis",
|
||||
type: "success",
|
||||
message: "方案分析成功",
|
||||
description: "方案分析完成,请在方案查询中查看结果。",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("分析请求失败:", error);
|
||||
|
||||
// 更新弹窗为失败状态
|
||||
open?.({
|
||||
key: "burst-analysis",
|
||||
type: "error",
|
||||
message: "提交分析失败",
|
||||
description:
|
||||
error instanceof Error ? error.message : "请检查网络连接或稍后重试",
|
||||
});
|
||||
} finally {
|
||||
setAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col h-full">
|
||||
{/* 选择爆管点 */}
|
||||
<Box className="mb-4">
|
||||
<Box className="flex items-center justify-between mb-2">
|
||||
<Typography variant="subtitle2" className="font-medium">
|
||||
选择爆管点
|
||||
</Typography>
|
||||
{/* 开始/结束选择按钮 */}
|
||||
{!isSelecting ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleStartSelection}
|
||||
className="border-blue-500 text-blue-600 hover:bg-blue-50"
|
||||
startIcon={
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
开始选择
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleEndSelection}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
startIcon={
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
结束选择
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isSelecting && (
|
||||
<Box className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||
💡 点击地图上的管道添加爆管点
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Stack spacing={2}>
|
||||
{pipePoints.map((pipe) => (
|
||||
<Box
|
||||
key={pipe.id}
|
||||
className="flex items-center gap-2 p-2 bg-gray-50 rounded"
|
||||
>
|
||||
<Typography className="flex-shrink-0 text-sm pl-1">
|
||||
{pipe.id}
|
||||
</Typography>
|
||||
<Typography className="flex-shrink-0 text-sm text-gray-600">
|
||||
管径: {pipe.diameter} mm
|
||||
</Typography>
|
||||
<Typography className="flex-shrink-0 text-sm text-gray-600 mr-2">
|
||||
爆管点面积
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={pipe.area}
|
||||
onChange={(e) => handleAreaChange(pipe.id, e.target.value)}
|
||||
type="number"
|
||||
className="w-25"
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<span className="text-xs text-gray-500">cm²</span>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleRemovePipe(pipe.id)}
|
||||
className="ml-auto"
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* 选择开始时间 */}
|
||||
<Box className="mb-4">
|
||||
<Typography variant="subtitle2" className="mb-2 font-medium">
|
||||
选择开始时间
|
||||
</Typography>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||||
<DateTimePicker
|
||||
value={startTime}
|
||||
onChange={(value) =>
|
||||
value && dayjs.isDayjs(value) && setStartTime(value)
|
||||
}
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
fullWidth: true,
|
||||
},
|
||||
}}
|
||||
localeText={
|
||||
pickerZhCN.components.MuiLocalizationProvider.defaultProps
|
||||
.localeText
|
||||
}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</Box>
|
||||
|
||||
{/* 持续时长 */}
|
||||
<Box className="mb-4">
|
||||
<Typography variant="subtitle2" className="mb-2 font-medium">
|
||||
持续时长 (秒)
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
type="number"
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(parseInt(e.target.value) || 0)}
|
||||
placeholder="输入持续时长"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 方案名称 */}
|
||||
<Box className="mb-4">
|
||||
<Typography variant="subtitle2" className="mb-2 font-medium">
|
||||
方案名称
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
value={schemeName}
|
||||
onChange={(e) => setSchemeName(e.target.value)}
|
||||
placeholder="输入方案名称"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 方案分析按钮 */}
|
||||
<Box className="mt-auto">
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={handleAnalyze}
|
||||
disabled={analyzing || !isFormValid}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{analyzing ? "方案提交分析中..." : "方案分析"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisParameters;
|
||||
Reference in New Issue
Block a user