新增水质模拟模块;移除docker配置文件,现放置到后端项目中
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} 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 { useNotification } from "@refinedev/core";
|
||||
import axios from "axios";
|
||||
import { config, NETWORK_NAME } from "@/config/config";
|
||||
import { useMap } from "@app/OlMap/MapComponent";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorSource from "ol/source/Vector";
|
||||
import { Style, Stroke, Fill, Circle as CircleStyle } from "ol/style";
|
||||
import Feature from "ol/Feature";
|
||||
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
||||
|
||||
const AnalysisParameters: React.FC = () => {
|
||||
const map = useMap();
|
||||
const { open } = useNotification();
|
||||
|
||||
const network = NETWORK_NAME;
|
||||
const [startTime, setStartTime] = useState<Dayjs | null>(dayjs(new Date()));
|
||||
const [sourceNode, setSourceNode] = useState<string>("");
|
||||
const [concentration, setConcentration] = useState<number>(1);
|
||||
const [duration, setDuration] = useState<number>(900);
|
||||
const [pattern, setPattern] = useState<string>("");
|
||||
const [isSelecting, setIsSelecting] = useState<boolean>(false);
|
||||
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||
|
||||
const [highlightLayer, setHighlightLayer] =
|
||||
useState<VectorLayer<VectorSource> | null>(null);
|
||||
const [highlightFeature, setHighlightFeature] = useState<Feature | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const isFormValid = useMemo(() => {
|
||||
return (
|
||||
Boolean(network) &&
|
||||
Boolean(startTime) &&
|
||||
Boolean(sourceNode) &&
|
||||
concentration > 0 &&
|
||||
duration > 0
|
||||
);
|
||||
}, [network, startTime, sourceNode, concentration, duration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const sourceStyle = new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 10,
|
||||
fill: new Fill({ color: "rgba(37, 125, 212, 0.35)" }),
|
||||
stroke: new Stroke({ color: "rgba(37, 125, 212, 1)", width: 3 }),
|
||||
}),
|
||||
});
|
||||
|
||||
const layer = new VectorLayer({
|
||||
source: new VectorSource(),
|
||||
style: sourceStyle,
|
||||
properties: {
|
||||
name: "污染源节点",
|
||||
value: "contaminant_source_highlight",
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer(layer);
|
||||
setHighlightLayer(layer);
|
||||
|
||||
return () => {
|
||||
map.removeLayer(layer);
|
||||
map.un("click", handleMapClickSelectFeatures);
|
||||
};
|
||||
}, [map]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!highlightLayer) return;
|
||||
const source = highlightLayer.getSource();
|
||||
if (!source) return;
|
||||
source.clear();
|
||||
if (highlightFeature) {
|
||||
source.addFeature(highlightFeature);
|
||||
}
|
||||
}, [highlightFeature, highlightLayer]);
|
||||
|
||||
const handleMapClickSelectFeatures = useCallback(
|
||||
async (event: { coordinate: number[] }) => {
|
||||
if (!map) return;
|
||||
const feature = await mapClickSelectFeatures(event, map);
|
||||
if (!feature) return;
|
||||
|
||||
const layerId = feature.getId()?.toString().split(".")[0] || "";
|
||||
const isJunction = layerId.includes("junction");
|
||||
if (!isJunction) {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "请选择节点类型要素作为污染源。",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const id = feature.getProperties().id;
|
||||
if (!id) return;
|
||||
setSourceNode(id);
|
||||
setHighlightFeature(feature);
|
||||
setIsSelecting(false);
|
||||
map.un("click", handleMapClickSelectFeatures);
|
||||
},
|
||||
[map, open]
|
||||
);
|
||||
|
||||
const handleStartSelection = () => {
|
||||
if (!map) return;
|
||||
setIsSelecting(true);
|
||||
map.on("click", handleMapClickSelectFeatures);
|
||||
};
|
||||
|
||||
const handleEndSelection = () => {
|
||||
if (!map) return;
|
||||
setIsSelecting(false);
|
||||
map.un("click", handleMapClickSelectFeatures);
|
||||
};
|
||||
|
||||
const handleClearSource = () => {
|
||||
setSourceNode("");
|
||||
setHighlightFeature(null);
|
||||
};
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
if (!startTime) return;
|
||||
setSubmitting(true);
|
||||
open?.({
|
||||
key: "contaminant-analysis",
|
||||
type: "progress",
|
||||
message: "方案提交分析中",
|
||||
undoableTimeout: 3,
|
||||
});
|
||||
|
||||
try {
|
||||
const params = {
|
||||
network,
|
||||
start_time: startTime.toISOString(),
|
||||
source: sourceNode,
|
||||
concentration,
|
||||
duration,
|
||||
pattern: pattern || undefined,
|
||||
};
|
||||
|
||||
await axios.get(`${config.BACKEND_URL}/contaminant_simulation/`, {
|
||||
params,
|
||||
});
|
||||
|
||||
open?.({
|
||||
key: "contaminant-analysis",
|
||||
type: "success",
|
||||
message: "方案分析成功",
|
||||
description: "水质模拟完成,请在方案查询中查看结果。",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("水质模拟请求失败:", error);
|
||||
open?.({
|
||||
key: "contaminant-analysis",
|
||||
type: "error",
|
||||
message: "提交分析失败",
|
||||
description:
|
||||
error instanceof Error ? error.message : "请检查网络连接或稍后重试",
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(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}>
|
||||
{sourceNode ? (
|
||||
<Box className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||
<Typography className="flex-shrink-0 text-sm">
|
||||
{sourceNode}
|
||||
</Typography>
|
||||
<Typography className="flex-shrink-0 text-sm text-gray-600">
|
||||
污染源节点
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleClearSource}
|
||||
className="ml-auto"
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className="p-3 rounded border border-dashed border-gray-200 text-sm text-gray-400">
|
||||
暂未选择污染源节点
|
||||
</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"
|
||||
value={network}
|
||||
disabled
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box className="mb-4">
|
||||
<Typography variant="subtitle2" className="mb-2 font-medium">
|
||||
污染源浓度 (mg/L)
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
type="number"
|
||||
value={concentration}
|
||||
onChange={(e) => setConcentration(parseFloat(e.target.value) || 0)}
|
||||
placeholder="输入浓度"
|
||||
/>
|
||||
</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, 10) || 0)}
|
||||
placeholder="输入持续时长"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box className="mb-4">
|
||||
<Typography variant="subtitle2" className="mb-2 font-medium">
|
||||
时间模式
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
value={pattern}
|
||||
onChange={(e) => setPattern(e.target.value)}
|
||||
placeholder="可选,输入 pattern 名称"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box className="mt-auto">
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={handleAnalyze}
|
||||
disabled={submitting || !isFormValid}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{submitting ? "方案提交分析中..." : "水质模拟"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisParameters;
|
||||
Reference in New Issue
Block a user