From d584268acde2a4883f11f7554e3fcc2cc643d91a Mon Sep 17 00:00:00 2001 From: JIANG Date: Fri, 30 Jan 2026 15:22:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B0=B4=E8=B4=A8=E5=88=86?= =?UTF-8?q?=E6=9E=90=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/contaminant_source.svg | 1 + src/app/OlMap/Controls/LayerControl.tsx | 30 ++- src/app/OlMap/MapComponent.tsx | 40 ++-- .../BurstPipeAnalysis/AnalysisParameters.tsx | 36 ++-- .../BurstPipeAnalysisPanel.tsx | 46 +--- .../BurstPipeAnalysis/LocationResults.tsx | 12 +- .../olmap/BurstPipeAnalysis/SchemeQuery.tsx | 98 ++++----- .../olmap/BurstPipeAnalysis/types.ts | 38 ++++ .../AnalysisParameters.tsx | 65 ++++-- .../ContaminantSimulation/SchemeQuery.tsx | 198 ++++++++++++++---- .../olmap/ContaminantSimulation/types.ts | 1 - .../SchemeQuery.tsx | 19 +- 12 files changed, 346 insertions(+), 238 deletions(-) create mode 100644 public/icons/contaminant_source.svg create mode 100644 src/components/olmap/BurstPipeAnalysis/types.ts diff --git a/public/icons/contaminant_source.svg b/public/icons/contaminant_source.svg new file mode 100644 index 0000000..5b3130e --- /dev/null +++ b/public/icons/contaminant_source.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/OlMap/Controls/LayerControl.tsx b/src/app/OlMap/Controls/LayerControl.tsx index fe16d37..8b539a5 100644 --- a/src/app/OlMap/Controls/LayerControl.tsx +++ b/src/app/OlMap/Controls/LayerControl.tsx @@ -28,10 +28,21 @@ const LayerControl: React.FC = () => { } = data; const [layerItems, setLayerItems] = useState([]); + const layerOrder = [ + "junctions", + "reservoirs", + "tanks", + "pipes", + "pumps", + "valves", + "scada", + "waterflowLayer", + "junctionContourLayer", + ]; + // 更新图层列表 const updateLayers = useCallback(() => { if (!map || !data) return; - const { deckLayer } = data; const items: LayerItem[] = []; @@ -93,19 +104,6 @@ const LayerControl: React.FC = () => { }); } - // 3. 定义图层显示顺序和过滤白名单 - const layerOrder = [ - "junctions", - "reservoirs", - "tanks", - "pipes", - "pumps", - "valves", - "scada", - "waterflowLayer", - "junctionContourLayer", - ]; - // 过滤并排序 const sortedItems = items .filter((item) => layerOrder.includes(item.id)) @@ -116,7 +114,7 @@ const LayerControl: React.FC = () => { }); setLayerItems(sortedItems); - }, [map, isWaterflowLayerAvailable, isContourLayerAvailable]); + }, [map, deckLayer, isWaterflowLayerAvailable, isContourLayerAvailable]); useEffect(() => { updateLayers(); @@ -146,7 +144,7 @@ const LayerControl: React.FC = () => { } setLayerItems((prev) => - prev.map((i) => (i.id === item.id ? { ...i, visible: checked } : i)) + prev.map((i) => (i.id === item.id ? { ...i, visible: checked } : i)), ); }; diff --git a/src/app/OlMap/MapComponent.tsx b/src/app/OlMap/MapComponent.tsx index 86ffd5c..c2e1068 100644 --- a/src/app/OlMap/MapComponent.tsx +++ b/src/app/OlMap/MapComponent.tsx @@ -111,7 +111,7 @@ const MapComponent: React.FC = ({ children }) => { const [schemeName, setSchemeName] = useState(""); // 当前方案名称 // 记录 id、对应属性的计算值 const [currentJunctionCalData, setCurrentJunctionCalData] = useState( - [] + [], ); const [currentPipeCalData, setCurrentPipeCalData] = useState([]); // junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染 @@ -180,7 +180,7 @@ const MapComponent: React.FC = ({ children }) => { setPipeData(tilePipeDataBuffer.current); tilePipeDataBuffer.current = []; } - }, 100) + }, 100), ); const setJunctionData = (newData: any[]) => { @@ -317,7 +317,7 @@ const MapComponent: React.FC = ({ children }) => { scale: 0.12, anchor: [0.5, 0.5], }), - }) + }), ); } return styles; @@ -500,7 +500,7 @@ const MapComponent: React.FC = ({ children }) => { const uniqueData = Array.from(data.values()); if (uniqueData.length > 0) { uniqueData.forEach((item) => - tileJunctionDataBuffer.current.push(item) + tileJunctionDataBuffer.current.push(item), ); debouncedUpdateData.current(); } @@ -709,7 +709,7 @@ const MapComponent: React.FC = ({ children }) => { if (center && typeof zoom === "number") { localStorage.setItem( MAP_VIEW_STORAGE_KEY, - JSON.stringify({ center, zoom }) + JSON.stringify({ center, zoom }), ); } } catch (err) { @@ -947,27 +947,12 @@ const MapComponent: React.FC = ({ children }) => { // 动画循环 const animate = () => { - if (!flowAnimation.current) { - try { - deckLayer.removeDeckLayer("waterflowLayer"); - } catch (error) { - console.error("Error in animation loop:", error); - } - return; - } // 动画总时长(秒) - if (mergedPipeData.length === 0) { - animationFrameId = requestAnimationFrame(animate); - return; - } const animationDuration = 10; - // 缓冲时间(秒) const bufferTime = 2; - // 完整循环周期 const loopLength = animationDuration + bufferTime; - // 确保时间范围与你的时间戳数据匹配 - const currentTime = (Date.now() / 1000) % loopLength; // (0,12) 之间循环 - // console.log("Current Time:", currentTime); + const currentTime = (Date.now() / 1000) % loopLength; + const waterflowLayer = new TripsLayer({ id: "waterflowLayer", name: "水流", @@ -981,7 +966,7 @@ const MapComponent: React.FC = ({ children }) => { visible: isWaterflowLayerAvailable && showWaterflowLayer && - flowAnimation.current && + flowAnimation.current && // 保持动画标志作为可见性的一部分 currentZoom >= 12 && currentZoom <= 24, widthMinPixels: 5, @@ -990,13 +975,17 @@ const MapComponent: React.FC = ({ children }) => { trailLength: 2, // 水流尾迹淡出时间 currentTime: currentTime, }); + if (deckLayer.getDeckLayerById("waterflowLayer")) { deckLayer.updateDeckLayer("waterflowLayer", waterflowLayer); } else { deckLayer.addDeckLayer(waterflowLayer); } - // 继续请求动画帧,每帧执行一次函数 - animationFrameId = requestAnimationFrame(animate); + + // 只有在需要动画时才请求下一帧,但图层已经添加到了 deckLayer 中 + if (flowAnimation.current) { + animationFrameId = requestAnimationFrame(animate); + } }; animate(); @@ -1007,6 +996,7 @@ const MapComponent: React.FC = ({ children }) => { } }; }, [ + currentPipeCalData, currentZoom, mergedPipeData, pipeText, diff --git a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx index 25dbe73..37d0ea4 100644 --- a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx +++ b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx @@ -44,7 +44,7 @@ const AnalysisParameters: React.FC = () => { const [startTime, setStartTime] = useState(dayjs(new Date())); const [duration, setDuration] = useState(3600); const [schemeName, setSchemeName] = useState( - "FANGAN" + new Date().getTime() + "FANGAN" + new Date().getTime(), ); const [network, setNetwork] = useState(NETWORK_NAME); const [isSelecting, setIsSelecting] = useState(false); @@ -88,7 +88,7 @@ const AnalysisParameters: React.FC = () => { width: 3, lineDash: [15, 10], }), - }) + }), ); const geometry = feature.getGeometry(); const lineCoords = @@ -115,7 +115,7 @@ const AnalysisParameters: React.FC = () => { scale: 0.2, anchor: [0.5, 1], }), - }) + }), ); } return styles; @@ -163,14 +163,14 @@ const AnalysisParameters: React.FC = () => { // 移除不在highlightFeatures中的 const filtered = prevPipes.filter((pipe) => highlightFeatures.some( - (feature) => feature.getProperties().id === pipe.id - ) + (feature) => feature.getProperties().id === pipe.id, + ), ); // 添加新的 const newPipes = highlightFeatures .filter( (feature) => - !filtered.some((p) => p.id === feature.getProperties().id) + !filtered.some((p) => p.id === feature.getProperties().id), ) .map((feature) => { const properties = feature.getProperties(); @@ -207,7 +207,7 @@ const AnalysisParameters: React.FC = () => { const featureId = feature.getProperties().id; setHighlightFeatures((prev) => { const existingIndex = prev.findIndex( - (f) => f.getProperties().id === featureId + (f) => f.getProperties().id === featureId, ); if (existingIndex !== -1) { // 如果已存在,移除 @@ -218,7 +218,7 @@ const AnalysisParameters: React.FC = () => { } }); }, - [map] + [map], ); // 开始选择管道 @@ -242,14 +242,14 @@ const AnalysisParameters: React.FC = () => { const handleRemovePipe = (id: string) => { // 从高亮features中移除 setHighlightFeatures((prev) => - prev.filter((f) => f.getProperties().id !== id) + 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)) + prev.map((pipe) => (pipe.id === id ? { ...pipe, area: numValue } : pipe)), ); }; @@ -266,30 +266,26 @@ const AnalysisParameters: React.FC = () => { const burst_ID = pipePoints.map((pipe) => pipe.id); const burst_size = pipePoints.map((pipe) => - parseInt(pipe.area.toString(), 10) + parseInt(pipe.area.toString(), 10), ); // 格式化开始时间,去除秒部分 const modify_pattern_start_time = startTime ? startTime.format("YYYY-MM-DDTHH:mm:00Z") : ""; const modify_total_duration = duration; - const body = { - name: network, + 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, + scheme_name: schemeName, }; try { - await axios.post(`${config.BACKEND_URL}/api/v1/burst_analysis/`, body, { - headers: { - "Accept-Encoding": "gzip", - "Content-Type": "application/json", - }, + await axios.get(`${config.BACKEND_URL}/api/v1/burst_analysis/`, { + params, }); - // 更新弹窗为成功状态 open?.({ key: "burst-analysis", diff --git a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx index 1a8a4fe..3460efc 100644 --- a/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx +++ b/src/components/olmap/BurstPipeAnalysis/BurstPipeAnalysisPanel.tsx @@ -19,7 +19,7 @@ import { } from "@mui/icons-material"; import AnalysisParameters from "./AnalysisParameters"; import SchemeQuery from "./SchemeQuery"; -import LocationResults, { LocationResult } from "./LocationResults"; +import LocationResults from "./LocationResults"; import ContaminantAnalysisParameters from "../ContaminantSimulation/AnalysisParameters"; import ContaminantSchemeQuery from "../ContaminantSimulation/SchemeQuery"; import ContaminantResultsPanel from "../ContaminantSimulation/ResultsPanel"; @@ -27,25 +27,7 @@ import axios from "axios"; import { config } from "@config/config"; import { useNotification } from "@refinedev/core"; import { useData } from "@app/OlMap/MapComponent"; -interface SchemeDetail { - burst_ID: string[]; - burst_size: number[]; - modify_total_duration: number; - modify_fixed_pump_pattern: any; - modify_valve_opening: any; - modify_variable_pump_pattern: any; -} - -interface SchemeRecord { - id: number; - schemeName: string; - type: string; - user: string; - create_time: string; - startTime: string; - // 详情信息 - schemeDetail?: SchemeDetail; -} +import { LocationResult, SchemeRecord } from "./types"; interface TabPanelProps { children?: React.ReactNode; @@ -82,7 +64,7 @@ const BurstPipeAnalysisPanel: React.FC = ({ const [currentTab, setCurrentTab] = useState(0); const [panelMode, setPanelMode] = useState("burst"); const previousMapText = useRef<{ junction?: string; pipe?: string } | null>( - null + null, ); const data = useData(); @@ -108,28 +90,10 @@ const BurstPipeAnalysisPanel: React.FC = ({ setCurrentTab(newValue); }; - useEffect(() => { - if (!data) return; - if (panelMode === "contaminant") { - if (!previousMapText.current) { - previousMapText.current = { - junction: data.junctionText, - pipe: data.pipeText, - }; - } - data.setJunctionText?.("quality"); - data.setPipeText?.("quality"); - } else if (panelMode === "burst" && previousMapText.current) { - data.setJunctionText?.(previousMapText.current.junction || "pressure"); - data.setPipeText?.(previousMapText.current.pipe || "flow"); - previousMapText.current = null; - } - }, [panelMode, data]); - const handleLocateScheme = async (scheme: SchemeRecord) => { try { const response = await axios.get( - `${config.BACKEND_URL}/api/v1/burst-locate-result/${scheme.schemeName}` + `${config.BACKEND_URL}/api/v1/burst-locate-result/${scheme.schemeName}`, ); setLocationResults(response.data); setCurrentTab(2); // 切换到定位结果标签页 @@ -304,7 +268,7 @@ const BurstPipeAnalysisPanel: React.FC = ({ onLocate={handleLocateScheme} /> ) : ( - + setCurrentTab(2)} /> )} diff --git a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx index 2292ad9..047b730 100644 --- a/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx +++ b/src/components/olmap/BurstPipeAnalysis/LocationResults.tsx @@ -29,15 +29,7 @@ import { Point } from "ol/geom"; import { toLonLat } from "ol/proj"; import moment from "moment"; import "moment-timezone"; - -export interface LocationResult { - id: number; - type: string; - burst_incident: string; - leakage: number | null; - detect_time: string; - locate_result: string[] | null; -} +import { LocationResult } from "./types"; interface LocationResultsProps { results?: LocationResult[]; @@ -56,7 +48,7 @@ const LocationResults: React.FC = ({ results = [] }) => { const handleLocatePipes = (pipeIds: string[]) => { if (pipeIds.length > 0) { - queryFeaturesByIds(pipeIds).then((features) => { + queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => { if (features.length > 0) { // 设置高亮要素 setHighlightFeatures(features); diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx index a380693..f32ffc1 100644 --- a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import ReactDOM from "react-dom"; // 添加这行 import { @@ -49,36 +49,7 @@ import { import { Point } from "ol/geom"; import { toLonLat } from "ol/proj"; import Timeline from "@app/OlMap/Controls/Timeline"; - -interface SchemeDetail { - burst_ID: string[]; - burst_size: number[]; - modify_total_duration: number; - modify_fixed_pump_pattern: any; - modify_valve_opening: any; - modify_variable_pump_pattern: any; -} - -interface SchemeRecord { - id: number; - schemeName: string; - type: string; - user: string; - create_time: string; - startTime: string; - // 详情信息 - schemeDetail?: SchemeDetail; -} - -interface SchemaItem { - scheme_id: number; - scheme_name: string; - scheme_type: string; - username: string; - create_time: string; - scheme_start_time: string; - scheme_detail?: SchemeDetail; -} +import { SchemaItem, SchemeRecord } from "./types"; interface SchemeQueryProps { schemes?: SchemeRecord[]; @@ -87,6 +58,8 @@ interface SchemeQueryProps { network?: string; } +const SCHEME_TYPE = "burst_analysis"; + const SchemeQuery: React.FC = ({ schemes: externalSchemes, onSchemesChange, @@ -114,8 +87,8 @@ const SchemeQuery: React.FC = ({ const map = useMap(); const data = useData(); - if (!data) return null; - const { schemeName, setSchemeName } = data; + const { schemeName, setSchemeName } = data || {}; + // 使用外部提供的 schemes 或内部状态 const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; @@ -127,13 +100,17 @@ const SchemeQuery: React.FC = ({ return time.format("MM-DD"); }; + const filteredSchemes = useMemo(() => { + return schemes.filter((scheme) => scheme.type === SCHEME_TYPE); + }, [schemes]); + const handleQuery = async () => { if (!queryAll && !queryDate) return; setLoading(true); try { const response = await axios.get( - `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}` + `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`, ); let filteredResults = response.data; @@ -154,7 +131,7 @@ const SchemeQuery: React.FC = ({ create_time: item.create_time, startTime: item.scheme_start_time, schemeDetail: item.scheme_detail, - })) + })), ); if (filteredResults.length === 0) { @@ -180,14 +157,14 @@ const SchemeQuery: React.FC = ({ const handleLocatePipes = (pipeIds: string[]) => { if (pipeIds.length > 0) { - queryFeaturesByIds(pipeIds).then((features) => { + queryFeaturesByIds(pipeIds, "geo_pipes_mat").then((features) => { if (features.length > 0) { // 设置高亮要素 setHighlightFeatures(features); // 将 OpenLayers Feature 转换为 GeoJSON Feature const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => - geojsonFormat.writeFeatureObject(feature) + geojsonFormat.writeFeatureObject(feature), ); const extent = bbox(featureCollection(geojsonFeatures as any)); @@ -202,25 +179,24 @@ const SchemeQuery: React.FC = ({ // 内部的方案查询函数 const handleViewDetails = (id: number) => { + const scheme = filteredSchemes.find((s) => s.id === id); + if (!scheme) return; + setShowTimeline(true); // 计算时间范围 - const scheme = schemes.find((s) => s.id === id); - const burstPipeIds = scheme?.schemeDetail?.burst_ID || []; - const schemeDate = scheme?.startTime + const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined; - if (scheme?.startTime && scheme.schemeDetail?.modify_total_duration) { + if (scheme.startTime && scheme.schemeDetail?.modify_total_duration) { const start = new Date(scheme.startTime); const end = new Date( - start.getTime() + scheme.schemeDetail.modify_total_duration * 1000 + start.getTime() + scheme.schemeDetail.modify_total_duration * 1000, ); setSelectedDate(schemeDate); setTimeRange({ start, end }); - if (setSchemeName) { - setSchemeName(scheme.schemeName); - } - handleLocatePipes(burstPipeIds); } + setSchemeName?.(scheme.schemeName); + handleLocatePipes(scheme.schemeDetail?.burst_ID || []); }; // 初始化管道图层和高亮图层 @@ -254,7 +230,7 @@ const SchemeQuery: React.FC = ({ width: 3, lineDash: [15, 10], }), - }) + }), ); const geometry = feature.getGeometry(); const lineCoords = @@ -281,7 +257,7 @@ const SchemeQuery: React.FC = ({ scale: 0.2, anchor: [0.5, 1], }), - }) + }), ); } return styles; @@ -336,9 +312,9 @@ const SchemeQuery: React.FC = ({ timeRange={timeRange} disableDateSelection={!!timeRange} schemeName={schemeName} - schemeType="burst_Analysis" + schemeType={SCHEME_TYPE} />, - mapContainer // 渲染到地图容器中,而不是 body + mapContainer, // 渲染到地图容器中,而不是 body )} {/* 查询条件 - 单行布局 */} @@ -391,7 +367,7 @@ const SchemeQuery: React.FC = ({ {/* 结果列表 */} - {schemes.length === 0 ? ( + {filteredSchemes.length === 0 ? ( = ({ ) : ( - 共 {schemes.length} 条记录 + 共 {filteredSchemes.length} 条记录 - {schemes.map((scheme) => ( + {filteredSchemes.map((scheme) => ( = ({ {scheme.schemeName} = ({ size="small" onClick={() => setExpandedId( - expandedId === scheme.id ? null : scheme.id + expandedId === scheme.id ? null : scheme.id, ) } color="primary" @@ -528,7 +508,7 @@ const SchemeQuery: React.FC = ({ > {pipeId} - ) + ), ) ) : ( = ({ className="font-medium text-gray-900" > {moment(scheme.create_time).format( - "YYYY-MM-DD HH:mm" + "YYYY-MM-DD HH:mm", )} @@ -634,7 +614,7 @@ const SchemeQuery: React.FC = ({ className="font-medium text-gray-900" > {moment(scheme.startTime).format( - "YYYY-MM-DD HH:mm" + "YYYY-MM-DD HH:mm", )} @@ -652,7 +632,7 @@ const SchemeQuery: React.FC = ({ className="border-blue-600 text-blue-600 hover:bg-blue-50" onClick={() => handleLocatePipes?.( - scheme.schemeDetail!.burst_ID + scheme.schemeDetail!.burst_ID, ) } sx={{ diff --git a/src/components/olmap/BurstPipeAnalysis/types.ts b/src/components/olmap/BurstPipeAnalysis/types.ts new file mode 100644 index 0000000..9da252f --- /dev/null +++ b/src/components/olmap/BurstPipeAnalysis/types.ts @@ -0,0 +1,38 @@ +export interface SchemeDetail { + burst_ID: string[]; + burst_size: number[]; + modify_total_duration: number; + modify_fixed_pump_pattern: any; + modify_valve_opening: any; + modify_variable_pump_pattern: any; +} + +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; +} + +export interface LocationResult { + id: number; + type: string; + burst_incident: string; + leakage: number | null; + detect_time: string; + locate_result: string[] | null; +} diff --git a/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx index 353ab29..560ef75 100644 --- a/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx +++ b/src/components/olmap/ContaminantSimulation/AnalysisParameters.tsx @@ -22,7 +22,7 @@ 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 { Style, Stroke, Fill, Circle as CircleStyle, Icon } from "ol/style"; import Feature from "ol/Feature"; import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService"; @@ -31,10 +31,13 @@ const AnalysisParameters: React.FC = () => { const { open } = useNotification(); const network = NETWORK_NAME; + const [schemeName, setSchemeName] = useState( + "WQ_" + new Date().getTime(), + ); const [startTime, setStartTime] = useState(dayjs(new Date())); const [sourceNode, setSourceNode] = useState(""); - const [concentration, setConcentration] = useState(1); - const [duration, setDuration] = useState(900); + const [concentration, setConcentration] = useState(100); + const [duration, setDuration] = useState(3600); const [pattern, setPattern] = useState(""); const [isSelecting, setIsSelecting] = useState(false); const [submitting, setSubmitting] = useState(false); @@ -42,7 +45,7 @@ const AnalysisParameters: React.FC = () => { const [highlightLayer, setHighlightLayer] = useState | null>(null); const [highlightFeature, setHighlightFeature] = useState( - null + null, ); const isFormValid = useMemo(() => { @@ -51,20 +54,41 @@ const AnalysisParameters: React.FC = () => { Boolean(startTime) && Boolean(sourceNode) && concentration > 0 && - duration > 0 + duration > 0 && + schemeName.trim() !== "" ); - }, [network, startTime, sourceNode, concentration, duration]); + }, [network, startTime, sourceNode, concentration, duration, schemeName]); 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 themeColor = "rgba(3, 168, 107"; // #03a86b + + const sourceStyle = [ + // 外层扩散光圈 + new Style({ + image: new CircleStyle({ + radius: 12, + fill: new Fill({ color: `${themeColor}, 0.2)` }), + }), }), - }); + // 中层扩散背景 + new Style({ + image: new CircleStyle({ + radius: 8, + stroke: new Stroke({ color: `${themeColor}, 0.5)`, width: 2 }), + fill: new Fill({ color: `${themeColor}, 0.3)` }), + }), + }), + // 上层图标 + new Style({ + image: new Icon({ + src: "/icons/contaminant_source.svg", + scale: 0.2, + anchor: [0.5, 1], + }), + }), + ]; const layer = new VectorLayer({ source: new VectorSource(), @@ -117,7 +141,7 @@ const AnalysisParameters: React.FC = () => { setIsSelecting(false); map.un("click", handleMapClickSelectFeatures); }, - [map, open] + [map, open], ); const handleStartSelection = () => { @@ -146,15 +170,19 @@ const AnalysisParameters: React.FC = () => { message: "方案提交分析中", undoableTimeout: 3, }); - + // 格式化开始时间,去除秒部分 + const start_time = startTime + ? startTime.format("YYYY-MM-DDTHH:mm:00Z") + : ""; try { const params = { network, - start_time: startTime.toISOString(), + start_time: start_time, source: sourceNode, concentration, duration, pattern: pattern || undefined, + scheme_name: schemeName, }; await axios.get(`${config.BACKEND_URL}/api/v1/contaminant_simulation/`, { @@ -297,13 +325,14 @@ const AnalysisParameters: React.FC = () => { - 管网名称 + 方案名称 setSchemeName(e.target.value)} + placeholder="输入方案名称" /> diff --git a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx index 51c2b2c..0dc0c87 100644 --- a/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx +++ b/src/components/olmap/ContaminantSimulation/SchemeQuery.tsx @@ -31,24 +31,36 @@ import { useNotification } from "@refinedev/core"; import { config, NETWORK_NAME } from "@config/config"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { useData, useMap } from "@app/OlMap/MapComponent"; +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 from "ol/Feature"; +import { bbox, featureCollection } from "@turf/turf"; import Timeline from "@app/OlMap/Controls/Timeline"; import { ContaminantSchemaItem, ContaminantSchemeRecord } from "./types"; interface SchemeQueryProps { schemes?: ContaminantSchemeRecord[]; onSchemesChange?: (schemes: ContaminantSchemeRecord[]) => void; + onViewResults?: () => void; network?: string; } -const SCHEME_TYPE = "contaminant_simulation"; +const SCHEME_TYPE = "contaminant_analysis"; const SchemeQuery: React.FC = ({ schemes: externalSchemes, onSchemesChange, + onViewResults, network = NETWORK_NAME, }) => { const [queryAll, setQueryAll] = useState(true); const [queryDate, setQueryDate] = useState(dayjs(new Date())); + const [highlightLayer, setHighlightLayer] = + useState | null>(null); + const [highlightFeatures, setHighlightFeatures] = useState([]); + const [showTimeline, setShowTimeline] = useState(false); const [selectedDate, setSelectedDate] = useState(undefined); const [timeRange, setTimeRange] = useState< @@ -64,8 +76,7 @@ const SchemeQuery: React.FC = ({ const { open } = useNotification(); const map = useMap(); const data = useData(); - const { schemeName, setSchemeName, setJunctionText, setPipeText } = - data || {}; + const { schemeName, setSchemeName } = data || {}; const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; @@ -79,6 +90,83 @@ const SchemeQuery: React.FC = ({ } }, [map]); + // 初始化高亮图层 + useEffect(() => { + if (!map) return; + + const themeColor = "rgba(3, 168, 107"; // #03a86b + + const sourceStyle = [ + // 外层扩散光圈 + 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 Icon({ + src: "/icons/contaminant_source.svg", + scale: 0.2, + anchor: [0.5, 1], + }), + }), + ]; + + const highlightLayer = new VectorLayer({ + source: new VectorSource(), + style: sourceStyle, + maxZoom: 24, + minZoom: 12, + properties: { + name: "污染源高亮", + value: "contaminant_source_highlight", + }, + }); + + map.addLayer(highlightLayer); + setHighlightLayer(highlightLayer); + + return () => { + map.removeLayer(highlightLayer); + }; + }, [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, highlightLayer]); + const formatTime = (timeStr: string) => { const time = moment(timeStr); return time.format("MM-DD"); @@ -93,16 +181,18 @@ const SchemeQuery: React.FC = ({ setLoading(true); try { const response = await axios.get( - `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}` + `${config.BACKEND_URL}/api/v1/getallschemes/?network=${network}`, ); let filteredResults = response.data; if (!queryAll) { const formattedDate = queryDate!.format("YYYY-MM-DD"); - filteredResults = response.data.filter((item: ContaminantSchemaItem) => { - const itemDate = moment(item.create_time).format("YYYY-MM-DD"); - return itemDate === formattedDate; - }); + filteredResults = response.data.filter( + (item: ContaminantSchemaItem) => { + const itemDate = moment(item.create_time).format("YYYY-MM-DD"); + return itemDate === formattedDate; + }, + ); } setSchemes( @@ -114,7 +204,7 @@ const SchemeQuery: React.FC = ({ create_time: item.create_time, startTime: item.scheme_start_time, schemeDetail: item.scheme_detail, - })) + })), ); if (filteredResults.length === 0) { @@ -138,19 +228,29 @@ const SchemeQuery: React.FC = ({ } }; - const handleLocateSource = (sourceId?: string) => { - if (!sourceId) return; - queryFeaturesByIds([sourceId], "geo_junctions_mat").then((features) => { - if (features.length > 0) { - const extent = features[0].getGeometry()?.getExtent(); - if (extent) { - map?.getView().fit(extent, { - maxZoom: 18, - duration: 1000, - }); + const handleLocateSource = (sourceIds: string[]) => { + if (sourceIds.length > 0) { + queryFeaturesByIds(sourceIds, "geo_junctions_mat").then((features) => { + if (features.length > 0) { + // 设置高亮要素 + setHighlightFeatures(features); + // 将 OpenLayers Feature 转换为 GeoJSON 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, + }); + } } - } - }); + }); + } }; const handleViewDetails = (id: number) => { @@ -158,17 +258,21 @@ const SchemeQuery: React.FC = ({ if (!scheme) return; setShowTimeline(true); - const schemeDate = scheme.startTime ? new Date(scheme.startTime) : undefined; + 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); + const end = new Date( + start.getTime() + scheme.schemeDetail.duration * 1000, + ); setSelectedDate(schemeDate); setTimeRange({ start, end }); } setSchemeName?.(scheme.schemeName); - setJunctionText?.("quality"); - setPipeText?.("quality"); - handleLocateSource(scheme.schemeDetail?.source); + if (scheme.schemeDetail?.source) { + handleLocateSource([scheme.schemeDetail.source]); + } }; return ( @@ -183,7 +287,7 @@ const SchemeQuery: React.FC = ({ schemeName={schemeName} schemeType={SCHEME_TYPE} />, - mapContainer + mapContainer, )} @@ -291,7 +395,11 @@ const SchemeQuery: React.FC = ({ {scheme.schemeName} = ({ variant="caption" className="text-gray-500 block" > - ID: {scheme.id} · 日期: {formatTime(scheme.create_time)} + ID: {scheme.id} · 日期:{" "} + {formatTime(scheme.create_time)} setExpandedId( - expandedId === scheme.id ? null : scheme.id + expandedId === scheme.id ? null : scheme.id, ) } color="primary" @@ -322,16 +433,19 @@ const SchemeQuery: React.FC = ({ - + {/* handleLocateSource(scheme.schemeDetail?.source)} + onClick={() => + scheme.schemeDetail?.source && + handleLocateSource([scheme.schemeDetail.source]) + } color="primary" className="p-1" > - + */} @@ -355,7 +469,9 @@ const SchemeQuery: React.FC = ({ className="font-medium text-blue-600 hover:text-blue-800 underline cursor-pointer" onClick={(e) => { e.preventDefault(); - handleLocateSource(scheme.schemeDetail?.source); + handleLocateSource([ + scheme.schemeDetail!.source!, + ]); }} > {scheme.schemeDetail.source} @@ -381,7 +497,8 @@ const SchemeQuery: React.FC = ({ variant="caption" className="font-medium text-gray-900" > - {scheme.schemeDetail?.concentration ?? "N/A"} mg/L + {scheme.schemeDetail?.concentration ?? "N/A"}{" "} + mg/L @@ -443,7 +560,7 @@ const SchemeQuery: React.FC = ({ className="font-medium text-gray-900" > {moment(scheme.create_time).format( - "YYYY-MM-DD HH:mm" + "YYYY-MM-DD HH:mm", )} @@ -459,7 +576,7 @@ const SchemeQuery: React.FC = ({ className="font-medium text-gray-900" > {moment(scheme.startTime).format( - "YYYY-MM-DD HH:mm" + "YYYY-MM-DD HH:mm", )} @@ -473,7 +590,10 @@ const SchemeQuery: React.FC = ({ fullWidth size="small" className="border-blue-600 text-blue-600 hover:bg-blue-50" - onClick={() => handleLocateSource(scheme.schemeDetail?.source)} + onClick={() => + scheme.schemeDetail?.source && + handleLocateSource([scheme.schemeDetail.source]) + } sx={{ textTransform: "none", fontWeight: 500, diff --git a/src/components/olmap/ContaminantSimulation/types.ts b/src/components/olmap/ContaminantSimulation/types.ts index 407c08d..fd8ef45 100644 --- a/src/components/olmap/ContaminantSimulation/types.ts +++ b/src/components/olmap/ContaminantSimulation/types.ts @@ -3,7 +3,6 @@ export interface ContaminantSchemeDetail { concentration: number; duration: number; pattern?: string | null; - start_time?: string; } export interface ContaminantSchemeRecord { diff --git a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx index 5414a1c..ad36ab6 100644 --- a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx +++ b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx @@ -141,6 +141,7 @@ const SchemeQuery: React.FC = ({ } }); }, [highlightFeatures]); + // 查询方案 const handleQuery = async () => { if (!queryAll && !queryDate) return; @@ -148,7 +149,7 @@ const SchemeQuery: React.FC = ({ setLoading(true); try { const response = await axios.get( - `${config.BACKEND_URL}/api/v1/getallsensorplacements/?network=${network}` + `${config.BACKEND_URL}/api/v1/getallsensorplacements/?network=${network}`, ); let filteredResults = response.data; @@ -171,7 +172,7 @@ const SchemeQuery: React.FC = ({ user: item.username, create_time: item.create_time, sensorLocation: item.sensor_location, - })) + })), ); if (filteredResults.length === 0) { @@ -206,14 +207,14 @@ const SchemeQuery: React.FC = ({ } if (sensorIds.length > 0) { - queryFeaturesByIds(sensorIds).then((features) => { + queryFeaturesByIds(sensorIds, "geo_junctions_mat").then((features) => { if (features.length > 0) { // 设置高亮要素 setHighlightFeatures(features); // 将 OpenLayers Feature 转换为 GeoJSON Feature const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => - geojsonFormat.writeFeatureObject(feature) + geojsonFormat.writeFeatureObject(feature), ); const extent = bbox(featureCollection(geojsonFeatures as any)); @@ -376,7 +377,7 @@ const SchemeQuery: React.FC = ({ size="small" onClick={() => setExpandedId( - expandedId === scheme.id ? null : scheme.id + expandedId === scheme.id ? null : scheme.id, ) } color="primary" @@ -385,7 +386,7 @@ const SchemeQuery: React.FC = ({ - + {/* onLocate?.(scheme.id)} @@ -394,7 +395,7 @@ const SchemeQuery: React.FC = ({ > - + */} @@ -466,7 +467,7 @@ const SchemeQuery: React.FC = ({ className="font-medium text-gray-900" > {moment(scheme.create_time).format( - "YYYY-MM-DD HH:mm" + "YYYY-MM-DD HH:mm", )} @@ -500,7 +501,7 @@ const SchemeQuery: React.FC = ({ > {sensorId} - ) + ), )}