修复lint errors

This commit is contained in:
JIANG
2026-03-10 17:52:00 +08:00
parent 62914f80c3
commit 73201ae44e
11 changed files with 343 additions and 378 deletions
@@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { import {
Box, Box,
Typography, Typography,
@@ -115,7 +115,7 @@ const EmptyState = () => (
const LocationResults: React.FC<Props> = ({ result }) => { const LocationResults: React.FC<Props> = ({ result }) => {
const map = useMap(); const map = useMap();
const [highlightLayer, setHighlightLayer] = useState<VectorLayer<VectorSource> | null>(null); const highlightLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]); const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const candidatePipes = useMemo<BurstCandidate[]>(() => { const candidatePipes = useMemo<BurstCandidate[]>(() => {
@@ -128,13 +128,13 @@ const LocationResults: React.FC<Props> = ({ result }) => {
return base; return base;
}, [result]); }, [result]);
const allCandidatePipeIds = useMemo<string[]>(() => { const allCandidatePipeIds = (() => {
const ids = candidatePipes.map((item) => item.pipe_id); const ids = candidatePipes.map((item) => item.pipe_id);
if (result?.located_pipe) { if (result?.located_pipe) {
ids.unshift(result.located_pipe); ids.unshift(result.located_pipe);
} }
return Array.from(new Set(ids.filter(Boolean))); return Array.from(new Set(ids.filter(Boolean)));
}, [candidatePipes, result?.located_pipe]); })();
useEffect(() => { useEffect(() => {
if (!map) return; if (!map) return;
@@ -159,19 +159,20 @@ const LocationResults: React.FC<Props> = ({ result }) => {
}, },
}); });
map.addLayer(layer); map.addLayer(layer);
setHighlightLayer(layer); highlightLayerRef.current = layer;
return () => { return () => {
highlightLayerRef.current = null;
map.removeLayer(layer); map.removeLayer(layer);
}; };
}, [map]); }, [map]);
useEffect(() => { useEffect(() => {
const source = highlightLayer?.getSource(); const source = highlightLayerRef.current?.getSource();
if (!source) return; if (!source) return;
source.clear(); source.clear();
highlightFeatures.forEach((feature) => source.addFeature(feature)); highlightFeatures.forEach((feature) => source.addFeature(feature));
}, [highlightFeatures, highlightLayer]); }, [highlightFeatures]);
const locatePipes = async (pipeIds: string[]) => { const locatePipes = async (pipeIds: string[]) => {
if (!pipeIds.length || !map) return; if (!pipeIds.length || !map) return;
@@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import { import {
Box, Box,
Typography, Typography,
@@ -41,8 +41,7 @@ interface LocationResultsProps {
const LocationResults: React.FC<LocationResultsProps> = ({ const LocationResults: React.FC<LocationResultsProps> = ({
results = [], results = [],
}) => { }) => {
const [highlightLayer, setHighlightLayer] = const highlightLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
useState<VectorLayer<VectorSource> | null>(null);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]); const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const map = useMap(); const map = useMap();
@@ -145,19 +144,17 @@ const LocationResults: React.FC<LocationResultsProps> = ({
}); });
map.addLayer(highlightLayer); map.addLayer(highlightLayer);
setHighlightLayer(highlightLayer); highlightLayerRef.current = highlightLayer;
return () => { return () => {
highlightLayerRef.current = null;
map.removeLayer(highlightLayer); map.removeLayer(highlightLayer);
}; };
}, [map]); }, [map]);
// 高亮要素的函数 // 高亮要素的函数
useEffect(() => { useEffect(() => {
if (!highlightLayer) { const source = highlightLayerRef.current?.getSource();
return;
}
const source = highlightLayer.getSource();
if (!source) { if (!source) {
return; return;
} }
@@ -169,7 +166,7 @@ const LocationResults: React.FC<LocationResultsProps> = ({
source.addFeature(feature); source.addFeature(feature);
} }
}); });
}, [highlightFeatures, highlightLayer]); }, [highlightFeatures]);
// 取第一条记录或空对象 // 取第一条记录或空对象
const result = results.length > 0 ? results[0] : null; const result = results.length > 0 ? results[0] : null;
@@ -1090,7 +1090,7 @@ const ValveIsolation: React.FC<ValveIsolationProps> = ({
</> </>
) : ( ) : (
<Alert severity="info" variant="outlined"> <Alert severity="info" variant="outlined">
2"扩大搜索" 2
</Alert> </Alert>
)} )}
</Box> </Box>
@@ -1,6 +1,6 @@
"use client"; "use client";
import React, { useState, useEffect, useRef, useCallback } from "react"; import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { useNotification } from "@refinedev/core"; import { useNotification } from "@refinedev/core";
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
@@ -27,7 +27,6 @@ import dayjs from "dayjs";
import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material";
import { TbArrowBackUp, TbArrowForwardUp } from "react-icons/tb"; import { TbArrowBackUp, TbArrowForwardUp } from "react-icons/tb";
import { FiSkipBack, FiSkipForward } from "react-icons/fi"; import { FiSkipBack, FiSkipForward } from "react-icons/fi";
import { useData } from "@components/olmap/core/MapComponent";
import { config, NETWORK_NAME } from "@/config/config"; import { config, NETWORK_NAME } from "@/config/config";
import { apiFetch } from "@/lib/apiFetch"; import { apiFetch } from "@/lib/apiFetch";
import { useMap } from "@components/olmap/core/MapComponent"; import { useMap } from "@components/olmap/core/MapComponent";
@@ -63,10 +62,6 @@ interface TimelineProps {
const Timeline: React.FC<TimelineProps> = ({ const Timeline: React.FC<TimelineProps> = ({
disableDateSelection = false, disableDateSelection = false,
}) => { }) => {
const data = useData();
if (!data) {
return <div>Loading...</div>; // 或其他占位符
}
const { open } = useNotification(); const { open } = useNotification();
const { const {
predictionResults, predictionResults,
@@ -79,7 +74,6 @@ const Timeline: React.FC<TimelineProps> = ({
const [isPlaying, setIsPlaying] = useState<boolean>(false); const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒 const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒
const [isPredicting, setIsPredicting] = useState<boolean>(false); const [isPredicting, setIsPredicting] = useState<boolean>(false);
const [pipeLayer, setPipeLayer] = useState<WebGLVectorTileLayer | null>(null);
// 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定 // 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定
const healthDataRef = useRef<Map<string, number>>(new Map()); const healthDataRef = useRef<Map<string, number>>(new Map());
@@ -228,10 +222,21 @@ const Timeline: React.FC<TimelineProps> = ({
clearTimeout(debounceRef.current); clearTimeout(debounceRef.current);
} }
}; };
}, [pipeLayer]); }, []);
// 获取地图实例 // 获取地图实例
const map = useMap(); const map = useMap();
const pipeLayer = useMemo(() => {
if (!map) return null;
const layers = map.getLayers().getArray();
return (
layers.find(
(layer) =>
layer instanceof WebGLVectorTileLayer && layer.get("value") === "pipes",
) as WebGLVectorTileLayer | undefined
) ?? null;
}, [map]);
// 根据索引从 survival_function 中获取生存概率 // 根据索引从 survival_function 中获取生存概率
const getSurvivalProbabilityAtYear = useCallback( const getSurvivalProbabilityAtYear = useCallback(
@@ -362,21 +367,6 @@ const Timeline: React.FC<TimelineProps> = ({
updatePipeHealthData, updatePipeHealthData,
]); ]);
// 初始化管道图层
useEffect(() => {
if (!map) return;
const layers = map.getLayers().getArray();
const pipesLayer = layers.find(
(layer) =>
layer instanceof WebGLVectorTileLayer && layer.get("value") === "pipes",
) as WebGLVectorTileLayer | undefined;
if (pipesLayer) {
setPipeLayer(pipesLayer);
}
}, [map]);
// 监听依赖变化,更新样式 // 监听依赖变化,更新样式
useEffect(() => { useEffect(() => {
if (predictionResults.length > 0 && pipeLayer) { if (predictionResults.length > 0 && pipeLayer) {
@@ -31,9 +31,7 @@ import { useMap } from "../MapComponent";
const DrawPanel: React.FC = () => { const DrawPanel: React.FC = () => {
const map = useMap(); const map = useMap();
const [activeTool, setActiveTool] = useState<string>("pan"); const [activeTool, setActiveTool] = useState<string>("pan");
const [drawLayer, setDrawLayer] = useState<VectorLayer<VectorSource> | null>( const drawLayerRef = useRef<VectorLayer<VectorSource> | null>(null);
null
);
const [drawnFeatures, setDrawnFeatures] = useState<Feature<Geometry>[]>([]); const [drawnFeatures, setDrawnFeatures] = useState<Feature<Geometry>[]>([]);
const [history, setHistory] = useState<{ const [history, setHistory] = useState<{
stack: Feature<Geometry>[][]; stack: Feature<Geometry>[][];
@@ -79,13 +77,14 @@ const DrawPanel: React.FC = () => {
}); });
map.addLayer(drawVectorLayer); map.addLayer(drawVectorLayer);
setDrawLayer(drawVectorLayer); drawLayerRef.current = drawVectorLayer;
return () => { return () => {
if (drawInteractionRef.current && map) { if (drawInteractionRef.current && map) {
map.removeInteraction(drawInteractionRef.current); map.removeInteraction(drawInteractionRef.current);
drawInteractionRef.current = null; drawInteractionRef.current = null;
} }
drawLayerRef.current = null;
map.removeLayer(drawVectorLayer); map.removeLayer(drawVectorLayer);
}; };
}, [map, drawInteractionRef]); }, [map, drawInteractionRef]);
@@ -110,6 +109,7 @@ const DrawPanel: React.FC = () => {
type: GeometryType, type: GeometryType,
geometryFunction?: GeometryFunction geometryFunction?: GeometryFunction
) => { ) => {
const drawLayer = drawLayerRef.current;
if (!drawLayer) return; if (!drawLayer) return;
if (!map) return; if (!map) return;
@@ -285,6 +285,7 @@ const DrawPanel: React.FC = () => {
// 删除所有绘制的要素 // 删除所有绘制的要素
const handleDelete = () => { const handleDelete = () => {
const drawLayer = drawLayerRef.current;
if (!drawLayer) return; if (!drawLayer) return;
const source = drawLayer.getSource(); const source = drawLayer.getSource();
@@ -301,6 +302,7 @@ const DrawPanel: React.FC = () => {
// 更新绘图图层 // 更新绘图图层
const updateDrawLayer = (features: Feature<Geometry>[]) => { const updateDrawLayer = (features: Feature<Geometry>[]) => {
const drawLayer = drawLayerRef.current;
if (!drawLayer) return; if (!drawLayer) return;
const source = drawLayer.getSource(); const source = drawLayer.getSource();
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useMemo } from "react";
import { useData, useMap } from "../MapComponent"; import { useData, useMap } from "../MapComponent";
import { Checkbox, FormControlLabel } from "@mui/material"; import { Checkbox, FormControlLabel } from "@mui/material";
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
@@ -18,15 +18,12 @@ interface LayerItem {
const LayerControl: React.FC = () => { const LayerControl: React.FC = () => {
const map = useMap(); const map = useMap();
const data = useData(); const data = useData();
if (!data) return; const [refreshKey, setRefreshKey] = useState(0);
const { const deckLayer = data?.deckLayer;
deckLayer, const isContourLayerAvailable = data?.isContourLayerAvailable;
isContourLayerAvailable, const isWaterflowLayerAvailable = data?.isWaterflowLayerAvailable;
isWaterflowLayerAvailable, const setShowWaterflowLayer = data?.setShowWaterflowLayer;
setShowWaterflowLayer, const setShowContourLayer = data?.setShowContourLayer;
setShowContourLayer,
} = data;
const [layerItems, setLayerItems] = useState<LayerItem[]>([]);
const layerOrder = [ const layerOrder = [
"junctions", "junctions",
@@ -40,16 +37,12 @@ const LayerControl: React.FC = () => {
"junctionContourLayer", "junctionContourLayer",
]; ];
// 更新图层列表 const layerItems = useMemo(() => {
const updateLayers = useCallback(() => { if (!map || !data) return [];
if (!map || !data) return;
const items: LayerItem[] = []; const items: LayerItem[] = [];
// 1. 获取 OpenLayers 图层 map.getLayers().getArray().forEach((layer) => {
const mapLayers = map.getLayers().getArray();
mapLayers.forEach((layer) => {
// 筛选特定类型的 OpenLayers 图层
if ( if (
layer instanceof WebGLVectorTileLayer || layer instanceof WebGLVectorTileLayer ||
layer instanceof VectorTileLayer || layer instanceof VectorTileLayer ||
@@ -57,7 +50,6 @@ const LayerControl: React.FC = () => {
) { ) {
const value = layer.get("value"); const value = layer.get("value");
const name = layer.get("name"); const name = layer.get("name");
// 只有设置了 value (作为 ID) 的图层才会被纳入控制
if (value) { if (value) {
items.push({ items.push({
id: value, id: value,
@@ -70,66 +62,56 @@ const LayerControl: React.FC = () => {
} }
}); });
// 2. 获取 DeckLayer 中的子图层
if (deckLayer && deckLayer instanceof DeckLayer) { if (deckLayer && deckLayer instanceof DeckLayer) {
const deckLayers = deckLayer.getDeckLayers(); deckLayer.getDeckLayers().forEach((layer: any) => {
deckLayers.forEach((layer: any) => { if (!layer?.id) return;
if (layer && layer.id) { if (layer.id !== "junctionContourLayer" && layer.id !== "waterflowLayer") {
// 仅处理 junctionContourLayer 和 waterflowLayer return;
if (
layer.id !== "junctionContourLayer" &&
layer.id !== "waterflowLayer"
) {
return;
}
// 检查可用性
if (
(layer.id === "junctionContourLayer" && !isContourLayerAvailable) ||
(layer.id === "waterflowLayer" && !isWaterflowLayerAvailable)
) {
return; // 跳过不可用图层
}
const visible =
deckLayer.getDeckLayerVisible(layer.id) ??
layer.props?.visible ??
true;
items.push({
id: layer.props.id,
name: layer.props.name, // 使用 name 属性作为显示名称
visible: visible,
type: "deck",
layerRef: layer,
});
} }
if (
(layer.id === "junctionContourLayer" && !isContourLayerAvailable) ||
(layer.id === "waterflowLayer" && !isWaterflowLayerAvailable)
) {
return;
}
items.push({
id: layer.props.id,
name: layer.props.name,
visible:
deckLayer.getDeckLayerVisible(layer.id) ?? layer.props?.visible ?? true,
type: "deck",
layerRef: layer,
});
}); });
} }
// 过滤并排序 return items
const sortedItems = items
.filter((item) => layerOrder.includes(item.id)) .filter((item) => layerOrder.includes(item.id))
.sort((a, b) => { .sort((a, b) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b.id));
const indexA = layerOrder.indexOf(a.id); }, [
const indexB = layerOrder.indexOf(b.id); map,
return indexA - indexB; data,
}); deckLayer,
isContourLayerAvailable,
setLayerItems(sortedItems); isWaterflowLayerAvailable,
}, [map, deckLayer, isWaterflowLayerAvailable, isContourLayerAvailable]); refreshKey,
]);
useEffect(() => { useEffect(() => {
updateLayers(); if (!map) return;
if (map) { const layerCollection = map.getLayers();
const layerCollection = map.getLayers(); const handleLayerChange = () => {
layerCollection.on("change:length", updateLayers); setRefreshKey((prev) => prev + 1);
} };
layerCollection.on("change:length", handleLayerChange);
return () => { return () => {
if (map) { map.getLayers().un("change:length", handleLayerChange);
map.getLayers().un("change:length", updateLayers);
}
}; };
}, [map, updateLayers]); }, [map]);
const handleVisibilityChange = (item: LayerItem, checked: boolean) => { const handleVisibilityChange = (item: LayerItem, checked: boolean) => {
if (item.type === "ol") { if (item.type === "ol") {
@@ -142,10 +124,7 @@ const LayerControl: React.FC = () => {
setShowWaterflowLayer && setShowWaterflowLayer(checked); setShowWaterflowLayer && setShowWaterflowLayer(checked);
} }
} }
setRefreshKey((prev) => prev + 1);
setLayerItems((prev) =>
prev.map((i) => (i.id === item.id ? { ...i, visible: checked } : i)),
);
}; };
if (!data) { if (!data) {
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from "react"; import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
// 导入Material-UI图标和组件 // 导入Material-UI图标和组件
import ColorLensIcon from "@mui/icons-material/ColorLens"; import ColorLensIcon from "@mui/icons-material/ColorLens";
@@ -180,26 +180,21 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
}) => { }) => {
const map = useMap(); const map = useMap();
const data = useData(); const data = useData();
if (!data) { const currentJunctionCalData = data?.currentJunctionCalData;
return <div>Loading...</div>; // 或其他占位符 const currentPipeCalData = data?.currentPipeCalData;
} const junctionText = data?.junctionText ?? "";
const { const pipeText = data?.pipeText ?? "";
currentJunctionCalData, const setShowJunctionTextLayer = data?.setShowJunctionTextLayer;
currentPipeCalData, const setShowPipeTextLayer = data?.setShowPipeTextLayer;
junctionText, const setShowJunctionId = data?.setShowJunctionId;
pipeText, const setShowPipeId = data?.setShowPipeId;
setShowJunctionTextLayer, const setContourLayerAvailable = data?.setContourLayerAvailable;
setShowPipeTextLayer, const setWaterflowLayerAvailable = data?.setWaterflowLayerAvailable;
setShowJunctionId, const setJunctionText = data?.setJunctionText;
setShowPipeId, const setPipeText = data?.setPipeText;
setContourLayerAvailable, const setContours = data?.setContours;
setWaterflowLayerAvailable, const diameterRange = data?.diameterRange;
setJunctionText, const elevationRange = data?.elevationRange;
setPipeText,
setContours,
diameterRange,
elevationRange,
} = data;
const unitHeadlossRange = [0, 5]; const unitHeadlossRange = [0, 5];
@@ -213,9 +208,6 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
const [renderLayers, setRenderLayers] = useState<WebGLVectorTileLayer[]>([]); const [renderLayers, setRenderLayers] = useState<WebGLVectorTileLayer[]>([]);
const [selectedRenderLayer, setSelectedRenderLayer] = const [selectedRenderLayer, setSelectedRenderLayer] =
useState<WebGLVectorTileLayer>(); useState<WebGLVectorTileLayer>();
const [availableProperties, setAvailableProperties] = useState<
{ name: string; value: string }[]
>([]);
const [styleConfig, setStyleConfig] = useState<StyleConfig>({ const [styleConfig, setStyleConfig] = useState<StyleConfig>({
property: "", property: "",
classificationMethod: "pretty_breaks", classificationMethod: "pretty_breaks",
@@ -237,6 +229,74 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
customColors: [], customColors: [],
}); });
const getDefaultCustomColors = (
segments: number,
existingColors: string[] = []
) => {
const nextColors = [...existingColors];
const baseColors = RAINBOW_PALETTES[0].colors;
while (nextColors.length < segments) {
nextColors.push(baseColors[nextColors.length % baseColors.length]);
}
return nextColors.slice(0, segments);
};
const getDefaultCustomBreaks = (
segments: number,
property: string,
layer: WebGLVectorTileLayer | undefined = selectedRenderLayer
) => {
if (!layer || !property) {
return Array.from({ length: segments }, () => 0);
}
const selectedLayerId = layer.get("value");
let dataArr: number[] = [];
const isElevation =
selectedLayerId === "junctions" && property === "elevation";
const isDiameter = selectedLayerId === "pipes" && property === "diameter";
if (isElevation && elevationRange) {
dataArr = [elevationRange[0], elevationRange[1]];
} else if (isDiameter && diameterRange) {
dataArr = [diameterRange[0], diameterRange[1]];
} else if (selectedLayerId === "junctions" && currentJunctionCalData) {
dataArr = currentJunctionCalData.map((d: any) => d.value);
} else if (selectedLayerId === "pipes" && currentPipeCalData) {
dataArr = currentPipeCalData.map((d: any) => d.value);
}
if (dataArr.length === 0) {
return Array.from({ length: segments }, () => 0);
}
const defaultBreaks = calculateClassification(
dataArr,
segments,
"pretty_breaks"
).slice(0, segments);
while (defaultBreaks.length < segments) {
defaultBreaks.push(defaultBreaks[defaultBreaks.length - 1] ?? 0);
}
return defaultBreaks;
};
const availableProperties = useMemo<{ name: string; value: string }[]>(() => {
if (!selectedRenderLayer) {
return [];
}
return (selectedRenderLayer.get("properties") || []) as {
name: string;
value: string;
}[];
}, [selectedRenderLayer]);
// 根据分段数生成相应数量的渐进颜色 // 根据分段数生成相应数量的渐进颜色
const generateGradientColors = useCallback( const generateGradientColors = useCallback(
(segments: number): string[] => { (segments: number): string[] => {
@@ -278,63 +338,56 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
[styleConfig.rainbowPaletteIndex] [styleConfig.rainbowPaletteIndex]
); );
// 保存当前图层的样式状态 // 保存当前图层的样式状态
const saveLayerStyle = useCallback( const saveLayerStyle = (
( layerId?: string,
layerId?: string, newLegendConfig?: LegendStyleConfig,
newLegendConfig?: LegendStyleConfig, overrideStyleConfig?: StyleConfig
overrideStyleConfig?: StyleConfig ) => {
) => { const currentStyleConfig = overrideStyleConfig || styleConfig;
const currentStyleConfig = overrideStyleConfig || styleConfig;
if (!currentStyleConfig.property) { if (!currentStyleConfig.property) {
console.warn("无法保存样式:缺少必要的图层或样式配置"); console.warn("无法保存样式:缺少必要的图层或样式配置");
return; return;
}
if (!layerId) return;
const layerName =
newLegendConfig?.layerName ||
selectedRenderLayer?.get("name") ||
`图层${layerId}`;
const property = availableProperties.find(
(p) => p.value === currentStyleConfig.property
);
const legendConfig: LegendStyleConfig = newLegendConfig || {
layerId,
layerName,
property: property?.name || currentStyleConfig.property,
colors: [],
type: selectedRenderLayer?.get("type") || "point",
dimensions: [],
breaks: [],
};
const newStyleState: LayerStyleState = {
layerId,
layerName,
styleConfig: { ...currentStyleConfig },
legendConfig: { ...legendConfig },
isActive: true,
};
setLayerStyleStates((prev) => {
const existingIndex = prev.findIndex((state) => state.layerId === layerId);
if (existingIndex !== -1) {
const updated = [...prev];
updated[existingIndex] = newStyleState;
return updated;
} }
if (!layerId) return; // 如果没有传入 layerId,则不保存
// 如果没有传入图例配置,则创建一个默认的空配置
const layerName =
newLegendConfig?.layerName ||
selectedRenderLayer?.get("name") ||
`图层${layerId}`;
const property = availableProperties.find(
(p) => p.value === currentStyleConfig.property
);
let legendConfig: LegendStyleConfig = newLegendConfig || {
layerId,
layerName,
property: property?.name || currentStyleConfig.property,
colors: [],
type: selectedRenderLayer?.get("type") || "point",
dimensions: [],
breaks: [],
};
const newStyleState: LayerStyleState = { return [...prev, newStyleState];
layerId, });
layerName, };
styleConfig: { ...currentStyleConfig },
legendConfig: { ...legendConfig },
isActive: true,
};
setLayerStyleStates((prev) => {
// 检查是否已存在该图层的样式状态
const existingIndex = prev.findIndex(
(state) => state.layerId === layerId
);
if (existingIndex !== -1) {
// 更新已存在的状态
const updated = [...prev];
updated[existingIndex] = newStyleState;
return updated;
} else {
// 添加新的状态
return [...prev, newStyleState];
}
});
},
[selectedRenderLayer, styleConfig, availableProperties]
);
// 设置分类样式参数,触发样式应用 // 设置分类样式参数,触发样式应用
const setStyleState = () => { const setStyleState = () => {
if (!selectedRenderLayer) return; if (!selectedRenderLayer) return;
@@ -787,7 +840,7 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
}; };
// 重置样式 // 重置样式
const resetStyle = useCallback(() => { const resetStyle = () => {
if (!selectedRenderLayer) return; if (!selectedRenderLayer) return;
// 重置 WebGL 图层样式 // 重置 WebGL 图层样式
const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE; const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE;
@@ -815,7 +868,7 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
setWaterflowLayerAvailable && setWaterflowLayerAvailable(false); setWaterflowLayerAvailable && setWaterflowLayerAvailable(false);
} }
} }
}, [selectedRenderLayer]); };
// 更新当前 VectorTileSource 中的所有缓冲要素属性 // 更新当前 VectorTileSource 中的所有缓冲要素属性
const updateVectorTileSource = (property: string, data: any[]) => { const updateVectorTileSource = (property: string, data: any[]) => {
if (!map) return; if (!map) return;
@@ -857,7 +910,7 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
}); });
}; };
// 新增事件,监听 VectorTileSource 的 tileloadend 事件,为新增瓦片数据动态更新要素属性 // 新增事件,监听 VectorTileSource 的 tileloadend 事件,为新增瓦片数据动态更新要素属性
const [tileLoadListeners, setTileLoadListeners] = useState< const tileLoadListenersRef = useRef<
Map<VectorTileSource, (event: any) => void> Map<VectorTileSource, (event: any) => void>
>(new Map()); >(new Map());
@@ -879,8 +932,6 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
dataMap.set(d.ID, d.value || 0); dataMap.set(d.ID, d.value || 0);
}); });
// 新增监听器并保存 // 新增监听器并保存
const newListeners = new Map<VectorTileSource, (event: any) => void>();
const listener = (event: any) => { const listener = (event: any) => {
try { try {
if (event.tile instanceof VectorTile) { if (event.tile instanceof VectorTile) {
@@ -906,8 +957,7 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
}; };
vectorTileSource.on("tileloadend", listener); vectorTileSource.on("tileloadend", listener);
newListeners.set(vectorTileSource, listener); tileLoadListenersRef.current.set(vectorTileSource, listener);
setTileLoadListeners(newListeners);
}; };
// 新增函数:取消对应 layerId 已添加的 on 事件 // 新增函数:取消对应 layerId 已添加的 on 事件
const removeVectorTileSourceLoadedEvent = (layerId: string) => { const removeVectorTileSourceLoadedEvent = (layerId: string) => {
@@ -918,14 +968,10 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
.map((layer) => layer.getSource() as VectorTileSource) .map((layer) => layer.getSource() as VectorTileSource)
.filter((source) => source)[0]; .filter((source) => source)[0];
if (!vectorTileSource) return; if (!vectorTileSource) return;
const listener = tileLoadListeners.get(vectorTileSource); const listener = tileLoadListenersRef.current.get(vectorTileSource);
if (listener) { if (listener) {
vectorTileSource.un("tileloadend", listener); vectorTileSource.un("tileloadend", listener);
setTileLoadListeners((prev) => { tileLoadListenersRef.current.delete(vectorTileSource);
const newMap = new Map(prev);
newMap.delete(vectorTileSource);
return newMap;
});
} }
}; };
@@ -1044,117 +1090,9 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
updateVisibleLayers(); updateVisibleLayers();
}, [map]); }, [map]);
// 获取选中图层的属性,并检查是否有已缓存的样式状态 if (!data) {
useEffect(() => { return <div>Loading...</div>;
// 如果没有矢量图层或没有选中图层,清空属性列表 }
if (!renderLayers || renderLayers.length === 0) {
setAvailableProperties([]);
return;
}
// 如果没有选中图层,清空属性列表
if (!selectedRenderLayer) {
setAvailableProperties([]);
return;
}
// 获取第一个要素的数值型属性
const properties = selectedRenderLayer.get("properties") || {};
setAvailableProperties(properties);
// 设置选中的渲染图层
const renderLayer = renderLayers.filter((layer) => {
return layer.get("value") === selectedRenderLayer?.get("value");
})[0];
setSelectedRenderLayer(renderLayer);
// 检查是否有已缓存的样式状态,如果有则自动恢复
const layerId = selectedRenderLayer.get("value");
const cachedStyleState = layerStyleStates.find(
(state) => state.layerId === layerId
);
if (cachedStyleState) {
setStyleConfig(cachedStyleState.styleConfig);
}
}, [renderLayers, selectedRenderLayer, map, renderLayers, layerStyleStates]);
// 监听颜色类型变化,当切换到单一色时自动勾选宽度调整选项
useEffect(() => {
if (styleConfig.colorType === "single") {
setStyleConfig((prev) => ({
...prev,
adjustWidthByProperty: true,
}));
}
}, [styleConfig.colorType]);
// 初始化或调整自定义断点数组长度,默认使用 pretty_breaks 生成若存在数据
useEffect(() => {
if (styleConfig.classificationMethod !== "custom_breaks") return;
const numBreaks = styleConfig.segments;
setStyleConfig((prev) => {
const prevBreaks = prev.customBreaks || [];
if (prevBreaks.length === numBreaks) return prev;
const selectedLayerId = selectedRenderLayer?.get("value");
let dataArr: number[] = [];
const isElevation =
selectedLayerId === "junctions" && styleConfig.property === "elevation";
const isDiameter =
selectedLayerId === "pipes" && styleConfig.property === "diameter";
if (isElevation && elevationRange) {
dataArr = [elevationRange[0], elevationRange[1]];
} else if (isDiameter && diameterRange) {
dataArr = [diameterRange[0], diameterRange[1]];
} else if (selectedLayerId === "junctions" && currentJunctionCalData) {
dataArr = currentJunctionCalData.map((d: any) => d.value);
} else if (selectedLayerId === "pipes" && currentPipeCalData) {
dataArr = currentPipeCalData.map((d: any) => d.value);
}
let defaultBreaks: number[] = Array.from({ length: numBreaks }, () => 0);
if (dataArr && dataArr.length > 0) {
defaultBreaks = calculateClassification(
dataArr,
styleConfig.segments,
"pretty_breaks"
);
defaultBreaks = defaultBreaks.slice(0, numBreaks);
if (defaultBreaks.length < numBreaks)
while (defaultBreaks.length < numBreaks)
defaultBreaks.push(defaultBreaks[defaultBreaks.length - 1] ?? 0);
}
return { ...prev, customBreaks: defaultBreaks };
});
}, [
styleConfig.classificationMethod,
styleConfig.segments,
styleConfig.property,
selectedRenderLayer,
currentJunctionCalData,
currentPipeCalData,
elevationRange,
diameterRange,
]);
// 初始化或调整自定义颜色数组长度
useEffect(() => {
const numColors = styleConfig.segments;
setStyleConfig((prev) => {
const prevColors = prev.customColors || [];
if (prevColors.length === numColors) return prev;
const newColors = [...prevColors];
const baseColors = RAINBOW_PALETTES[0].colors;
while (newColors.length < numColors) {
newColors.push(baseColors[newColors.length % baseColors.length]);
}
return { ...prev, customColors: newColors.slice(0, numColors) };
});
}, [styleConfig.segments]);
const getColorSetting = () => { const getColorSetting = () => {
if (styleConfig.colorType === "single") { if (styleConfig.colorType === "single") {
@@ -1624,9 +1562,21 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
const cachedStyleState = layerStyleStates.find( const cachedStyleState = layerStyleStates.find(
(state) => state.layerId === layerId (state) => state.layerId === layerId
); );
// 只有在没有缓存时才清空属性 if (cachedStyleState) {
if (!cachedStyleState) { setStyleConfig(cachedStyleState.styleConfig);
setStyleConfig((prev) => ({ ...prev, property: "" })); } else {
setStyleConfig((prev) => ({
...prev,
property: "",
customBreaks:
prev.classificationMethod === "custom_breaks"
? getDefaultCustomBreaks(prev.segments, "", newLayer)
: prev.customBreaks,
customColors: getDefaultCustomColors(
prev.segments,
prev.customColors
),
}));
} }
} }
}} }}
@@ -1647,7 +1597,15 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
<Select <Select
value={styleConfig.property} value={styleConfig.property}
onChange={(e) => { onChange={(e) => {
setStyleConfig((prev) => ({ ...prev, property: e.target.value })); const nextProperty = e.target.value;
setStyleConfig((prev) => ({
...prev,
property: nextProperty,
customBreaks:
prev.classificationMethod === "custom_breaks"
? getDefaultCustomBreaks(prev.segments, nextProperty)
: prev.customBreaks,
}));
}} }}
disabled={!selectedRenderLayer} disabled={!selectedRenderLayer}
> >
@@ -1664,9 +1622,14 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
<Select <Select
value={styleConfig.classificationMethod} value={styleConfig.classificationMethod}
onChange={(e) => { onChange={(e) => {
const nextMethod = e.target.value;
setStyleConfig((prev) => ({ setStyleConfig((prev) => ({
...prev, ...prev,
classificationMethod: e.target.value, classificationMethod: nextMethod,
customBreaks:
nextMethod === "custom_breaks"
? getDefaultCustomBreaks(prev.segments, prev.property)
: prev.customBreaks,
})); }));
}} }}
> >
@@ -1695,7 +1658,14 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
return { return {
...prev, ...prev,
segments: newSegments, segments: newSegments,
customColors: newCustomColors, customBreaks:
prev.classificationMethod === "custom_breaks"
? getDefaultCustomBreaks(newSegments, prev.property)
: prev.customBreaks,
customColors: getDefaultCustomColors(
newSegments,
newCustomColors
),
}; };
}) })
} }
@@ -1782,6 +1752,10 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
return { return {
...prev, ...prev,
colorType: newColorType, colorType: newColorType,
adjustWidthByProperty:
newColorType === "single"
? true
: prev.adjustWidthByProperty,
customColors: newCustomColors, customColors: newCustomColors,
}; };
}); });
+18 -22
View File
@@ -47,29 +47,21 @@ const Timeline: React.FC<TimelineProps> = ({
schemeType = "burst_Analysis", schemeType = "burst_Analysis",
}) => { }) => {
const data = useData(); const data = useData();
if (!data) { const hasTimelineState =
return <div>Loading...</div>; // 或其他占位符 data &&
} data.setCurrentTime !== undefined &&
const { data.currentTime !== undefined &&
currentTime, data.selectedDate !== undefined &&
setCurrentTime, data.setSelectedDate !== undefined;
selectedDate, const currentTime = data?.currentTime ?? -1;
setSelectedDate, const setCurrentTime = data?.setCurrentTime ?? ((_: any) => undefined);
setCurrentJunctionCalData, const selectedDate = data?.selectedDate ?? new Date();
setCurrentPipeCalData, const setSelectedDate = data?.setSelectedDate ?? ((_: any) => undefined);
junctionText, const setCurrentJunctionCalData = data?.setCurrentJunctionCalData;
pipeText, const setCurrentPipeCalData = data?.setCurrentPipeCalData;
} = data; const junctionText = data?.junctionText ?? "";
if ( const pipeText = data?.pipeText ?? "";
setCurrentTime === undefined ||
currentTime === undefined ||
selectedDate === undefined ||
setSelectedDate === undefined
) {
return <div>Loading...</div>; // 或其他占位符
}
const { open } = useNotification(); const { open } = useNotification();
const [isPlaying, setIsPlaying] = useState<boolean>(false); const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒 const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒
const [calculatedInterval, setCalculatedInterval] = useState<number>(15); // 分钟 const [calculatedInterval, setCalculatedInterval] = useState<number>(15); // 分钟
@@ -549,6 +541,10 @@ const Timeline: React.FC<TimelineProps> = ({
} }
}; };
if (!hasTimelineState) {
return <div>Loading...</div>;
}
return ( return (
<Draggable nodeRef={draggableRef} handle=".drag-handle"> <Draggable nodeRef={draggableRef} handle=".drag-handle">
<div <div
@@ -39,8 +39,6 @@ const Toolbar: React.FC<ToolbarProps> = ({
const map = useMap(); const map = useMap();
const data = useData(); const data = useData();
const { open } = useNotification(); const { open } = useNotification();
if (!data) return null;
const { currentTime, selectedDate, schemeName } = data;
const [activeTools, setActiveTools] = useState<string[]>([]); const [activeTools, setActiveTools] = useState<string[]>([]);
const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]); const [highlightFeatures, setHighlightFeatures] = useState<Feature[]>([]);
const [showPropertyPanel, setShowPropertyPanel] = useState<boolean>(false); const [showPropertyPanel, setShowPropertyPanel] = useState<boolean>(false);
@@ -49,6 +47,9 @@ const Toolbar: React.FC<ToolbarProps> = ({
const [showHistoryPanel, setShowHistoryPanel] = useState<boolean>(false); const [showHistoryPanel, setShowHistoryPanel] = useState<boolean>(false);
const [highlightLayer, setHighlightLayer] = const [highlightLayer, setHighlightLayer] =
useState<VectorLayer<VectorSource> | null>(null); useState<VectorLayer<VectorSource> | null>(null);
const currentTime = data?.currentTime;
const selectedDate = data?.selectedDate;
const schemeName = data?.schemeName;
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式 // 样式状态管理 - 在 Toolbar 中管理,带有默认样式
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([ const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([
@@ -721,6 +722,10 @@ const Toolbar: React.FC<ToolbarProps> = ({
return {}; return {};
}, [highlightFeatures, computedProperties]); }, [highlightFeatures, computedProperties]);
if (!data) {
return null;
}
return ( return (
<> <>
<div className="absolute top-4 left-4 bg-white p-1 rounded-xl shadow-lg flex opacity-85 hover:opacity-100 transition-opacity"> <div className="absolute top-4 left-4 bg-white p-1 rounded-xl shadow-lg flex opacity-85 hover:opacity-100 transition-opacity">
+45 -18
View File
@@ -78,15 +78,33 @@ const MapContext = createContext<OlMap | undefined>(undefined);
const DataContext = createContext<DataContextType | undefined>(undefined); const DataContext = createContext<DataContextType | undefined>(undefined);
// 添加防抖函数 // 添加防抖函数
function debounce<F extends (...args: any[]) => any>(func: F, waitFor: number) { type DebouncedFunction<F extends (...args: any[]) => any> = ((
...args: Parameters<F>
) => void) & {
cancel: () => void;
};
function debounce<F extends (...args: any[]) => any>(
func: F,
waitFor: number
): DebouncedFunction<F> {
let timeout: ReturnType<typeof setTimeout> | null = null; let timeout: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<F>): void => { const debounced = (...args: Parameters<F>): void => {
if (timeout !== null) { if (timeout !== null) {
clearTimeout(timeout); clearTimeout(timeout);
} }
timeout = setTimeout(() => func(...args), waitFor); timeout = setTimeout(() => func(...args), waitFor);
}; };
debounced.cancel = () => {
if (timeout !== null) {
clearTimeout(timeout);
timeout = null;
}
};
return debounced;
} }
export const useMap = () => { export const useMap = () => {
@@ -187,20 +205,6 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
[number, number] | undefined [number, number] | undefined
>(); >();
// 防抖更新函数
const debouncedUpdateData = useRef(
debounce(() => {
if (tileJunctionDataBuffer.current.length > 0) {
setJunctionData(tileJunctionDataBuffer.current);
tileJunctionDataBuffer.current = [];
}
if (tilePipeDataBuffer.current.length > 0) {
setPipeData(tilePipeDataBuffer.current);
tilePipeDataBuffer.current = [];
}
}, 100),
);
const setJunctionData = (newData: any[]) => { const setJunctionData = (newData: any[]) => {
const uniqueNewData = newData.filter((item) => { const uniqueNewData = newData.filter((item) => {
if (!item || !item.id) return false; if (!item || !item.id) return false;
@@ -232,6 +236,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
}); });
} }
}; };
const setPipeData = (newData: any[]) => { const setPipeData = (newData: any[]) => {
const uniqueNewData = newData.filter((item) => { const uniqueNewData = newData.filter((item) => {
if (!item || !item.id) return false; if (!item || !item.id) return false;
@@ -263,6 +268,28 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
}); });
} }
}; };
const debouncedUpdateDataRef = useRef<DebouncedFunction<() => void> | null>(
null,
);
useEffect(() => {
debouncedUpdateDataRef.current = debounce(() => {
if (tileJunctionDataBuffer.current.length > 0) {
setJunctionData(tileJunctionDataBuffer.current);
tileJunctionDataBuffer.current = [];
}
if (tilePipeDataBuffer.current.length > 0) {
setPipeData(tilePipeDataBuffer.current);
tilePipeDataBuffer.current = [];
}
}, 100);
return () => {
debouncedUpdateDataRef.current?.cancel();
debouncedUpdateDataRef.current = null;
};
}, []);
// 配置地图数据源、图层和样式 // 配置地图数据源、图层和样式
const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE; const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE;
// 定义 SCADA 图层的样式函数,根据 type 字段选择不同图标 // 定义 SCADA 图层的样式函数,根据 type 字段选择不同图标
@@ -520,7 +547,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
uniqueData.forEach((item) => uniqueData.forEach((item) =>
tileJunctionDataBuffer.current.push(item), tileJunctionDataBuffer.current.push(item),
); );
debouncedUpdateData.current(); debouncedUpdateDataRef.current?.();
} }
} }
} catch (error) { } catch (error) {
@@ -600,7 +627,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const uniqueData = Array.from(data.values()); const uniqueData = Array.from(data.values());
if (uniqueData.length > 0) { if (uniqueData.length > 0) {
uniqueData.forEach((item) => tilePipeDataBuffer.current.push(item)); uniqueData.forEach((item) => tilePipeDataBuffer.current.push(item));
debouncedUpdateData.current(); debouncedUpdateDataRef.current?.();
} }
} }
} catch (error) { } catch (error) {
+8 -14
View File
@@ -29,26 +29,20 @@ type ColorModeContextProviderProps = {
export const ColorModeContextProvider: React.FC< export const ColorModeContextProvider: React.FC<
PropsWithChildren<ColorModeContextProviderProps> PropsWithChildren<ColorModeContextProviderProps>
> = ({ children, defaultMode }) => { > = ({ children, defaultMode }) => {
const [isMounted, setIsMounted] = useState(false);
const [mode, setMode] = useState(defaultMode || "light");
useEffect(() => {
setIsMounted(true);
}, []);
const systemTheme = useMediaQuery(`(prefers-color-scheme: dark)`); const systemTheme = useMediaQuery(`(prefers-color-scheme: dark)`);
const [storedMode, setStoredMode] = useState<string | null>(() => {
useEffect(() => { if (typeof window === "undefined") {
if (isMounted) { return defaultMode ?? null;
const theme = Cookies.get("theme") || (systemTheme ? "dark" : "light");
setMode(theme);
} }
}, [isMounted, systemTheme]);
return Cookies.get("theme") || defaultMode || null;
});
const mode = storedMode || (systemTheme ? "dark" : "light");
const toggleTheme = () => { const toggleTheme = () => {
const nextTheme = mode === "light" ? "dark" : "light"; const nextTheme = mode === "light" ? "dark" : "light";
setMode(nextTheme); setStoredMode(nextTheme);
Cookies.set("theme", nextTheme); Cookies.set("theme", nextTheme);
}; };