From 4fbe8450151ade860c762e149f298a8c05054b4b Mon Sep 17 00:00:00 2001 From: JIANG Date: Thu, 5 Feb 2026 17:38:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=AE=A1=E9=81=93=E5=86=B2?= =?UTF-8?q?=E6=B4=97=E5=8A=9F=E8=83=BD=E9=A1=B5=E9=9D=A2=EF=BC=9B=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=B0=B4=E8=B4=A8=E6=A8=A1=E6=8B=9F=E9=BB=98=E8=AE=A4?= =?UTF-8?q?pattern=EF=BC=9B=E8=B0=83=E6=95=B4sidebar=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E5=90=8D=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pipe-flushing/page.tsx | 2 + src/app/_refine_context.tsx | 2 +- .../BurstPipeAnalysis/AnalysisParameters.tsx | 2 +- .../BurstPipeAnalysis/ValveIsolation.tsx | 2 +- .../AnalysisParameters.tsx | 6 +- .../FlushingAnalysis/AnalysisParameters.tsx | 451 ++++++++++++++ .../FlushingAnalysisPanel.tsx | 194 ++++++ .../olmap/FlushingAnalysis/SchemeQuery.tsx | 584 ++++++++++++++++++ .../olmap/FlushingAnalysis/types.ts | 27 + 9 files changed, 1264 insertions(+), 6 deletions(-) create mode 100644 src/components/olmap/FlushingAnalysis/AnalysisParameters.tsx create mode 100644 src/components/olmap/FlushingAnalysis/FlushingAnalysisPanel.tsx create mode 100644 src/components/olmap/FlushingAnalysis/SchemeQuery.tsx create mode 100644 src/components/olmap/FlushingAnalysis/types.ts diff --git a/src/app/(main)/hydraulic-simulation/pipe-flushing/page.tsx b/src/app/(main)/hydraulic-simulation/pipe-flushing/page.tsx index cf12b8b..0ff6b53 100644 --- a/src/app/(main)/hydraulic-simulation/pipe-flushing/page.tsx +++ b/src/app/(main)/hydraulic-simulation/pipe-flushing/page.tsx @@ -2,12 +2,14 @@ import MapComponent from "@app/OlMap/MapComponent"; import MapToolbar from "@app/OlMap/Controls/Toolbar"; +import FlushingAnalysisPanel from "@/components/olmap/FlushingAnalysis/FlushingAnalysisPanel"; export default function Home() { return (
+
); diff --git a/src/app/_refine_context.tsx b/src/app/_refine_context.tsx index d5366d1..170030f 100644 --- a/src/app/_refine_context.tsx +++ b/src/app/_refine_context.tsx @@ -158,7 +158,7 @@ const App = (props: React.PropsWithChildren) => { name: "Hydraulic Simulation", meta: { icon: , - label: "水力仿真", + label: "水力模拟", }, }, { diff --git a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx index 37d0ea4..16bff99 100644 --- a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx +++ b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx @@ -381,7 +381,7 @@ const AnalysisParameters: React.FC = () => { key={pipe.id} className="flex items-center gap-2 p-2 bg-gray-50 rounded" > - + {pipe.id} diff --git a/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx b/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx index 51962f0..765a8dd 100644 --- a/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx +++ b/src/components/olmap/BurstPipeAnalysis/ValveIsolation.tsx @@ -908,7 +908,7 @@ const ValveIsolation: React.FC = ({ {selectedPipeId ? ( - + {selectedPipeId} diff --git a/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx index 560ef75..d64035d 100644 --- a/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx +++ b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx @@ -181,7 +181,7 @@ const AnalysisParameters: React.FC = () => { source: sourceNode, concentration, duration, - pattern: pattern || undefined, + pattern: pattern || "CONSTANT", scheme_name: schemeName, }; @@ -276,7 +276,7 @@ const AnalysisParameters: React.FC = () => { {sourceNode ? ( - + {sourceNode} @@ -373,7 +373,7 @@ const AnalysisParameters: React.FC = () => { size="small" value={pattern} onChange={(e) => setPattern(e.target.value)} - placeholder="可选,输入 pattern 名称" + placeholder="可选,输入 pattern 名称,默认为 CONSTANT" /> diff --git a/src/components/olmap/FlushingAnalysis/AnalysisParameters.tsx b/src/components/olmap/FlushingAnalysis/AnalysisParameters.tsx new file mode 100644 index 0000000..438d684 --- /dev/null +++ b/src/components/olmap/FlushingAnalysis/AnalysisParameters.tsx @@ -0,0 +1,451 @@ +"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 "@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 { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; +import Feature, { FeatureLike } from "ol/Feature"; +import { useNotification } from "@refinedev/core"; +import axios from "axios"; +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(0); + const [duration, setDuration] = useState(3600); + + const [selectionMode, setSelectionMode] = useState<'none' | 'valve' | 'drainage'>('none'); + const [analyzing, setAnalyzing] = useState(false); + + const [highlightLayer, setHighlightLayer] = useState | null>(null); + + // 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]); + + // 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]); + + // 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 }]; // Default k=1.0? User can change. + }); + + } else if (selectionMode === 'drainage') { + if (layer !== 'geo_junctions') { + open?.({ + type: "error", + message: "请选择节点要素作为排水点", + }); + return; + } + setDrainageNode(featureId); + setDrainageFeature(feature); + setSelectionMode('none'); // Auto exit selection after picking one + map.un("click", handleMapClickSelectFeatures); + } + }, + [map, selectionMode, open] + ); + + // 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: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 axios.get(`${config.BACKEND_URL}/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="请输入方案名称" + /> + + + + + + 冲洗流量 + + setFlushFlow(parseFloat(e.target.value) || 0)} + /> + + + + 持续时长 (秒) + + setDuration(parseInt(e.target.value) || 0)} + /> + + + + + + + + + ); +}; + +export default AnalysisParameters; diff --git a/src/components/olmap/FlushingAnalysis/FlushingAnalysisPanel.tsx b/src/components/olmap/FlushingAnalysis/FlushingAnalysisPanel.tsx new file mode 100644 index 0000000..83523bc --- /dev/null +++ b/src/components/olmap/FlushingAnalysis/FlushingAnalysisPanel.tsx @@ -0,0 +1,194 @@ +"use client"; + +import React, { useState } from "react"; +import { + Box, + Drawer, + Tabs, + Tab, + Typography, + IconButton, + Tooltip, +} from "@mui/material"; +import { + ChevronRight, + ChevronLeft, + Analytics as AnalyticsIcon, + Search as SearchIcon, +} from "@mui/icons-material"; +import { MdCleaningServices } from "react-icons/md"; +import AnalysisParameters from "./AnalysisParameters"; +import SchemeQuery from "./SchemeQuery"; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +const TabPanel: React.FC = ({ children, value, index }) => { + return ( + + ); +}; + +interface FlushingAnalysisPanelProps { + open?: boolean; + onToggle?: () => void; +} + +const FlushingAnalysisPanel: React.FC = ({ + open: controlledOpen, + onToggle, +}) => { + const [internalOpen, setInternalOpen] = useState(true); + const [currentTab, setCurrentTab] = useState(0); + + // Using controlled or uncontrolled state + const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen; + const handleToggle = () => { + if (onToggle) { + onToggle(); + } else { + setInternalOpen(!internalOpen); + } + }; + + const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { + setCurrentTab(newValue); + }; + + const drawerWidth = 450; // Slightly narrower than burst analysis as we have fewer tabs + const panelTitle = "管道冲洗分析"; + + return ( + <> + {/* Toggle Button when closed */} + {!isOpen && ( + + + + + {panelTitle} + + + + + )} + + {/* Main Panel */} + + + {/* Header */} + + + + + {panelTitle} + + + + + + + + + + + + } + iconPosition="start" + label="分析参数" + /> + } + iconPosition="start" + label="方案查询" + /> + + + + {/* Tab Content */} + + + + + + + + + + + ); +}; + +export default FlushingAnalysisPanel; diff --git a/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx b/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx new file mode 100644 index 0000000..eb75457 --- /dev/null +++ b/src/components/olmap/FlushingAnalysis/SchemeQuery.tsx @@ -0,0 +1,584 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import ReactDOM from "react-dom"; + +import { + Box, + Button, + Typography, + Checkbox, + FormControlLabel, + IconButton, + Card, + CardContent, + Chip, + Tooltip, + Collapse, + Link, +} from "@mui/material"; +import { + Info as InfoIcon, + Search as SearchIcon, + LocationOn as LocationIcon, +} from "@mui/icons-material"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import "dayjs/locale/zh-cn"; +import dayjs, { Dayjs } from "dayjs"; +import axios from "axios"; +import moment from "moment"; +import { config, NETWORK_NAME } from "@config/config"; +import { useNotification } from "@refinedev/core"; +import { useData, useMap } from "@app/OlMap/MapComponent"; +import { queryFeaturesByIds } from "@/utils/mapQueryService"; +import { GeoJSON } from "ol/format"; +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; +import { Style, Icon, Circle, Fill, Stroke } from "ol/style"; +import Feature, { FeatureLike } from "ol/Feature"; +import { bbox, featureCollection } from "@turf/turf"; +import Timeline from "@app/OlMap/Controls/Timeline"; +import { SchemeRecord, SchemaItem } from "./types"; + +interface SchemeQueryProps { + schemes?: SchemeRecord[]; + onSchemesChange?: (schemes: SchemeRecord[]) => void; + network?: string; +} + +const SCHEME_TYPE = "flushing_analysis"; + +const SchemeQuery: React.FC = ({ + schemes: externalSchemes, + onSchemesChange, + network = NETWORK_NAME, +}) => { + const [queryAll, setQueryAll] = useState(true); + const [queryDate, setQueryDate] = useState(dayjs(new Date())); + const [internalSchemes, setInternalSchemes] = useState([]); + const [loading, setLoading] = useState(false); + const [expandedId, setExpandedId] = useState(null); + + const [highlightLayer, setHighlightLayer] = + useState | null>(null); + const [highlightFeatures, setHighlightFeatures] = useState([]); + + // Timeline related state + const [showTimeline, setShowTimeline] = useState(false); + const [selectedDate, setSelectedDate] = useState(undefined); + const [timeRange, setTimeRange] = useState<{ start: Date; end: Date } | undefined>(); + const [mapContainer, setMapContainer] = useState(null); + + const { open } = useNotification(); + const map = useMap(); + const data = useData(); + const { schemeName, setSchemeName } = data || {}; + + const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; + const setSchemes = onSchemesChange || setInternalSchemes; + + useEffect(() => { + if (!map) return; + const target = map.getTargetElement(); + if (target) { + setMapContainer(target); + } + }, [map]); + + // Initialize highlight layer + useEffect(() => { + if (!map) return; + + const themeColor = "rgba(0, 0, 255"; // Blue for drainage + const valveColor = "rgba(255, 165, 0"; // Orange for valves + + const sourceStyle = function (feature: FeatureLike) { + const type = (feature as any).get("type"); + if (type === "valve") { + return [ + new Style({ + image: new Circle({ + radius: 8, + fill: new Fill({ color: `${valveColor}, 0.8)` }), + stroke: new Stroke({ color: "white", width: 2 }), + }), + }) + ]; + } else { + // Default drainage + return [ + new Style({ + image: new Circle({ + radius: 12, + fill: new Fill({ color: `${themeColor}, 0.2)` }), + }), + }), + new Style({ + image: new Circle({ + radius: 8, + stroke: new Stroke({ color: `${themeColor}, 0.5)`, width: 2 }), + fill: new Fill({ color: `${themeColor}, 0.3)` }), + }), + }), + new Style({ + image: new Circle({ + radius: 4, + fill: new Fill({ color: `${themeColor}, 1)` }), + stroke: new Stroke({ color: "white", width: 1 }), + }) + }), + ]; + } + }; + + const layer = new VectorLayer({ + source: new VectorSource(), + style: sourceStyle, + zIndex: 1000, + properties: { + name: "FlushingQueryResultHighlight", + }, + }); + + map.addLayer(layer); + setHighlightLayer(layer); + + return () => { + map.removeLayer(layer); + }; + }, [map]); + + // Update highlight features + 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 handleLocateDrainageNode = (nodeId: string) => { + if (!nodeId) return; + queryFeaturesByIds([nodeId], "geo_junctions_mat").then((features) => { + if (features.length > 0) { + // Add type property to distinguish styling + features.forEach(f => f.set("type", "drainage")); + setHighlightFeatures(features); + zoomToFeatures(features); + } else { + open?.({ + type: "error", + message: "未找到该节点要素", + }); + } + }); + }; + + const handleLocateValves = (valveIds: string[]) => { + if (!valveIds || valveIds.length === 0) return; + queryFeaturesByIds(valveIds, "geo_valves").then((features) => { + if (features.length > 0) { + features.forEach(f => f.set("type", "valve")); + setHighlightFeatures(features); + zoomToFeatures(features); + } else { + open?.({ + type: "error", + message: "未找到阀门要素", + }); + } + }); + }; + + const zoomToFeatures = (features: 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, + padding: [50, 50, 50, 50], + }); + } + }; + + const formatTime = (timeStr: string) => { + return moment(timeStr).format("MM-DD HH:mm"); + }; + + const handleQuery = async () => { + if (!queryAll && !queryDate) return; + + setLoading(true); + try { + const response = await axios.get( + `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`, + ); + + let filteredResults = response.data; + + // Filter by type + filteredResults = filteredResults.filter((item: SchemaItem) => item.scheme_type === SCHEME_TYPE); + + if (!queryAll && queryDate) { + const formattedDate = queryDate.format("YYYY-MM-DD"); + filteredResults = filteredResults.filter((item: SchemaItem) => { + const itemDate = moment(item.create_time).format("YYYY-MM-DD"); + return itemDate === formattedDate; + }); + } + + setSchemes( + filteredResults.map((item: SchemaItem) => ({ + id: item.scheme_id, + schemeName: item.scheme_name, + type: item.scheme_type, + user: item.username, + create_time: item.create_time, + startTime: item.scheme_start_time, + schemeDetail: item.scheme_detail, + })), + ); + + if (filteredResults.length === 0) { + open?.({ + type: "error", + message: "未找到相关方案", + description: "请尝试更改查询条件", + }); + } + } catch (error) { + console.error("查询请求失败:", error); + open?.({ + type: "error", + message: "查询失败", + description: "获取方案列表失败,请稍后重试", + }); + } finally { + setLoading(false); + } + }; + + const handleViewResults = (scheme: SchemeRecord) => { + setShowTimeline(true); + + const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined; + + if (scheme.startTime && scheme.schemeDetail?.duration) { + const start = new Date(scheme.startTime); + const end = new Date(start.getTime() + scheme.schemeDetail.duration * 1000); + setSelectedDate(schemeDate); + setTimeRange({ start, end }); + } + + setSchemeName?.(scheme.schemeName); + + // Locate drainage node by default if available + if (scheme.schemeDetail?.drainage_node_ID) { + handleLocateDrainageNode(scheme.schemeDetail.drainage_node_ID); + } + }; + + return ( + <> + {showTimeline && + mapContainer && + ReactDOM.createPortal( + , + mapContainer, + )} + + {/* Query Controls */} + + + + setQueryAll(e.target.checked)} + size="small" + /> + } + label={查询全部} + className="m-0" + /> + + + value && dayjs.isDayjs(value) && setQueryDate(value) + } + format="YYYY-MM-DD" + disabled={queryAll} + slotProps={{ + textField: { + size: "small", + sx: { width: 160 }, + }, + }} + /> + + + + + + + {/* Results List */} + + {schemes.length === 0 ? ( + + 暂无方案数据 + + ) : ( + + + 共 {schemes.length} 条记录 + + {schemes.map((scheme) => ( + + + + + + + {scheme.schemeName} + + + + + 用户: {scheme.user} · 时间: {formatTime(scheme.create_time)} + + + + + + scheme.schemeDetail?.drainage_node_ID && + handleLocateDrainageNode(scheme.schemeDetail.drainage_node_ID) + } + color="primary" + className="p-1" + > + + + + + + setExpandedId( + expandedId === scheme.id ? null : scheme.id, + ) + } + color="primary" + className="p-1" + > + + + + + + + + + + + + {/* 排水节点 */} + + + 排水节点: + + + {scheme.schemeDetail?.drainage_node_ID ? ( + { + e.preventDefault(); + handleLocateDrainageNode(scheme.schemeDetail!.drainage_node_ID); + }} + > + {scheme.schemeDetail.drainage_node_ID} + + ) : ( + + N/A + + )} + + + + {/* 冲洗流量 */} + + + 冲洗流量: + + + {scheme.schemeDetail?.flushing_flow ?? "-"} m³/h + + + + {/* 持续时长 */} + + + 持续时长: + + + {scheme.schemeDetail?.duration ?? "-"} 秒 + + + + + + + + {/* 用户 */} + + + 用户: + + + {scheme.user} + + + + {/* 创建时间 */} + + + 创建时间: + + + {formatTime(scheme.create_time)} + + + + {/* 开始时间 */} + + + 模拟开始: + + + {formatTime(scheme.startTime)} + + + + + + {/* 阀门列表 */} + + + 参与阀门及开度: + + + {scheme.schemeDetail?.valve_opening && Object.entries(scheme.schemeDetail.valve_opening).length > 0 ? ( + Object.entries(scheme.schemeDetail.valve_opening).map(([id, k]) => ( + + handleLocateValves([id])} + className="text-xs h-6 bg-gray-50 cursor-pointer hover:bg-orange-50 hover:border-orange-200" + /> + + )) + ) : ( + + )} + + + + + + + + + + + + + ))} + + )} + + + + ); +}; + +export default SchemeQuery; diff --git a/src/components/olmap/FlushingAnalysis/types.ts b/src/components/olmap/FlushingAnalysis/types.ts new file mode 100644 index 0000000..776bcad --- /dev/null +++ b/src/components/olmap/FlushingAnalysis/types.ts @@ -0,0 +1,27 @@ +export interface SchemeDetail { + valve_opening: Record; + drainage_node_ID: string; + flushing_flow: number; + duration: number; +} + +export interface SchemeRecord { + id: number; + schemeName: string; + type: string; + user: string; + create_time: string; + startTime: string; + // 详情信息 + schemeDetail?: SchemeDetail; +} + +export interface SchemaItem { + scheme_id: number; + scheme_name: string; + scheme_type: string; + username: string; + create_time: string; + scheme_start_time: string; + scheme_detail?: SchemeDetail; +}