@@ -227,11 +229,12 @@ const BaseLayers: React.FC = () => {
className="flex flex-auto flex-col justify-center items-center text-gray-500 text-xs"
onClick={() => handleMapLayers(item.id)}
>
-

= ({
const hasDevices = deviceIds.length > 0;
const hasData = timeSeries.length > 0;
+ const featureInfosKey = useMemo(
+ () => JSON.stringify(featureInfos),
+ [featureInfos]
+ );
const dataset = useMemo(
() => buildDataset(timeSeries, deviceIds, fractionDigits),
@@ -468,7 +472,7 @@ const SCADADataPanel: React.FC
= ({
} else {
setTimeSeries([]);
}
- }, [JSON.stringify(featureInfos)]);
+ }, [featureInfosKey, handleFetch, hasDevices]);
// 当设备数量变化时,调整数据源选择
useEffect(() => {
diff --git a/src/components/olmap/core/Controls/LayerControl.tsx b/src/components/olmap/core/Controls/LayerControl.tsx
index d71fe10..c3dec4c 100644
--- a/src/components/olmap/core/Controls/LayerControl.tsx
+++ b/src/components/olmap/core/Controls/LayerControl.tsx
@@ -15,6 +15,18 @@ interface LayerItem {
layerRef: any; // OpenLayers Layer 实例或 deck.gl layer 对象
}
+const LAYER_ORDER = [
+ "junctions",
+ "reservoirs",
+ "tanks",
+ "pipes",
+ "pumps",
+ "valves",
+ "scada",
+ "waterflowLayer",
+ "junctionContourLayer",
+];
+
const LayerControl: React.FC = () => {
const map = useMap();
const data = useData();
@@ -25,19 +37,9 @@ const LayerControl: React.FC = () => {
const setShowWaterflowLayer = data?.setShowWaterflowLayer;
const setShowContourLayer = data?.setShowContourLayer;
- const layerOrder = [
- "junctions",
- "reservoirs",
- "tanks",
- "pipes",
- "pumps",
- "valves",
- "scada",
- "waterflowLayer",
- "junctionContourLayer",
- ];
-
const layerItems = useMemo(() => {
+ void refreshKey;
+
if (!map || !data) return [];
const items: LayerItem[] = [];
@@ -87,8 +89,8 @@ const LayerControl: React.FC = () => {
}
return items
- .filter((item) => layerOrder.includes(item.id))
- .sort((a, b) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b.id));
+ .filter((item) => LAYER_ORDER.includes(item.id))
+ .sort((a, b) => LAYER_ORDER.indexOf(a.id) - LAYER_ORDER.indexOf(b.id));
}, [
map,
data,
diff --git a/src/components/olmap/core/Controls/StyleEditorPanel.tsx b/src/components/olmap/core/Controls/StyleEditorPanel.tsx
index a3ad909..ebafaba 100644
--- a/src/components/olmap/core/Controls/StyleEditorPanel.tsx
+++ b/src/components/olmap/core/Controls/StyleEditorPanel.tsx
@@ -321,7 +321,7 @@ const StyleEditorPanel: React.FC = ({
}
return colors;
},
- [styleConfig.gradientPaletteIndex, parseColor]
+ [styleConfig.gradientPaletteIndex]
);
// 根据分段数生成彩虹色
@@ -1065,6 +1065,8 @@ const StyleEditorPanel: React.FC = ({
if (!applyPipeStyle) {
removeVectorTileSourceLoadedEvent("pipes");
}
+ // This effect is intentionally driven by explicit style triggers and data snapshots.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [
styleUpdateTrigger,
applyJunctionStyle,
diff --git a/src/components/olmap/core/Controls/Timeline.tsx b/src/components/olmap/core/Controls/Timeline.tsx
index eefdeed..f883634 100644
--- a/src/components/olmap/core/Controls/Timeline.tsx
+++ b/src/components/olmap/core/Controls/Timeline.tsx
@@ -39,6 +39,9 @@ interface TimelineProps {
schemeType?: string;
}
+const NOOP_SET_CURRENT_TIME = (_: any) => undefined;
+const NOOP_SET_SELECTED_DATE = (_: any) => undefined;
+
const Timeline: React.FC = ({
schemeDate,
timeRange,
@@ -47,6 +50,7 @@ const Timeline: React.FC = ({
schemeType = "burst_Analysis",
}) => {
const data = useData();
+ const fallbackSelectedDateRef = useRef(new Date());
const hasTimelineState =
data &&
data.setCurrentTime !== undefined &&
@@ -54,9 +58,9 @@ const Timeline: React.FC = ({
data.selectedDate !== undefined &&
data.setSelectedDate !== undefined;
const currentTime = data?.currentTime ?? -1;
- const setCurrentTime = data?.setCurrentTime ?? ((_: any) => undefined);
- const selectedDate = data?.selectedDate ?? new Date();
- const setSelectedDate = data?.setSelectedDate ?? ((_: any) => undefined);
+ const setCurrentTime = data?.setCurrentTime ?? NOOP_SET_CURRENT_TIME;
+ const selectedDate = data?.selectedDate ?? fallbackSelectedDateRef.current;
+ const setSelectedDate = data?.setSelectedDate ?? NOOP_SET_SELECTED_DATE;
const setCurrentJunctionCalData = data?.setCurrentJunctionCalData;
const setCurrentPipeCalData = data?.setCurrentPipeCalData;
const junctionText = data?.junctionText ?? "";
@@ -78,7 +82,7 @@ const Timeline: React.FC = ({
if (schemeDate) {
setSelectedDate(schemeDate);
}
- }, [schemeDate]);
+ }, [schemeDate, setSelectedDate]);
// 新增:用于 Draggable 的 nodeRef
const draggableRef = useRef(null);
@@ -90,7 +94,20 @@ const Timeline: React.FC = ({
// 添加防抖引用
const debounceRef = useRef(null);
- const fetchFrameData = async (
+ const updateDataStates = useCallback((nodeResults: any[], linkResults: any[]) => {
+ if (setCurrentJunctionCalData) {
+ setCurrentJunctionCalData(nodeResults);
+ } else {
+ console.log("setCurrentJunctionCalData is undefined");
+ }
+ if (setCurrentPipeCalData) {
+ setCurrentPipeCalData(linkResults);
+ } else {
+ console.log("setCurrentPipeCalData is undefined");
+ }
+ }, [setCurrentJunctionCalData, setCurrentPipeCalData]);
+
+ const fetchFrameData = useCallback(async (
queryTime: Date,
junctionProperties: string,
pipeProperties: string,
@@ -170,21 +187,7 @@ const Timeline: React.FC = ({
}
// 更新状态
updateDataStates(nodeRecords.results || [], linkRecords.results || []);
- };
-
- // 提取更新状态的逻辑
- const updateDataStates = (nodeResults: any[], linkResults: any[]) => {
- if (setCurrentJunctionCalData) {
- setCurrentJunctionCalData(nodeResults);
- } else {
- console.log("setCurrentJunctionCalData is undefined");
- }
- if (setCurrentPipeCalData) {
- setCurrentPipeCalData(linkResults);
- } else {
- console.log("setCurrentPipeCalData is undefined");
- }
- };
+ }, [disableDateSelection, updateDataStates]);
// 时间刻度数组 (每5分钟一个刻度)
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
@@ -241,7 +244,7 @@ const Timeline: React.FC = ({
setCurrentTime(value);
}, 500); // 500ms 防抖延迟
},
- [timeRange, minTime, maxTime],
+ [timeRange, minTime, maxTime, setCurrentTime],
);
// 播放控制
@@ -268,7 +271,7 @@ const Timeline: React.FC = ({
});
}, playInterval);
}
- }, [isPlaying, playInterval]);
+ }, [isPlaying, playInterval, timeRange, maxTime, minTime, setCurrentTime]);
const handlePause = useCallback(() => {
setIsPlaying(false);
@@ -288,7 +291,7 @@ const Timeline: React.FC = ({
clearInterval(intervalRef.current);
intervalRef.current = null;
}
- }, []);
+ }, [setCurrentTime]);
// 步进控制
const handleDayStepBackward = useCallback(() => {
@@ -297,14 +300,14 @@ const Timeline: React.FC = ({
newDate.setDate(newDate.getDate() - 1);
return newDate;
});
- }, []);
+ }, [setSelectedDate]);
const handleDayStepForward = useCallback(() => {
setSelectedDate((prev) => {
const newDate = new Date(prev);
newDate.setDate(newDate.getDate() + 1);
return newDate;
});
- }, []);
+ }, [setSelectedDate]);
const handleStepBackward = useCallback(() => {
setCurrentTime((prev) => {
let next = prev - 15;
@@ -315,7 +318,7 @@ const Timeline: React.FC = ({
}
return next;
});
- }, [timeRange, minTime, maxTime]);
+ }, [timeRange, minTime, maxTime, setCurrentTime]);
const handleStepForward = useCallback(() => {
setCurrentTime((prev) => {
@@ -327,14 +330,14 @@ const Timeline: React.FC = ({
}
return next;
});
- }, [timeRange, minTime, maxTime]);
+ }, [timeRange, minTime, maxTime, setCurrentTime]);
// 日期选择处理
const handleDateChange = useCallback((newDate: Date | null) => {
if (newDate) {
setSelectedDate(newDate);
}
- }, []);
+ }, [setSelectedDate]);
// 播放间隔改变处理
const handleIntervalChange = useCallback(
@@ -358,7 +361,7 @@ const Timeline: React.FC = ({
}, newInterval);
}
},
- [isPlaying],
+ [isPlaying, timeRange, maxTime, minTime, setCurrentTime],
);
// 计算时间段改变处理
const handleCalculatedIntervalChange = useCallback((event: any) => {
@@ -389,6 +392,7 @@ const Timeline: React.FC = ({
);
}
}, [
+ fetchFrameData,
junctionText,
pipeText,
currentTime,
@@ -414,14 +418,14 @@ const Timeline: React.FC = ({
clearTimeout(debounceRef.current);
}
};
- }, []);
+ }, [setCurrentTime]);
// 当 timeRange 改变时,设置 currentTime 到 minTime
useEffect(() => {
if (timeRange) {
setCurrentTime(minTime);
}
- }, [timeRange, minTime]);
+ }, [timeRange, minTime, setCurrentTime]);
// 获取地图实例
const map = useMap();
// 这里防止地图缩放时,瓦片重新加载引起的属性更新出错
diff --git a/src/components/olmap/core/Controls/Toolbar.tsx b/src/components/olmap/core/Controls/Toolbar.tsx
index c5f476c..0c7345c 100644
--- a/src/components/olmap/core/Controls/Toolbar.tsx
+++ b/src/components/olmap/core/Controls/Toolbar.tsx
@@ -409,8 +409,7 @@ const Toolbar: React.FC = ({
setComputedProperties({});
} else {
setComputedProperties(data.result[0] || {});
- console.log("查询到的计算属性:", data.result[0]);
- console.log(computedProperties);
+ // console.log("查询到的计算属性:", data.result[0]);
}
} catch (error) {
console.error("Error querying computed properties:", error);
@@ -419,7 +418,7 @@ const Toolbar: React.FC = ({
};
// 仅当 currentTime 有效时查询
if (currentTime !== -1 && queryType) queryComputedProperties();
- }, [highlightFeatures, currentTime, selectedDate, queryType, schemeName, schemeType]);
+ }, [highlightFeatures, currentTime, selectedDate, queryType, schemeName, schemeType, showPropertyPanel]);
// 从要素属性中提取属性面板需要的数据
const getFeatureProperties = useCallback(() => {
diff --git a/src/components/olmap/core/MapComponent.tsx b/src/components/olmap/core/MapComponent.tsx
index e142b47..0374982 100644
--- a/src/components/olmap/core/MapComponent.tsx
+++ b/src/components/olmap/core/MapComponent.tsx
@@ -515,6 +515,7 @@ const MapComponent: React.FC = ({ children }) => {
},
});
+ // The map and layer instances are intentionally rebuilt only when workspace or extent changes.
useEffect(() => {
if (!mapRef.current) return;
// 缓存 junction、pipe 数据,提供给 deck.gl 提供坐标供标签显示
@@ -807,6 +808,7 @@ const MapComponent: React.FC = ({ children }) => {
map.dispose();
deck.finalize();
};
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [MAP_WORKSPACE, MAP_EXTENT]);
// 当数据变化时,更新 deck.gl 图层
diff --git a/src/contexts/ProjectContext.tsx b/src/contexts/ProjectContext.tsx
index a128613..98d69cc 100644
--- a/src/contexts/ProjectContext.tsx
+++ b/src/contexts/ProjectContext.tsx
@@ -1,5 +1,5 @@
"use client";
-import React, { createContext, useContext, useEffect, useState } from "react";
+import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { config, NETWORK_NAME, setMapWorkspace, setNetworkName, setMapExtent } from "@/config/config";
import { ProjectSelector } from "@/components/project/ProjectSelector";
@@ -28,25 +28,7 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({
extent: config.MAP_EXTENT,
});
- useEffect(() => {
- // Check localStorage
- const savedWorkspace = localStorage.getItem("NEXT_PUBLIC_MAP_WORKSPACE");
- const savedNetwork = localStorage.getItem("NEXT_PUBLIC_NETWORK_NAME");
- const savedExtent = localStorage.getItem("NEXT_PUBLIC_MAP_EXTENT");
- const savedProjectId = localStorage.getItem("active_project");
-
- // If we have saved config, use it.
- if (savedWorkspace && savedNetwork) {
- applyConfig(
- savedProjectId || savedNetwork || savedWorkspace,
- savedWorkspace,
- savedNetwork,
- savedExtent ? savedExtent.split(",").map(Number) : config.MAP_EXTENT,
- );
- }
- }, []);
-
- const applyConfig = async (
+ const applyConfig = useCallback(async (
projectId: string,
ws: string,
net: string,
@@ -91,7 +73,25 @@ export const ProjectProvider: React.FC<{ children: React.ReactNode }> = ({
} catch (error) {
console.error("Failed to open project:", error);
}
- };
+ }, [setCurrentProjectId]);
+
+ useEffect(() => {
+ // Check localStorage
+ const savedWorkspace = localStorage.getItem("NEXT_PUBLIC_MAP_WORKSPACE");
+ const savedNetwork = localStorage.getItem("NEXT_PUBLIC_NETWORK_NAME");
+ const savedExtent = localStorage.getItem("NEXT_PUBLIC_MAP_EXTENT");
+ const savedProjectId = localStorage.getItem("active_project");
+
+ // If we have saved config, use it.
+ if (savedWorkspace && savedNetwork) {
+ applyConfig(
+ savedProjectId || savedNetwork || savedWorkspace,
+ savedWorkspace,
+ savedNetwork,
+ savedExtent ? savedExtent.split(",").map(Number) : config.MAP_EXTENT,
+ );
+ }
+ }, [applyConfig]);
// Only show selector if authenticated and not configured
if (status === "authenticated" && !isConfigured) {