diff --git a/package-lock.json b/package-lock.json index e4996e7..717e3a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-icons": "^5.5.0", + "react-window": "^2.2.2", "tailwindcss": "^4.1.13" }, "devDependencies": { @@ -16460,6 +16461,16 @@ "react": "^16.6.3 || ^17.0.0" } }, + "node_modules/react-window": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.2.tgz", + "integrity": "sha512-kvHKwFImKBWNbx2S87NZOhQhAVkBthjmnOfHlhQI45p3A+D+V53E+CqQMsyHrxNe3ke+YtWXuKDa1eoHAaIWJg==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", diff --git a/package.json b/package.json index b106098..7cc74e7 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-icons": "^5.5.0", + "react-window": "^2.2.2", "tailwindcss": "^4.1.13" }, "devDependencies": { diff --git a/public/icons/burst_pipe.svg b/public/icons/burst_pipe.svg new file mode 100644 index 0000000..3528977 --- /dev/null +++ b/public/icons/burst_pipe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/burst_pipe_icon.svg b/public/icons/burst_pipe_icon.svg deleted file mode 100644 index 524c549..0000000 --- a/public/icons/burst_pipe_icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/icons/pump.svg b/public/icons/pump.svg new file mode 100644 index 0000000..66319cf --- /dev/null +++ b/public/icons/pump.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/reservoirs.svg b/public/icons/reservoirs.svg new file mode 100644 index 0000000..d5ed17f --- /dev/null +++ b/public/icons/reservoirs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/scada_flow.svg b/public/icons/scada_flow.svg new file mode 100644 index 0000000..75baf92 --- /dev/null +++ b/public/icons/scada_flow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/scada_pressure.svg b/public/icons/scada_pressure.svg new file mode 100644 index 0000000..3adb45a --- /dev/null +++ b/public/icons/scada_pressure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/sensor.svg b/public/icons/sensor.svg new file mode 100644 index 0000000..54a7dd3 --- /dev/null +++ b/public/icons/sensor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/tank.svg b/public/icons/tank.svg new file mode 100644 index 0000000..b9562af --- /dev/null +++ b/public/icons/tank.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/(main)/burst-pipe-analysis/page.tsx b/src/app/(main)/burst-pipe-analysis/page.tsx index bdaef6e..7f1754c 100644 --- a/src/app/(main)/burst-pipe-analysis/page.tsx +++ b/src/app/(main)/burst-pipe-analysis/page.tsx @@ -8,7 +8,7 @@ export default function Home() { return (
- +
diff --git a/src/app/(main)/network-simulation/page.tsx b/src/app/(main)/network-simulation/page.tsx index b44fcca..fadecef 100644 --- a/src/app/(main)/network-simulation/page.tsx +++ b/src/app/(main)/network-simulation/page.tsx @@ -14,35 +14,35 @@ const mockDevices = [ name: "SCADA-001", type: "pressure", coordinates: [121.4737, 31.2304] as [number, number], - status: "online" as const, + status: "在线" as const, }, { id: "SCADA-002", name: "SCADA-002", type: "flow", coordinates: [121.4807, 31.2204] as [number, number], - status: "warning" as const, + status: "警告" as const, }, { id: "SCADA-003", name: "SCADA-003", type: "pressure", coordinates: [121.4607, 31.2354] as [number, number], - status: "offline" as const, + status: "离线" as const, }, { id: "SCADA-004", name: "SCADA-004", type: "demand", coordinates: [121.4457, 31.2104] as [number, number], - status: "online" as const, + status: "在线" as const, }, { id: "SCADA-005", name: "SCADA-005", type: "level", coordinates: [121.4457, 31.2104] as [number, number], - status: "online" as const, + status: "在线" as const, }, ]; @@ -77,21 +77,21 @@ export default function Home() { return (
- + + + - -
); } diff --git a/src/app/(main)/scada-data-cleaning/page.tsx b/src/app/(main)/scada-data-cleaning/page.tsx index a1b2d47..127b954 100644 --- a/src/app/(main)/scada-data-cleaning/page.tsx +++ b/src/app/(main)/scada-data-cleaning/page.tsx @@ -13,35 +13,35 @@ const mockDevices = [ name: "SCADA-001", type: "pressure", coordinates: [121.4737, 31.2304] as [number, number], - status: "online" as const, + status: "在线" as const, }, { id: "SCADA-002", name: "SCADA-002", type: "flow", coordinates: [121.4807, 31.2204] as [number, number], - status: "warning" as const, + status: "警告" as const, }, { id: "SCADA-003", name: "SCADA-003", type: "pressure", coordinates: [121.4607, 31.2354] as [number, number], - status: "offline" as const, + status: "离线" as const, }, { id: "SCADA-004", name: "SCADA-004", type: "demand", coordinates: [121.4457, 31.2104] as [number, number], - status: "online" as const, + status: "在线" as const, }, { id: "SCADA-005", name: "SCADA-005", type: "level", coordinates: [121.4457, 31.2104] as [number, number], - status: "online" as const, + status: "在线" as const, }, ]; @@ -77,19 +77,19 @@ export default function Home() {
+ + - -
); } diff --git a/src/app/OlMap/Controls/LayerControl.tsx b/src/app/OlMap/Controls/LayerControl.tsx index e5058cb..0785ed2 100644 --- a/src/app/OlMap/Controls/LayerControl.tsx +++ b/src/app/OlMap/Controls/LayerControl.tsx @@ -3,6 +3,7 @@ import { useMap } from "../MapComponent"; import { Layer } from "ol/layer"; import { Checkbox, FormControlLabel } from "@mui/material"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; +import VectorLayer from "ol/layer/Vector"; const LayerControl: React.FC = () => { const map = useMap(); @@ -16,7 +17,10 @@ const LayerControl: React.FC = () => { const mapLayers = map .getLayers() .getArray() - .filter((layer) => layer instanceof WebGLVectorTileLayer) as Layer[]; + .filter( + (layer) => + layer instanceof WebGLVectorTileLayer || layer instanceof VectorLayer + ) as Layer[]; setLayers(mapLayers); const visible = new Map(); mapLayers.forEach((layer) => { diff --git a/src/app/OlMap/Controls/Timeline.tsx b/src/app/OlMap/Controls/Timeline.tsx index d772ac2..5f11322 100644 --- a/src/app/OlMap/Controls/Timeline.tsx +++ b/src/app/OlMap/Controls/Timeline.tsx @@ -25,8 +25,9 @@ import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; import { TbRewindBackward15, TbRewindForward15 } from "react-icons/tb"; import { FiSkipBack, FiSkipForward } from "react-icons/fi"; import { useData } from "../MapComponent"; -import { config } from "@/config/config"; +import { config, NETWORK_NAME } from "@/config/config"; import { useMap } from "../MapComponent"; +import { Network } from "inspector/promises"; const backendUrl = config.backendUrl; interface TimelineProps { @@ -69,6 +70,7 @@ const Timeline: React.FC = ({ const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(5000); // 毫秒 const [calculatedInterval, setCalculatedInterval] = useState(15); // 分钟 + const [isCalculating, setIsCalculating] = useState(false); // 计算时间轴范围 const minTime = timeRange @@ -105,6 +107,7 @@ const Timeline: React.FC = ({ // 检查node缓存 if (junctionProperties !== "") { const nodeCacheKey = `${query_time}_${junctionProperties}_${schemeName}`; + console.log("Node Cache Key:", nodeCacheKey); if (nodeCacheRef.current.has(nodeCacheKey)) { nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!; } else { @@ -121,7 +124,7 @@ const Timeline: React.FC = ({ // 检查link缓存 if (pipeProperties !== "") { - const linkCacheKey = `${query_time}_${pipeProperties}`; + const linkCacheKey = `${query_time}_${pipeProperties}_${schemeName}`; if (linkCacheRef.current.has(linkCacheKey)) { linkRecords = linkCacheRef.current.get(linkCacheKey)!; } else { @@ -148,9 +151,9 @@ const Timeline: React.FC = ({ if (!nodeResponse.ok) throw new Error(`Node fetch failed: ${nodeResponse.status}`); nodeRecords = await nodeResponse.json(); - // 缓存数据 + // 缓存数据(修复键以包含 schemeName) nodeCacheRef.current.set( - `${query_time}_${junctionProperties}`, + `${query_time}_${junctionProperties}_${schemeName}`, nodeRecords || [] ); } @@ -159,9 +162,9 @@ const Timeline: React.FC = ({ if (!linkResponse.ok) throw new Error(`Link fetch failed: ${linkResponse.status}`); linkRecords = await linkResponse.json(); - // 缓存数据 + // 缓存数据(修复键以包含 schemeName) linkCacheRef.current.set( - `${query_time}_${pipeProperties}`, + `${query_time}_${pipeProperties}_${schemeName}`, linkRecords || [] ); } @@ -429,6 +432,63 @@ const Timeline: React.FC = ({ } }, [map, handlePause]); + const handleForceCalculate = async () => { + if (!NETWORK_NAME) { + open?.({ + type: "error", + message: "方案名称未设置,无法进行强制计算。", + }); + return; + } + + setIsCalculating(true); + // 显示处理中的通知 + open?.({ + type: "progress", + message: "正在强制计算,请稍候...", + undoableTimeout: 3, + }); + try { + const body = { + name: NETWORK_NAME, + simulation_date: selectedDate.toISOString().split("T")[0], // YYYY-MM-DD + start_time: `${formatTime(currentTime)}:00`, // HH:MM:00 + duration: calculatedInterval, + }; + + const response = await fetch( + `${backendUrl}/runsimulationmanuallybydate/`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + } + ); + + if (response.ok) { + open?.({ + type: "success", + message: "重新计算成功", + }); + } else { + open?.({ + type: "error", + message: "重新计算失败", + }); + } + } catch (error) { + console.error("Recalculation failed:", error); + open?.({ + type: "error", + message: "重新计算时发生错误", + }); + } finally { + setIsCalculating(false); + } + }; + return (
@@ -574,7 +634,8 @@ const Timeline: React.FC = ({ variant="outlined" size="small" startIcon={} - // onClick={onRefresh} + onClick={handleForceCalculate} + disabled={isCalculating} > 强制计算 @@ -610,6 +671,7 @@ const Timeline: React.FC = ({ "& .MuiSlider-track": { backgroundColor: "primary.main", height: 6, + display: timeRange ? "none" : "block", }, "& .MuiSlider-rail": { backgroundColor: "grey.300", diff --git a/src/app/OlMap/Controls/Toolbar.tsx b/src/app/OlMap/Controls/Toolbar.tsx index 51759ea..171c514 100644 --- a/src/app/OlMap/Controls/Toolbar.tsx +++ b/src/app/OlMap/Controls/Toolbar.tsx @@ -59,12 +59,13 @@ interface LayerStyleState { // 添加接口定义隐藏按钮的props interface ToolbarProps { hiddenButtons?: string[]; // 可选的隐藏按钮列表,例如 ['info', 'draw', 'style'] + queryType?: string; // 可选的查询类型参数 } -const Toolbar: React.FC = ({ hiddenButtons }) => { +const Toolbar: React.FC = ({ hiddenButtons, queryType }) => { const map = useMap(); const data = useData(); if (!data) return null; - const { currentTime, selectedDate } = data; + const { currentTime, selectedDate, schemeName } = data; const [activeTools, setActiveTools] = useState([]); const [highlightFeature, setHighlightFeature] = useState( null @@ -319,9 +320,16 @@ const Toolbar: React.FC = ({ hiddenButtons }) => { dateObj.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0); // 转为 UTC ISO 字符串 const querytime = dateObj.toISOString(); // 例如 "2025-09-16T16:30:00.000Z" - const response = await fetch( - `${backendUrl}/queryrecordsbyidtime/?id=${id}&querytime=${querytime}&type=${type}` - ); + let response; + if (queryType === "scheme") { + response = await fetch( + `${backendUrl}/queryschemesimulationrecordsbyidtime/?scheme_name=${schemeName}&id=${id}&querytime=${querytime}&type=${type}` + ); + } else { + response = await fetch( + `${backendUrl}/querysimulationrecordsbyidtime/?id=${id}&querytime=${querytime}&type=${type}` + ); + } if (!response.ok) { throw new Error("API request failed"); } @@ -333,7 +341,7 @@ const Toolbar: React.FC = ({ hiddenButtons }) => { } }; // 仅当 currentTime 有效时查询 - if (currentTime !== -1) queryComputedProperties(); + if (currentTime !== -1 && queryType) queryComputedProperties(); }, [highlightFeature, currentTime, selectedDate]); // 从要素属性中提取属性面板需要的数据 diff --git a/src/app/OlMap/MapComponent.tsx b/src/app/OlMap/MapComponent.tsx index 445f65f..a5cf51a 100644 --- a/src/app/OlMap/MapComponent.tsx +++ b/src/app/OlMap/MapComponent.tsx @@ -23,6 +23,10 @@ import { Deck } from "@deck.gl/core"; import { TextLayer } from "@deck.gl/layers"; import { TripsLayer } from "@deck.gl/geo-layers"; import { CollisionFilterExtension } from "@deck.gl/extensions"; +import VectorSource from "ol/source/Vector"; +import GeoJson from "ol/format/GeoJSON"; +import VectorLayer from "ol/layer/Vector"; +import { Style, Icon } from "ol/style"; interface MapComponentProps { children?: React.ReactNode; @@ -31,6 +35,8 @@ interface DataContextType { currentTime?: number; // 当前时间 setCurrentTime?: React.Dispatch>; selectedDate?: Date; // 选择的日期 + schemeName?: string; // 当前方案名称 + setSchemeName?: React.Dispatch>; setSelectedDate?: React.Dispatch>; currentJunctionCalData?: any[]; // 当前计算结果 setCurrentJunctionCalData?: React.Dispatch>; @@ -44,6 +50,7 @@ interface DataContextType { pipeText: string; setJunctionText?: React.Dispatch>; setPipeText?: React.Dispatch>; + scadaData?: any[]; // SCADA 数据 } // 创建自定义Layer类来包装deck.gl @@ -103,6 +110,7 @@ const MapComponent: React.FC = ({ children }) => { const [currentTime, setCurrentTime] = useState(-1); // 默认选择当前时间 // const [selectedDate, setSelectedDate] = useState(new Date("2025-9-17")); const [selectedDate, setSelectedDate] = useState(new Date()); // 默认今天 + const [schemeName, setSchemeName] = useState(""); // 当前方案名称 const [currentJunctionCalData, setCurrentJunctionCalData] = useState( [] @@ -165,7 +173,22 @@ const MapComponent: React.FC = ({ children }) => { setPipeDataState((prev) => [...prev, ...uniqueNewData]); } }; + // 配置地图数据源、图层和样式 const defaultFlatStyle: FlatStyleLike = config.mapDefaultStyle; + // 定义 SCADA 图层的样式函数,根据 type 字段选择不同图标 + const scadaStyle = (feature: any) => { + const type = feature.get("type"); + const scadaPressureIcon = "/icons/scada_pressure.svg"; + const scadaFlowIcon = "/icons/scada_flow.svg"; + // 如果 type 不匹配,可以设置默认图标或不显示 + return new Style({ + image: new Icon({ + src: type === "pipe_flow" ? scadaFlowIcon : scadaPressureIcon, + scale: 0.1, // 根据需要调整图标大小 + anchor: [0.5, 0.5], // 图标锚点居中 + }), + }); + }; // 矢量瓦片数据源和图层 const junctionSource = new VectorTileSource({ url: `${mapUrl}/gwc/service/tms/1.0.0/TJWater:geo_junctions_mat@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`, // 替换为你的 MVT 瓦片服务 URL @@ -177,6 +200,10 @@ const MapComponent: React.FC = ({ children }) => { format: new MVT(), projection: "EPSG:3857", }); + const scadaSource = new VectorSource({ + url: `${mapUrl}/TJWater/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=TJWater:geo_scada&outputFormat=application/json`, + format: new GeoJson(), + }); // WebGL 渲染优化显示 const junctionLayer = new WebGLVectorTileLayer({ source: junctionSource as any, // 使用 WebGL 渲染 @@ -223,7 +250,19 @@ const MapComponent: React.FC = ({ children }) => { ], }, }); - + const scadaLayer = new VectorLayer({ + source: scadaSource, + style: scadaStyle, + // extent: extent, // 设置图层范围 + maxZoom: 24, + minZoom: 12, + properties: { + name: "SCADA", // 设置图层名称 + value: "scada", + type: "point", + properties: [], + }, + }); useEffect(() => { if (!mapRef.current) return; // 缓存 junction、pipe 数据,提供给 deck.gl 显示标签使用 @@ -340,7 +379,6 @@ const MapComponent: React.FC = ({ children }) => { console.error("Pipe tile load error:", error); } }); - // 更新标签可见性状态 // 监听 junctionLayer 的 visible 变化 const handleJunctionVisibilityChange = () => { const isVisible = junctionLayer.getVisible(); @@ -361,7 +399,7 @@ const MapComponent: React.FC = ({ children }) => { projection: "EPSG:3857", }), // 图层依面、线、点、标注次序添加 - layers: [pipeLayer, junctionLayer], + layers: [pipeLayer, junctionLayer, scadaLayer], controls: [], }); setMap(map); @@ -417,7 +455,7 @@ const MapComponent: React.FC = ({ children }) => { d[junctionText] ? (d[junctionText] as number).toFixed(3) : "", getSize: 18, fontWeight: "bold", - getColor: [255, 138, 92], + getColor: [0, 0, 0], getAngle: 0, getTextAnchor: "middle", getAlignmentBaseline: "center", @@ -593,6 +631,8 @@ const MapComponent: React.FC = ({ children }) => { setCurrentTime, selectedDate, setSelectedDate, + schemeName, + setSchemeName, currentJunctionCalData, setCurrentJunctionCalData, currentPipeCalData, diff --git a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx index 9ea065f..c188fd6 100644 --- a/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx +++ b/src/components/olmap/BurstPipeAnalysis/AnalysisParameters.tsx @@ -110,7 +110,7 @@ const AnalysisParameters: React.FC = () => { new Style({ geometry: new Point(midPointMercator), image: new Icon({ - src: "/icons/burst_pipe_icon.svg", + src: "/icons/burst_pipe.svg", scale: 0.2, anchor: [0.5, 1], }), diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx index a10d40f..56aad29 100644 --- a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx @@ -32,14 +32,20 @@ import { config, NETWORK_NAME } from "@config/config"; import { useNotification } from "@refinedev/core"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; -import { useMap } from "@app/OlMap/MapComponent"; -import * as turf from "@turf/turf"; +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 { Stroke, Style, Icon } from "ol/style"; import Feature, { FeatureLike } from "ol/Feature"; -import { along, lineString, length, toMercator } from "@turf/turf"; +import { + along, + lineString, + length, + toMercator, + bbox, + featureCollection, +} from "@turf/turf"; import { Point } from "ol/geom"; import { toLonLat } from "ol/proj"; import Timeline from "@app/OlMap/Controls/Timeline"; @@ -100,7 +106,6 @@ const SchemeQuery: React.FC = ({ { start: Date; end: Date } | undefined >(); const [internalSchemes, setInternalSchemes] = useState([]); - const [schemeName, setSchemeName] = useState(""); const [loading, setLoading] = useState(false); const [expandedId, setExpandedId] = useState(null); const [mapContainer, setMapContainer] = useState(null); // 地图容器元素 @@ -108,7 +113,9 @@ const SchemeQuery: React.FC = ({ const { open } = useNotification(); const map = useMap(); - + const data = useData(); + if (!data) return null; + const { schemeName, setSchemeName } = data; // 使用外部提供的 schemes 或内部状态 const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; @@ -183,9 +190,7 @@ const SchemeQuery: React.FC = ({ geojsonFormat.writeFeatureObject(feature) ); - const extent = turf.bbox( - turf.featureCollection(geojsonFeatures as any) - ); + const extent = bbox(featureCollection(geojsonFeatures as any)); if (extent) { map?.getView().fit(extent, { maxZoom: 18, duration: 1000 }); @@ -211,7 +216,9 @@ const SchemeQuery: React.FC = ({ ); setSelectedDate(schemeDate); setTimeRange({ start, end }); - setSchemeName(scheme.schemeName); + if (setSchemeName) { + setSchemeName(scheme.schemeName); + } handleLocatePipes(burstPipeIds); } }; @@ -270,7 +277,7 @@ const SchemeQuery: React.FC = ({ new Style({ geometry: new Point(midPointMercator), image: new Icon({ - src: "/icons/burst_pipe_icon.svg", + src: "/icons/burst_pipe.svg", scale: 0.2, anchor: [0.5, 1], }), @@ -283,6 +290,8 @@ const SchemeQuery: React.FC = ({ const highlightLayer = new VectorLayer({ source: new VectorSource(), style: burstPipeStyle, + maxZoom: 24, + minZoom: 12, properties: { name: "爆管管段高亮", value: "burst_pipe_highlight", diff --git a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx index f694115..afcf3ff 100644 --- a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx +++ b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Box, Button, @@ -30,7 +30,12 @@ import { config, NETWORK_NAME } from "@config/config"; import { useNotification } from "@refinedev/core"; import { useMap } from "@app/OlMap/MapComponent"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; -import * as turf from "@turf/turf"; +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"; interface SchemeRecord { id: number; @@ -74,23 +79,68 @@ const SchemeQuery: React.FC = ({ const { open } = useNotification(); const map = useMap(); + const [highlightLayer, setHighlightLayer] = + useState | null>(null); + const [highlightFeatures, setHighlightFeatures] = useState([]); // 使用外部提供的 schemes 或内部状态 const schemes = externalSchemes !== undefined ? externalSchemes : internalSchemes; const setSchemes = onSchemesChange || setInternalSchemes; - // 格式化日期 - const formatTime = (timeStr: string) => { - const time = moment(timeStr); - return time.format("YYYY-MM-DD HH:mm:ss"); - }; - // 格式化简短日期 const formatShortDate = (timeStr: string) => { const time = moment(timeStr); return time.format("MM-DD"); }; + // 初始化管道图层和高亮图层 + useEffect(() => { + if (!map) return; + // 定义传感器样式 + const sensorStyle = new Style({ + image: new Icon({ + src: "/icons/sensor.svg", + scale: 0.2, + anchor: [0.5, 1], + }), + }); + // 创建高亮图层 - 爆管管段标识样式 + const highlightLayer = new VectorLayer({ + source: new VectorSource(), + style: sensorStyle, + maxZoom: 24, + minZoom: 12, + properties: { + name: "传感器高亮", + value: "sensor_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]); // 查询方案 const handleQuery = async () => { if (!queryAll && !queryDate) return; @@ -158,29 +208,19 @@ const SchemeQuery: React.FC = ({ if (sensorIds.length > 0) { queryFeaturesByIds(sensorIds).then((features) => { if (features.length > 0) { - // 计算范围并缩放 - const geojsonFormat = new (window as any).ol.format.GeoJSON(); + // 设置高亮要素 + setHighlightFeatures(features); + // 将 OpenLayers Feature 转换为 GeoJSON Feature + const geojsonFormat = new GeoJSON(); const geojsonFeatures = features.map((feature) => geojsonFormat.writeFeatureObject(feature) ); - const extent = turf.bbox( - turf.featureCollection(geojsonFeatures as any) - ); + const extent = bbox(featureCollection(geojsonFeatures as any)); if (extent) { - map.getView().fit(extent, { maxZoom: 18, duration: 1000 }); + map?.getView().fit(extent, { maxZoom: 18, duration: 1000 }); } - - open?.({ - type: "success", - message: `已定位 ${features.length} 个传感器位置`, - }); - } else { - open?.({ - type: "error", - message: "未找到传感器位置", - }); } }); } @@ -321,7 +361,8 @@ const SchemeQuery: React.FC = ({ variant="caption" className="text-gray-500 block" > - 最小半径: {scheme.minDiameter} · 用户: {scheme.user} · 日期: {formatShortDate(scheme.create_time)} + 最小半径: {scheme.minDiameter} · 用户: {scheme.user} · + 日期: {formatShortDate(scheme.create_time)} {/* 操作按钮 */} @@ -434,54 +475,58 @@ const SchemeQuery: React.FC = ({ {/* 传感器位置列表 */} - {scheme.sensorLocation && scheme.sensorLocation.length > 0 && ( - - - 传感器位置 ({scheme.sensorLocation.length}个): - - - - {scheme.sensorLocation.map((sensorId, index) => ( - { - e.preventDefault(); - handleLocateSensors([sensorId]); - }} - > - {sensorId} - - ))} + {scheme.sensorLocation && + scheme.sensorLocation.length > 0 && ( + + + 传感器位置 ({scheme.sensorLocation.length}个): + + + + {scheme.sensorLocation.map( + (sensorId, index) => ( + { + e.preventDefault(); + handleLocateSensors([sensorId]); + }} + > + {sensorId} + + ) + )} + - - )} + )} {/* 操作按钮区域 */} - {scheme.sensorLocation && scheme.sensorLocation.length > 0 && ( - - )} + {scheme.sensorLocation && + scheme.sensorLocation.length > 0 && ( + + )}