"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Box, TextField, Button, Typography, IconButton, Stack, Alert, Divider, } 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, Fill, Circle as CircleStyle } 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"; interface ValveItem { id: string; k: number; feature?: any; } const AnalysisParameters: React.FC = () => { const map = useMap(); const { open } = useNotification(); // State const [schemeName, setSchemeName] = useState( "Flushing_" + new Date().getTime(), ); const [valves, setValves] = useState([]); const [drainageNode, setDrainageNode] = useState(null); const [drainageFeature, setDrainageFeature] = useState(null); const [startTime, setStartTime] = useState(dayjs(new Date())); const [flushFlow, setFlushFlow] = useState(200); const [duration, setDuration] = useState(3600); const [selectionMode, setSelectionMode] = useState<'none' | 'valve' | 'drainage'>('none'); const [analyzing, setAnalyzing] = useState(false); const [highlightLayer, setHighlightLayer] = useState | null>(null); // Map click handler const handleMapClickSelectFeatures = useCallback( async (event: { coordinate: number[] }) => { if (!map || selectionMode === 'none') return; const feature = await mapClickSelectFeatures(event, map); if (!feature) return; const layer = feature.getId()?.toString().split(".")[0]; const featureId = feature.getProperties().id; if (selectionMode === 'valve') { if (layer !== 'geo_valves') { open?.({ type: "error", message: "请选择阀门要素", }); return; } setValves((prev) => { if (prev.some((v) => v.id === featureId)) { open?.({ type: "error", message: "该阀门已添加", }); return prev; } return [...prev, { id: featureId, k: 1.0, feature }]; }); } else if (selectionMode === 'drainage') { if (layer !== 'geo_junctions') { open?.({ type: "error", message: "请选择节点要素作为排水点", }); return; } setDrainageNode(featureId); setDrainageFeature(feature); setSelectionMode('none'); map.un("click", handleMapClickSelectFeatures); } }, [map, selectionMode, open] ); // Initialize highlight layer useEffect(() => { if (!map) return; const highlightStyle = function (feature: FeatureLike) { const styles = []; const type = feature.get("type"); // We will set this property when adding to source if (type === "valve") { styles.push( new Style({ image: new CircleStyle({ radius: 8, fill: new Fill({ color: "rgba(255, 165, 0, 0.8)" }), // Orange for valves stroke: new Stroke({ color: "white", width: 2 }), }), }) ); } else if (type === "drainage") { styles.push( new Style({ image: new CircleStyle({ radius: 8, fill: new Fill({ color: "rgba(0, 0, 255, 0.8)" }), // Blue for drainage stroke: new Stroke({ color: "white", width: 2 }), }), }) ); } return styles; }; const layer = new VectorLayer({ source: new VectorSource(), style: highlightStyle, zIndex: 1000, properties: { name: "FlushingHighlight", }, }); map.addLayer(layer); setHighlightLayer(layer); return () => { map.removeLayer(layer); map.un("click", handleMapClickSelectFeatures); }; }, [map, handleMapClickSelectFeatures]); // Update highlight layer features useEffect(() => { if (!highlightLayer) return; const source = highlightLayer.getSource(); if (!source) return; source.clear(); // Add valves valves.forEach((v) => { if (v.feature) { const f = v.feature.clone(); // Clone to avoid modifying original f.set("type", "valve"); // Ensure geometry is present (it should be for features from map) if (f.getGeometry()) { source.addFeature(f); } } }); // Add drainage node if (drainageFeature) { const f = drainageFeature.clone(); f.set("type", "drainage"); source.addFeature(f); } }, [highlightLayer, valves, drainageFeature]); // Bind click event based on selection mode useEffect(() => { if (!map || selectionMode === "none") return; map.on("click", handleMapClickSelectFeatures); return () => { map.un("click", handleMapClickSelectFeatures); }; }, [map, selectionMode, handleMapClickSelectFeatures]); // Toggle selection const toggleSelection = (mode: 'valve' | 'drainage') => { // If clicking same mode, turn off if (selectionMode === mode) { setSelectionMode('none'); } else { setSelectionMode(mode); } }; const handleRemoveValve = (id: string) => { setValves((prev) => prev.filter((v) => v.id !== id)); }; const handleValveKChange = (id: string, k: string) => { const numK = parseFloat(k); setValves(prev => prev.map(v => v.id === id ? { ...v, k: isNaN(numK) ? 0 : numK } : v)); }; const handleAnalyze = async () => { if (!startTime || !drainageNode || !schemeName.trim()) { open?.({ type: "error", message: "请填写完整参数", description: "方案名称、开始时间和排水点为必填项", }); return; } setAnalyzing(true); try { const formattedTime = startTime.format("YYYY-MM-DDTHH:mm:00Z"); // ISO format with seconds set to 00 const params = { scheme_name: schemeName, network: NETWORK_NAME, start_time: formattedTime, valves: valves.map(v => v.id), valves_k: valves.map(v => v.k), drainage_node_ID: drainageNode, flush_flow: flushFlow, duration: duration }; // Use params serializer to handle array params correctly if needed, // but axios usually handles array as valves[]=1&valves[]=2 // FastAPI default expects repeated query params. const response = await api.get(`${config.BACKEND_URL}/api/v1/flushing_analysis/`, { params, // Ensure arrays are sent as repeated keys: valves=1&valves=2 paramsSerializer: { indexes: null // Result: valves=1&valves=2 } }); if (response.status !== 200) { throw new Error(`分析请求失败,状态码: ${response.status}`); } open?.({ type: "success", message: "方案分析成功", description: "管道冲洗模拟完成,请在方案查询中查看结果。", }); } catch (error) { console.error("提交分析失败", error); open?.({ type: "error", message: "提交分析失败", description: error instanceof Error ? error.message : "未知错误", }); } finally { setAnalyzing(false); } }; return ( {/* 1. Valve Selection */} 参与阀门 {selectionMode === 'valve' && ( 💡 点击地图上的阀门进行添加 )} {valves.map((valve) => ( {valve.id} handleValveKChange(valve.id, e.target.value)} className="w-20" slotProps={{ htmlInput: { step: 0.1, min: 0, max: 1 } }} /> handleRemoveValve(valve.id)}> ))} {valves.length === 0 && ( 暂无选中阀门 )} {/* 2. Drainage Node Selection */} 排水节点 {selectionMode === 'drainage' && ( 💡 点击地图上的节点作为排水点 )} {drainageNode && ( {drainageNode} { setDrainageNode(null); setDrainageFeature(null); }} > )} {!drainageNode && ( 暂无选中排水节点 )} {/* 3. Parameters */} 开始时间 setStartTime(newValue)} format="YYYY-MM-DD HH:mm" slotProps={{ textField: { size: "small", fullWidth: true } }} localeText={ pickerZhCN.components.MuiLocalizationProvider.defaultProps .localeText } /> {/* Scheme Name */} 方案名称 setSchemeName(e.target.value)} placeholder="请输入方案名称" /> 冲洗流量 (CMH) setFlushFlow(parseFloat(e.target.value) || 0)} /> 持续时长 (秒) setDuration(parseInt(e.target.value) || 0)} /> ); }; export default AnalysisParameters;