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 (
+
+ {value === index && (
+ {children}
+ )}
+
+ );
+};
+
+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 },
+ },
+ }}
+ />
+
+
+ }
+ className="bg-blue-600 hover:bg-blue-700"
+ >
+ 查询
+
+
+
+
+ {/* 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;
+}