fix(timeline): use backend timestep

This commit is contained in:
2026-07-16 14:16:07 +08:00
parent d90ca7c951
commit dfaee645ff
7 changed files with 370 additions and 82 deletions
@@ -30,6 +30,7 @@ import { FiSkipBack, FiSkipForward } from "react-icons/fi";
import { config, NETWORK_NAME } from "@/config/config";
import { apiFetch } from "@/lib/apiFetch";
import { useMap } from "@components/olmap/core/MapComponent";
import { useTimelineTimeConfig } from "@components/olmap/core/Controls/useTimelineTimeConfig";
import { useHealthRisk } from "./HealthRiskContext";
import {
PredictionResult,
@@ -38,10 +39,11 @@ import {
RISK_BREAKS,
} from "./types";
// 辅助函数:将日期向下取整到最近的15分钟
const getRoundedDate = (date: Date): Date => {
// 辅助函数:将日期向下取整到配置的水力时间步长
const getRoundedDate = (date: Date, stepMinutes: number): Date => {
const safeStep = stepMinutes > 0 ? stepMinutes : 15;
const minutes = date.getHours() * 60 + date.getMinutes();
const roundedMinutes = Math.floor(minutes / 15) * 15;
const roundedMinutes = Math.floor(minutes / safeStep) * safeStep;
const roundedDate = new Date(date);
roundedDate.setHours(
Math.floor(roundedMinutes / 60),
@@ -91,6 +93,7 @@ const Timeline: React.FC<TimelineProps> = ({
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒
const [isPredicting, setIsPredicting] = useState<boolean>(false);
const { stepMinutes } = useTimelineTimeConfig();
// 使用 ref 存储当前的健康数据,供事件监听器读取,避免重复绑定
const healthDataRef = useRef<Map<string, number>>(new Map());
@@ -200,11 +203,14 @@ const Timeline: React.FC<TimelineProps> = ({
}, [minTime, maxTime, setCurrentYear]);
// 日期时间选择处理
const handleDateTimeChange = useCallback((newDate: Date | null) => {
if (newDate) {
setSelectedDateTime(getRoundedDate(newDate));
}
}, []);
const handleDateTimeChange = useCallback(
(newDate: Date | null) => {
if (newDate) {
setSelectedDateTime(getRoundedDate(newDate, stepMinutes));
}
},
[stepMinutes],
);
// 播放间隔改变处理
const handleIntervalChange = useCallback(
@@ -227,9 +233,9 @@ const Timeline: React.FC<TimelineProps> = ({
[isPlaying, maxTime, minTime, setCurrentYear],
);
// 组件加载时设置初始时间为当前时间的最近15分钟
// 组件加载时设置初始时间为当前时间的配置时间步长
useEffect(() => {
setSelectedDateTime(getRoundedDate(new Date()));
setSelectedDateTime(getRoundedDate(new Date(), stepMinutes));
return () => {
if (intervalRef.current) {
@@ -239,7 +245,7 @@ const Timeline: React.FC<TimelineProps> = ({
clearTimeout(debounceRef.current);
}
};
}, []);
}, [stepMinutes]);
// 获取地图实例
const map = useMap();
@@ -513,7 +519,7 @@ const Timeline: React.FC<TimelineProps> = ({
}
format="YYYY-MM-DD HH:mm"
views={["year", "month", "day", "hours", "minutes"]}
minutesStep={15}
minutesStep={stepMinutes}
sx={{ width: 200 }}
slotProps={{
textField: {
+94 -61
View File
@@ -1,6 +1,6 @@
"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 Draggable from "react-draggable";
@@ -36,6 +36,7 @@ import {
getTimelineDisabledRangePercentages,
normalizeTimelineMinutes,
} from "./timelineTime";
import { useTimelineTimeConfig } from "./useTimelineTimeConfig";
interface TimelineProps {
schemeDate?: Date;
@@ -64,6 +65,13 @@ const timelineIconButtonSx = {
const NOOP_SET_CURRENT_TIME = (_: any) => undefined;
const NOOP_SET_SELECTED_DATE = (_: any) => undefined;
const BASE_CALCULATED_INTERVAL_OPTIONS = [
{ value: 1440, label: "1 天" },
{ value: 60, label: "1 小时" },
{ value: 30, label: "30 分钟" },
{ value: 15, label: "15 分钟" },
{ value: 5, label: "5 分钟" },
];
const Timeline: React.FC<TimelineProps> = ({
schemeDate,
@@ -93,18 +101,28 @@ const Timeline: React.FC<TimelineProps> = ({
const pipeText = data?.pipeText ?? "";
const setForceStyleAutoApplyVersion = data?.setForceStyleAutoApplyVersion;
const { open } = useNotification();
const { durationMinutes, stepMinutes } = useTimelineTimeConfig();
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒
const [calculatedInterval, setCalculatedInterval] = useState<number>(15); // 分钟
const [calculatedInterval, setCalculatedInterval] =
useState<number>(stepMinutes); // 分钟
const [isCalculating, setIsCalculating] = useState<boolean>(false);
// 计算时间轴范围
const minTime = timeRange
? timeRange.start.getHours() * 60 + timeRange.start.getMinutes()
? normalizeTimelineMinutes(
timeRange.start.getHours() * 60 + timeRange.start.getMinutes(),
0,
durationMinutes,
)
: 0;
const maxTime = timeRange
? timeRange.end.getHours() * 60 + timeRange.end.getMinutes()
: 1440;
? normalizeTimelineMinutes(
timeRange.end.getHours() * 60 + timeRange.end.getMinutes(),
0,
durationMinutes,
)
: durationMinutes;
const timelineCurrentTime = normalizeTimelineMinutes(
currentTime,
minTime,
@@ -113,6 +131,7 @@ const Timeline: React.FC<TimelineProps> = ({
const disabledRangePercentages = getTimelineDisabledRangePercentages(
minTime,
maxTime,
durationMinutes,
);
useEffect(() => {
if (schemeDate) {
@@ -334,16 +353,28 @@ const Timeline: React.FC<TimelineProps> = ({
[disableDateSelection, fetchDataBySource, isCompareMode]
);
// 时间刻度数组 (每5分钟一个刻度)
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
value: i * 5,
label: i % 24 === 0 ? formatTime(i * 5) : "",
}));
// 格式化时间显示
function formatTime(minutes: number): string {
const formatTime = useCallback((minutes: number): string => {
return formatTimelineTime(minutes, minTime, maxTime);
}
}, [maxTime, minTime]);
const timeMarks = useMemo(() => {
const marks = [];
const markInterval = 120;
for (let value = 0; value <= durationMinutes; value += markInterval) {
marks.push({
value,
label: formatTime(value),
});
}
if (marks.at(-1)?.value !== durationMinutes) {
marks.push({
value: durationMinutes,
label: formatTime(durationMinutes),
});
}
return marks;
}, [durationMinutes, formatTime]);
const currentTimeToDate = useCallback((selectedDate: Date, minutes: number): Date => {
const date = new Date(selectedDate);
@@ -362,13 +393,31 @@ const Timeline: React.FC<TimelineProps> = ({
{ value: 20000, label: "20秒" },
];
// 强制计算时间段选项
const calculatedIntervalOptions = [
{ value: 1440, label: "1 天" },
{ value: 60, label: "1 小时" },
{ value: 30, label: "30 分钟" },
{ value: 15, label: "15 分钟" },
{ value: 5, label: "5 分钟" },
];
const resolvedCalculatedIntervalOptions = useMemo(() => {
const options = BASE_CALCULATED_INTERVAL_OPTIONS.filter(
(option) => option.value >= stepMinutes,
);
if (!options.some((option) => option.value === stepMinutes)) {
options.push({ value: stepMinutes, label: `${stepMinutes} 分钟` });
}
return options.sort((a, b) => b.value - a.value);
}, [stepMinutes]);
const advanceTimelineTime = useCallback(
(previousTime: number, direction: 1 | -1 = 1) => {
const baseTime = Number.isFinite(previousTime) ? previousTime : minTime;
let next = baseTime + stepMinutes * direction;
if (timeRange) {
if (next > maxTime) next = minTime;
if (next < minTime) next = maxTime;
} else {
if (next > durationMinutes) next = 0;
if (next < 0) next = durationMinutes;
}
return next;
},
[durationMinutes, maxTime, minTime, stepMinutes, timeRange],
);
// 处理时间轴滑动
const handleSliderChange = useCallback(
@@ -403,17 +452,11 @@ const Timeline: React.FC<TimelineProps> = ({
intervalRef.current = setInterval(() => {
setCurrentTime((prev) => {
let next = prev + 15;
if (timeRange) {
if (next > maxTime) next = minTime;
} else {
if (next >= 1440) next = 0;
}
return next;
return advanceTimelineTime(prev);
});
}, playInterval);
}
}, [isPlaying, playInterval, timeRange, maxTime, minTime, setCurrentTime]);
}, [advanceTimelineTime, isPlaying, playInterval, setCurrentTime]);
const handlePause = useCallback(() => {
setIsPlaying(false);
@@ -426,12 +469,14 @@ const Timeline: React.FC<TimelineProps> = ({
const handleStop = useCallback(() => {
setIsPlaying(false);
// 设置为当前时间
setCurrentTime(getRoundedCurrentTimelineMinutes()); // 重置为当前时间
setCurrentTime(
getRoundedCurrentTimelineMinutes(new Date(), stepMinutes, durationMinutes),
); // 重置为当前时间
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [setCurrentTime]);
}, [durationMinutes, setCurrentTime, stepMinutes]);
// 步进控制
const handleDayStepBackward = useCallback(() => {
@@ -450,27 +495,15 @@ const Timeline: React.FC<TimelineProps> = ({
}, [setSelectedDate]);
const handleStepBackward = useCallback(() => {
setCurrentTime((prev) => {
let next = prev - 15;
if (timeRange) {
if (next < minTime) next = maxTime;
} else {
if (next < 0) next += 1440;
}
return next;
return advanceTimelineTime(prev, -1);
});
}, [timeRange, minTime, maxTime, setCurrentTime]);
}, [advanceTimelineTime, setCurrentTime]);
const handleStepForward = useCallback(() => {
setCurrentTime((prev) => {
let next = prev + 15;
if (timeRange) {
if (next > maxTime) next = minTime;
} else {
if (next >= 1440) next = 0;
}
return next;
return advanceTimelineTime(prev);
});
}, [timeRange, minTime, maxTime, setCurrentTime]);
}, [advanceTimelineTime, setCurrentTime]);
// 日期选择处理
const handleDateChange = useCallback((newDate: Date | null) => {
@@ -490,18 +523,12 @@ const Timeline: React.FC<TimelineProps> = ({
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setCurrentTime((prev) => {
let next = prev + 15;
if (timeRange) {
if (next > maxTime) next = minTime;
} else {
if (next >= 1440) next = 0;
}
return next;
return advanceTimelineTime(prev);
});
}, newInterval);
}
},
[isPlaying, timeRange, maxTime, minTime, setCurrentTime],
[advanceTimelineTime, isPlaying, setCurrentTime],
);
// 计算时间段改变处理
const handleCalculatedIntervalChange = useCallback((event: any) => {
@@ -509,6 +536,10 @@ const Timeline: React.FC<TimelineProps> = ({
setCalculatedInterval(newInterval);
}, []);
useEffect(() => {
setCalculatedInterval(stepMinutes);
}, [stepMinutes]);
// 添加 useEffect 来监听 currentTime 和 selectedDate 的变化,并获取数据
useEffect(() => {
// 首次加载时,如果 selectedDate 或 currentTime 未定义,则跳过执行,避免报错
@@ -546,7 +577,9 @@ const Timeline: React.FC<TimelineProps> = ({
// 组件卸载时清理定时器和防抖
useEffect(() => {
// 设置为当前时间
setCurrentTime(getRoundedCurrentTimelineMinutes()); // 初始化为当前时间
setCurrentTime(
getRoundedCurrentTimelineMinutes(new Date(), stepMinutes, durationMinutes),
); // 初始化为当前时间
return () => {
if (intervalRef.current) {
@@ -556,7 +589,7 @@ const Timeline: React.FC<TimelineProps> = ({
clearTimeout(debounceRef.current);
}
};
}, [setCurrentTime]);
}, [durationMinutes, setCurrentTime, stepMinutes]);
// 当 timeRange 改变时,设置 currentTime 到 minTime
useEffect(() => {
@@ -868,7 +901,7 @@ const Timeline: React.FC<TimelineProps> = ({
label="强制计算时间段"
onChange={handleCalculatedIntervalChange}
>
{calculatedIntervalOptions.map((option) => (
{resolvedCalculatedIntervalOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
@@ -906,9 +939,9 @@ const Timeline: React.FC<TimelineProps> = ({
<Slider
value={timelineCurrentTime}
min={0}
max={1440} // 24:00 = 1440分钟
step={15} // 每15分钟一个步进
marks={timeMarks.filter((_, index) => index % 12 === 0)} // 每小时显示一个标记
max={durationMinutes}
step={stepMinutes}
marks={timeMarks}
onChange={handleSliderChange}
valueLabelDisplay="auto"
valueLabelFormat={formatTime}
@@ -976,7 +1009,7 @@ const Timeline: React.FC<TimelineProps> = ({
/>
)}
{/* 右侧禁用区域 */}
{maxTime < 1440 && (
{maxTime < durationMinutes && (
<Box
sx={{
position: "absolute",
@@ -1,8 +1,10 @@
import {
coerceTimelineDurationMinutes,
formatTimelineTime,
getRoundedCurrentTimelineMinutes,
getTimelineDisabledRangePercentages,
normalizeTimelineMinutes,
parseTimelineDurationMinutes,
} from "./timelineTime";
describe("timelineTime", () => {
@@ -20,6 +22,28 @@ describe("timelineTime", () => {
expect(getRoundedCurrentTimelineMinutes(new Date("2026-07-16T10:29:00"))).toBe(615);
});
it("rounds the current time down to a custom timeline step", () => {
expect(
getRoundedCurrentTimelineMinutes(
new Date("2026-07-16T10:29:00"),
60,
),
).toBe(600);
});
it("parses EPANET-style duration values as minutes", () => {
expect(parseTimelineDurationMinutes("0:05")).toBe(5);
expect(parseTimelineDurationMinutes("1:00")).toBe(60);
expect(parseTimelineDurationMinutes("24:00")).toBe(1440);
expect(parseTimelineDurationMinutes("0:05:00")).toBe(5);
});
it("falls back for invalid, missing, and zero duration values", () => {
expect(coerceTimelineDurationMinutes("invalid", 1440)).toBe(1440);
expect(coerceTimelineDurationMinutes(undefined, 1440)).toBe(1440);
expect(coerceTimelineDurationMinutes("0:00", 1440)).toBe(1440);
});
it("calculates disabled range as full-day percentages", () => {
expect(getTimelineDisabledRangePercentages(360, 1080)).toEqual({
leftWidth: 25,
@@ -1,6 +1,41 @@
export const TIMELINE_MINUTES_PER_DAY = 1440;
export const TIMELINE_STEP_MINUTES = 15;
export const parseTimelineDurationMinutes = (
value: unknown,
): number | undefined => {
if (typeof value !== "string") {
return undefined;
}
const parts = value
.trim()
.split(":")
.map((part) => Number(part));
if (
(parts.length !== 2 && parts.length !== 3) ||
parts.some((part) => !Number.isFinite(part) || part < 0)
) {
return undefined;
}
const [hours, minutes, seconds = 0] = parts;
if (minutes >= 60 || seconds >= 60) {
return undefined;
}
return hours * 60 + minutes + Math.floor(seconds / 60);
};
export const coerceTimelineDurationMinutes = (
value: unknown,
fallbackMinutes: number,
) => {
const minutes = parseTimelineDurationMinutes(value);
return minutes && minutes > 0 ? minutes : fallbackMinutes;
};
export const normalizeTimelineMinutes = (
minutes: number | undefined,
minTime = 0,
@@ -13,9 +48,18 @@ export const normalizeTimelineMinutes = (
return Math.min(Math.max(minutes, minTime), maxTime);
};
export const getRoundedCurrentTimelineMinutes = (date = new Date()) => {
export const getRoundedCurrentTimelineMinutes = (
date = new Date(),
stepMinutes = TIMELINE_STEP_MINUTES,
maxMinutes = TIMELINE_MINUTES_PER_DAY,
) => {
const safeStep = stepMinutes > 0 ? stepMinutes : TIMELINE_STEP_MINUTES;
const minutes = date.getHours() * 60 + date.getMinutes();
return Math.floor(minutes / TIMELINE_STEP_MINUTES) * TIMELINE_STEP_MINUTES;
return normalizeTimelineMinutes(
Math.floor(minutes / safeStep) * safeStep,
0,
maxMinutes,
);
};
export const formatTimelineTime = (
@@ -34,14 +78,17 @@ export const formatTimelineTime = (
export const getTimelineDisabledRangePercentages = (
minTime: number,
maxTime: number,
totalMinutes = TIMELINE_MINUTES_PER_DAY,
) => {
const rangeStart = normalizeTimelineMinutes(minTime);
const rangeEnd = normalizeTimelineMinutes(maxTime);
const safeTotalMinutes =
totalMinutes > 0 ? totalMinutes : TIMELINE_MINUTES_PER_DAY;
const rangeStart = normalizeTimelineMinutes(minTime, 0, safeTotalMinutes);
const rangeEnd = normalizeTimelineMinutes(maxTime, 0, safeTotalMinutes);
return {
leftWidth: (rangeStart / TIMELINE_MINUTES_PER_DAY) * 100,
rightLeft: (rangeEnd / TIMELINE_MINUTES_PER_DAY) * 100,
leftWidth: (rangeStart / safeTotalMinutes) * 100,
rightLeft: (rangeEnd / safeTotalMinutes) * 100,
rightWidth:
((TIMELINE_MINUTES_PER_DAY - rangeEnd) / TIMELINE_MINUTES_PER_DAY) * 100,
((safeTotalMinutes - rangeEnd) / safeTotalMinutes) * 100,
};
};
@@ -0,0 +1,69 @@
import { renderHook, waitFor } from "@testing-library/react";
import { useTimelineTimeConfig, toTimelineTimeConfig } from "./useTimelineTimeConfig";
const apiFetch = jest.fn();
jest.mock("@/lib/apiFetch", () => ({
apiFetch: (...args: unknown[]) => apiFetch(...args),
}));
jest.mock("@/contexts/ProjectContext", () => ({
useProject: () => ({ networkName: "test-network" }),
}));
describe("useTimelineTimeConfig", () => {
beforeEach(() => {
apiFetch.mockReset();
});
it("derives duration and step from backend time properties", async () => {
apiFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
DURATION: "24:00",
"HYDRAULIC TIMESTEP": "1:00",
}),
});
const { result } = renderHook(() => useTimelineTimeConfig());
await waitFor(() => {
expect(result.current.stepMinutes).toBe(60);
});
expect(result.current.durationMinutes).toBe(1440);
expect(String(apiFetch.mock.calls[0][0])).toContain(
"/api/v1/gettimeproperties/?network=test-network",
);
});
it("falls back when fetching time properties fails", async () => {
apiFetch.mockRejectedValueOnce(new Error("network failure"));
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
const { result } = renderHook(() => useTimelineTimeConfig());
await waitFor(() => {
expect(apiFetch).toHaveBeenCalledTimes(1);
});
expect(result.current).toMatchObject({
durationMinutes: 1440,
stepMinutes: 15,
});
warnSpy.mockRestore();
});
it("parses wrapped times payloads", () => {
expect(
toTimelineTimeConfig({
times: {
DURATION: "24:00",
"HYDRAULIC TIMESTEP": "0:05",
},
}),
).toMatchObject({
durationMinutes: 1440,
stepMinutes: 5,
});
});
});
@@ -0,0 +1,105 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { config, NETWORK_NAME } from "@/config/config";
import { useProject } from "@/contexts/ProjectContext";
import { apiFetch } from "@/lib/apiFetch";
import {
coerceTimelineDurationMinutes,
TIMELINE_MINUTES_PER_DAY,
TIMELINE_STEP_MINUTES,
} from "./timelineTime";
export interface TimelineTimeConfig {
durationMinutes: number;
stepMinutes: number;
properties: Record<string, unknown>;
}
const DEFAULT_TIMELINE_TIME_CONFIG: TimelineTimeConfig = {
durationMinutes: TIMELINE_MINUTES_PER_DAY,
stepMinutes: TIMELINE_STEP_MINUTES,
properties: {},
};
const resolveTimeProperties = (data: unknown): Record<string, unknown> => {
if (!data || typeof data !== "object") {
return {};
}
const record = data as Record<string, unknown>;
if (record.times && typeof record.times === "object") {
return record.times as Record<string, unknown>;
}
return record;
};
export const toTimelineTimeConfig = (data: unknown): TimelineTimeConfig => {
const properties = resolveTimeProperties(data);
return {
durationMinutes: coerceTimelineDurationMinutes(
properties.DURATION,
TIMELINE_MINUTES_PER_DAY,
),
stepMinutes: coerceTimelineDurationMinutes(
properties["HYDRAULIC TIMESTEP"],
TIMELINE_STEP_MINUTES,
),
properties,
};
};
export const useTimelineTimeConfig = (): TimelineTimeConfig => {
const project = useProject();
const networkName = project?.networkName || NETWORK_NAME;
const [timeConfig, setTimeConfig] = useState<TimelineTimeConfig>(
DEFAULT_TIMELINE_TIME_CONFIG,
);
useEffect(() => {
if (!networkName) {
setTimeConfig(DEFAULT_TIMELINE_TIME_CONFIG);
return;
}
let isMounted = true;
const fetchTimeProperties = async () => {
try {
const response = await apiFetch(
`${config.BACKEND_URL}/api/v1/gettimeproperties/?network=${encodeURIComponent(
networkName,
)}`,
);
if (!response.ok) {
throw new Error(`Failed to fetch time properties: ${response.status}`);
}
const data = await response.json();
if (isMounted) {
setTimeConfig(toTimelineTimeConfig(data));
}
} catch (error) {
console.warn("Failed to load timeline time properties:", error);
if (isMounted) {
setTimeConfig(DEFAULT_TIMELINE_TIME_CONFIG);
}
}
};
fetchTimeProperties();
return () => {
isMounted = false;
};
}, [networkName]);
return useMemo(
() => ({
durationMinutes: timeConfig.durationMinutes,
stepMinutes: timeConfig.stepMinutes,
properties: timeConfig.properties,
}),
[timeConfig],
);
};
+6 -2
View File
@@ -33,6 +33,7 @@ import {
} from "./mapLifecycle";
import { createOperationalMapResources } from "./operationalLayers";
import { getRoundedCurrentTimelineMinutes } from "./Controls/timelineTime";
import { useTimelineTimeConfig } from "./Controls/useTimelineTimeConfig";
interface MapComponentProps {
children?: React.ReactNode;
@@ -140,6 +141,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
];
const MAP_URL = config.MAP_URL;
const MAP_VIEW_STORAGE_KEY = `${MAP_WORKSPACE}_map_view`; // 持久化 key
const { durationMinutes, stepMinutes } = useTimelineTimeConfig();
const mapRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
@@ -778,7 +780,9 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
}, [compareMap, isCompareMode, map]);
useEffect(() => {
setCurrentTime(getRoundedCurrentTimelineMinutes());
setCurrentTime(
getRoundedCurrentTimelineMinutes(new Date(), stepMinutes, durationMinutes),
);
setSelectedDate(new Date());
setSchemeName("");
setCurrentJunctionCalData([]);
@@ -803,7 +807,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
deckLayerRef.current?.resetSessionLayers();
operationalResources.resetStyles();
};
}, [pathname, map, operationalResources]);
}, [durationMinutes, pathname, map, operationalResources, stepMinutes]);
// 当数据变化时,更新 deck.gl 图层
useEffect(() => {