diff --git a/src/app/OlMap/Controls/StyleEditorPanel.tsx b/src/app/OlMap/Controls/StyleEditorPanel.tsx index 8614100..deaa6b5 100644 --- a/src/app/OlMap/Controls/StyleEditorPanel.tsx +++ b/src/app/OlMap/Controls/StyleEditorPanel.tsx @@ -104,6 +104,40 @@ const GRADIENT_PALETTES = [ end: "rgba(148, 103, 189, 1)", }, ]; +// 离散彩虹色系 - 提供高区分度的颜色 +const RAINBOW_PALETTES = [ + { + name: "正向彩虹", + colors: [ + "rgba(255, 0, 0, 1)", // 红 #FF0000 + "rgba(255, 127, 0, 1)", // 橙 #FF7F00 + "rgba(255, 215, 0, 1)", // 金黄 #FFD700 + "rgba(199, 224, 0, 1)", // 黄绿 #C7E000 + "rgba(76, 175, 80, 1)", // 中绿 #4CAF50 + "rgba(0, 158, 115, 1)", // 青绿/翡翠 #009E73 + "rgba(0, 188, 212, 1)", // 青/青色 #00BCD4 + "rgba(33, 150, 243, 1)", // 天蓝 #2196F3 + "rgba(63, 81, 181, 1)", // 靛青 #3F51B5 + "rgba(142, 68, 173, 1)", // 紫 #8E44AD + ], + }, + { + name: "反向彩虹", + colors: [ + "rgba(142, 68, 173, 1)", // 紫 #8E44AD + "rgba(63, 81, 181, 1)", // 靛青 #3F51B5 + "rgba(33, 150, 243, 1)", // 天蓝 #2196F3 + "rgba(0, 188, 212, 1)", // 青/青色 #00BCD4 + "rgba(0, 158, 115, 1)", // 青绿/翡翠 #009E73 + "rgba(76, 175, 80, 1)", // 中绿 #4CAF50 + "rgba(199, 224, 0, 1)", // 黄绿 #C7E000 + "rgba(255, 215, 0, 1)", // 金黄 #FFD700 + "rgba(255, 127, 0, 1)", // 橙 #FF7F00 + "rgba(255, 0, 0, 1)", // 红 #FF0000 + ], + }, +]; + // 预设分类方法 const CLASSIFICATION_METHODS = [ { name: "优雅分段", value: "pretty_breaks" }, @@ -164,6 +198,7 @@ const StyleEditorPanel: React.FC = ({ // 颜色方案选择 const [singlePaletteIndex, setSinglePaletteIndex] = useState(0); const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0); + const [rainbowPaletteIndex, setRainbowPaletteIndex] = useState(0); // 根据分段数生成相应数量的渐进颜色 const generateGradientColors = useCallback( (segments: number): string[] => { @@ -189,6 +224,29 @@ const StyleEditorPanel: React.FC = ({ }, [gradientPaletteIndex, parseColor] ); + + // 根据分段数生成彩虹色 + const generateRainbowColors = useCallback( + (segments: number): string[] => { + const baseColors = RAINBOW_PALETTES[rainbowPaletteIndex].colors; + + if (segments <= baseColors.length) { + // 如果分段数小于等于基础颜色数,均匀选取 + const step = baseColors.length / segments; + return Array.from( + { length: segments }, + (_, i) => baseColors[Math.floor(i * step)] + ); + } else { + // 如果分段数大于基础颜色数,重复使用 + return Array.from( + { length: segments }, + (_, i) => baseColors[i % baseColors.length] + ); + } + }, + [rainbowPaletteIndex] + ); // 保存当前图层的样式状态 const saveLayerStyle = useCallback( (layerId?: string, newLegendConfig?: LegendStyleConfig) => { @@ -350,7 +408,9 @@ const StyleEditorPanel: React.FC = ({ Array.from({ length: breaksLength }, () => { return SINGLE_COLOR_PALETTES[singlePaletteIndex].color; }) - : generateGradientColors(breaksLength); + : styleConfig.colorType === "gradient" + ? generateGradientColors(breaksLength) + : generateRainbowColors(breaksLength); // 计算每个分段的线条粗细和点大小 const dimensions: number[] = layerType === "linestring" @@ -803,6 +863,73 @@ const StyleEditorPanel: React.FC = ({ ); } + if (styleConfig.colorType === "rainbow") { + return ( + + 离散彩虹方案 + + + ); + } }; // 根据不同图层的类型和颜色分类方案显示不同的大小设置 const getSizeSetting = () => { @@ -813,7 +940,9 @@ const StyleEditorPanel: React.FC = ({ } else if (styleConfig.colorType === "gradient") { const { start, end } = GRADIENT_PALETTES[gradientPaletteIndex]; colors = [start, end]; - } else if (styleConfig.colorType === "categorical") { + } else if (styleConfig.colorType === "rainbow") { + const rainbowColors = RAINBOW_PALETTES[rainbowPaletteIndex].colors; + colors = [rainbowColors[0], rainbowColors[rainbowColors.length - 1]]; } if (selectedRenderLayer?.get("type") === "point") { @@ -1024,10 +1153,20 @@ const StyleEditorPanel: React.FC = ({ } onChange={(e) => { const index = e.target.value as number; - setSelectedRenderLayer( - index >= 0 ? renderLayers[index] : undefined - ); - setStyleConfig((prev) => ({ ...prev, property: "" })); + const newLayer = index >= 0 ? renderLayers[index] : undefined; + setSelectedRenderLayer(newLayer); + + // 检查新图层是否有缓存的样式,没有才清空 + if (newLayer) { + const layerId = newLayer.get("value"); + const cachedStyleState = layerStyleStates.find( + (state) => state.layerId === layerId + ); + // 只有在没有缓存时才清空属性 + if (!cachedStyleState) { + setStyleConfig((prev) => ({ ...prev, property: "" })); + } + } }} > {renderLayers.map((layer, index) => { @@ -1102,13 +1241,13 @@ const StyleEditorPanel: React.FC = ({ onChange={(e) => setStyleConfig((prev) => ({ ...prev, - colorType: e.target.value as "gradient" | "categorical", + colorType: e.target.value as "single" | "gradient" | "rainbow", })) } > 单一色 渐进色 - {/* 分类色 */} + 离散彩虹 {getColorSetting()} diff --git a/src/app/OlMap/Controls/Timeline.tsx b/src/app/OlMap/Controls/Timeline.tsx index eddcc85..096e6f1 100644 --- a/src/app/OlMap/Controls/Timeline.tsx +++ b/src/app/OlMap/Controls/Timeline.tsx @@ -27,7 +27,6 @@ import { FiSkipBack, FiSkipForward } from "react-icons/fi"; import { useData } from "../MapComponent"; import { config, NETWORK_NAME } from "@/config/config"; import { useMap } from "../MapComponent"; -import { Network } from "inspector/promises"; const backendUrl = config.backendUrl; interface TimelineProps { @@ -139,10 +138,6 @@ const Timeline: React.FC = ({ } } - // console.log( - // "Query Time:", - // queryTime.toLocaleDateString() + " " + queryTime.toLocaleTimeString() - // ); // 等待所有有效请求 const responses = await Promise.all(requests); @@ -211,7 +206,6 @@ const Timeline: React.FC = ({ // 播放时间间隔选项 const intervalOptions = [ - // { value: 1000, label: "1秒" }, { value: 2000, label: "2秒" }, { value: 5000, label: "5秒" }, { value: 10000, label: "10秒" }, @@ -239,7 +233,7 @@ const Timeline: React.FC = ({ } debounceRef.current = setTimeout(() => { setCurrentTime(value); - }, 300); // 300ms 防抖延迟 + }, 500); // 500ms 防抖延迟 }, [timeRange, minTime, maxTime] ); @@ -441,6 +435,11 @@ const Timeline: React.FC = ({ return; } + // 提前提取日期和时间值,避免异步操作期间被时间轴拖动改变 + const calculationDate = selectedDate; + const calculationTime = currentTime; + const calculationDateStr = calculationDate.toISOString().split("T")[0]; + setIsCalculating(true); // 显示处理中的通知 open?.({ @@ -451,8 +450,8 @@ const Timeline: React.FC = ({ try { const body = { name: NETWORK_NAME, - simulation_date: selectedDate.toISOString().split("T")[0], // YYYY-MM-DD - start_time: `${formatTime(currentTime)}:00`, // HH:MM:00 + simulation_date: calculationDateStr, // YYYY-MM-DD + start_time: `${formatTime(calculationTime)}:00`, // HH:MM:00 duration: calculatedInterval, }; @@ -468,6 +467,44 @@ const Timeline: React.FC = ({ ); if (response.ok) { + // 清空当天当前时刻及之后的缓存 + const currentDateStr = calculationDateStr; + const currentTimeInMinutes = calculationTime; + + // 清空node缓存 + const nodeCacheKeys = Array.from(nodeCacheRef.current.keys()); + nodeCacheKeys.forEach((key) => { + const keyParts = key.split("_"); + const cacheDate = keyParts[0].split("T")[0]; + const cacheTimeStr = keyParts[0].split("T")[1]; + + if (cacheDate === currentDateStr && cacheTimeStr) { + const [hours, minutes] = cacheTimeStr.split(":"); + const cacheTimeInMinutes = parseInt(hours) * 60 + parseInt(minutes); + + if (cacheTimeInMinutes >= currentTimeInMinutes) { + nodeCacheRef.current.delete(key); + } + } + }); + + // 清空link缓存 + const linkCacheKeys = Array.from(linkCacheRef.current.keys()); + linkCacheKeys.forEach((key) => { + const keyParts = key.split("_"); + const cacheDate = keyParts[0].split("T")[0]; + const cacheTimeStr = keyParts[0].split("T")[1]; + + if (cacheDate === currentDateStr && cacheTimeStr) { + const [hours, minutes] = cacheTimeStr.split(":"); + const cacheTimeInMinutes = parseInt(hours) * 60 + parseInt(minutes); + + if (cacheTimeInMinutes >= currentTimeInMinutes) { + linkCacheRef.current.delete(key); + } + } + }); + open?.({ type: "success", message: "重新计算成功", diff --git a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx index 56aad29..3ef6ef5 100644 --- a/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx +++ b/src/components/olmap/BurstPipeAnalysis/SchemeQuery.tsx @@ -286,7 +286,7 @@ const SchemeQuery: React.FC = ({ } return styles; }; - // 创建高亮图层 - 爆管管段标识样式 + // 创建高亮图层 const highlightLayer = new VectorLayer({ source: new VectorSource(), style: burstPipeStyle, diff --git a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx index afcf3ff..43be43a 100644 --- a/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx +++ b/src/components/olmap/MonitoringPlaceOptimization/SchemeQuery.tsx @@ -103,7 +103,7 @@ const SchemeQuery: React.FC = ({ anchor: [0.5, 1], }), }); - // 创建高亮图层 - 爆管管段标识样式 + // 创建高亮图层 const highlightLayer = new VectorLayer({ source: new VectorSource(), style: sensorStyle, diff --git a/src/components/olmap/SCADADataPanel.tsx b/src/components/olmap/SCADADataPanel.tsx index bfea23c..40abc87 100644 --- a/src/components/olmap/SCADADataPanel.tsx +++ b/src/components/olmap/SCADADataPanel.tsx @@ -336,7 +336,7 @@ const SCADADataPanel: React.FC = ({ } else { setTimeSeries([]); } - }, [deviceIds.join(","), hasDevices]); + }, [deviceIds.join(",")]); // 移除 hasDevices,因为它由 deviceIds 决定,避免潜在的依赖循环 const columns: GridColDef[] = useMemo(() => { const base: GridColDef[] = [ diff --git a/src/components/olmap/SCADADeviceList.tsx b/src/components/olmap/SCADADeviceList.tsx index c1a0ba1..e881a19 100644 --- a/src/components/olmap/SCADADeviceList.tsx +++ b/src/components/olmap/SCADADeviceList.tsx @@ -42,8 +42,13 @@ import { FixedSizeList } from "react-window"; import { 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, Circle } from "ol/style"; +import Feature from "ol/Feature"; import { Point } from "ol/geom"; import config from "@/config/config"; +import { get } from "http"; const STATUS_OPTIONS: { value: "online" | "offline" | "warning" | "error"; @@ -57,12 +62,14 @@ const STATUS_OPTIONS: { interface SCADADevice { id: string; name: string; + transmission_frequency: string; + reliability: number; type: string; - coordinates: [number, number]; status: { value: "online" | "offline" | "warning" | "error"; name: "在线" | "离线" | "警告" | "错误"; }; + coordinates: [number, number]; properties?: Record; } @@ -85,6 +92,7 @@ const SCADADeviceList: React.FC = ({ const [searchQuery, setSearchQuery] = useState(""); const [selectedType, setSelectedType] = useState("all"); const [selectedStatus, setSelectedStatus] = useState("all"); + const [selectedReliability, setSelectedReliability] = useState("all"); const [isExpanded, setIsExpanded] = useState(true); const [internalSelection, setInternalSelection] = useState([]); const [pendingSelection, setPendingSelection] = useState( @@ -94,6 +102,10 @@ const SCADADeviceList: React.FC = ({ const [loading, setLoading] = useState(true); const [inputValue, setInputValue] = useState(""); + const [highlightLayer, setHighlightLayer] = + useState | null>(null); + const [highlightFeatures, setHighlightFeatures] = useState([]); + const debounceTimerRef = useRef(null); // 防抖更新搜索查询 @@ -139,6 +151,8 @@ const SCADADeviceList: React.FC = ({ const data = features.map((feature) => ({ id: feature.get("id") || feature.getId(), name: feature.get("id") || feature.getId(), + transmission_frequency: feature.get("transmission_frequency"), + reliability: feature.get("reliability"), type: feature.get("type") === "pipe_flow" ? "流量" : "压力", status: STATUS_OPTIONS[Math.floor(Math.random() * 4)], coordinates: (feature.getGeometry() as Point)?.getCoordinates() as [ @@ -170,6 +184,20 @@ const SCADADeviceList: React.FC = ({ // 获取设备状态列表 const deviceStatuses = STATUS_OPTIONS; + // 可靠度文字映射 + const getReliability = (reliability: number) => { + switch (reliability) { + case 1: + return "高"; + case 2: + return "中"; + case 3: + return "低"; + default: + return "未知"; + } + }; + // 过滤设备列表 const filteredDevices = useMemo(() => { return effectiveDevices.filter((device) => { @@ -186,10 +214,21 @@ const SCADADeviceList: React.FC = ({ selectedType === "all" || device.type === selectedType; const matchesStatus = selectedStatus === "all" || device.status.value === selectedStatus; + const matchesReliability = + selectedReliability === "all" || + getReliability(device.reliability) === selectedReliability; - return matchesSearch && matchesType && matchesStatus; + return ( + matchesSearch && matchesType && matchesStatus && matchesReliability + ); }); - }, [effectiveDevices, searchQuery, selectedType, selectedStatus]); + }, [ + effectiveDevices, + searchQuery, + selectedType, + selectedStatus, + selectedReliability, + ]); // 状态颜色映射 const getStatusColor = (status: string) => { @@ -222,6 +261,17 @@ const SCADADeviceList: React.FC = ({ return "●"; } }; + // 传输频率文字对应 + const getTransmissionFrequency = (transmission_frequency: string) => { + // 传输频率文本:00:01:00,00:05:00,00:10:00,00:30:00,01:00:00,转换为分钟数 + const parts = transmission_frequency.split(":"); + if (parts.length !== 3) return transmission_frequency; + const hours = parseInt(parts[0], 10); + const minutes = parseInt(parts[1], 10); + const seconds = parseInt(parts[2], 10); + const totalMinutes = hours * 60 + minutes + (seconds >= 30 ? 1 : 0); + return totalMinutes; + }; // 处理设备点击 const handleDeviceClick = (device: SCADADevice, event?: React.MouseEvent) => { @@ -264,6 +314,7 @@ const SCADADeviceList: React.FC = ({ setSearchQuery(""); setSelectedType("all"); setSelectedStatus("all"); + setSelectedReliability("all"); }); }, []); @@ -273,6 +324,56 @@ const SCADADeviceList: React.FC = ({ setPendingSelection([]); }, []); + // 初始化管道图层和高亮图层 + useEffect(() => { + if (!map) return; + // 获取地图的目标容器 + const SCADASelectedStyle = () => { + return new Style({ + image: new Circle({ + stroke: new Stroke({ color: "yellow", width: 2 }), + radius: 15, + }), + }); + }; + // 创建高亮图层 + const highlightLayer = new VectorLayer({ + source: new VectorSource(), + style: SCADASelectedStyle, + maxZoom: 24, + minZoom: 12, + properties: { + name: "SCADA 选中高亮", + value: "scada_selected_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); + } + }); + }, [selectedDeviceIds, highlightFeatures]); // 清理定时器 useEffect(() => { return () => { @@ -364,14 +465,14 @@ const SCADADeviceList: React.FC = ({ {/* 筛选器 */} - + 设备类型 - + 状态 + + 可靠度 + + + @@ -523,6 +638,14 @@ const SCADADeviceList: React.FC = ({ variant="outlined" sx={{ fontSize: "0.7rem", height: 20 }} /> + } secondary={ @@ -537,8 +660,11 @@ const SCADADeviceList: React.FC = ({ variant="caption" color="text.secondary" > - 坐标: {device.coordinates[0].toFixed(6)},{" "} - {device.coordinates[1].toFixed(6)} + 传输频率:{" "} + {getTransmissionFrequency( + device.transmission_frequency + )}{" "} + 分钟 }