diff --git a/src/components/olmap/core/Controls/Timeline.tsx b/src/components/olmap/core/Controls/Timeline.tsx index 31ad733..e5a290d 100644 --- a/src/components/olmap/core/Controls/Timeline.tsx +++ b/src/components/olmap/core/Controls/Timeline.tsx @@ -30,6 +30,11 @@ import { useData } from "../MapComponent"; import { config, NETWORK_NAME } from "@/config/config"; import { apiFetch } from "@/lib/apiFetch"; import { useMap } from "../MapComponent"; +import { + formatTimelineTime, + getRoundedCurrentTimelineMinutes, + normalizeTimelineMinutes, +} from "./timelineTime"; interface TimelineProps { schemeDate?: Date; @@ -99,6 +104,11 @@ const Timeline: React.FC = ({ const maxTime = timeRange ? timeRange.end.getHours() * 60 + timeRange.end.getMinutes() : 1440; + const timelineCurrentTime = normalizeTimelineMinutes( + currentTime, + minTime, + maxTime, + ); useEffect(() => { if (schemeDate) { setSelectedDate(schemeDate); @@ -327,20 +337,17 @@ const Timeline: React.FC = ({ // 格式化时间显示 function formatTime(minutes: number): string { - const hours = Math.floor(minutes / 60); - const mins = minutes % 60; - return `${hours.toString().padStart(2, "0")}:${mins - .toString() - .padStart(2, "0")}`; + return formatTimelineTime(minutes, minTime, maxTime); } - function currentTimeToDate(selectedDate: Date, minutes: number): Date { + const currentTimeToDate = useCallback((selectedDate: Date, minutes: number): Date => { const date = new Date(selectedDate); - const hours = Math.floor(minutes / 60); - const mins = minutes % 60; + const normalizedMinutes = normalizeTimelineMinutes(minutes, minTime, maxTime); + const hours = Math.floor(normalizedMinutes / 60); + const mins = normalizedMinutes % 60; date.setHours(hours, mins, 0, 0); return date; - } + }, [maxTime, minTime]); // 播放时间间隔选项 const intervalOptions = [ @@ -414,9 +421,7 @@ const Timeline: React.FC = ({ const handleStop = useCallback(() => { setIsPlaying(false); // 设置为当前时间 - const currentTime = new Date(); - const minutes = currentTime.getHours() * 60 + currentTime.getMinutes(); - setCurrentTime(minutes); // 组件卸载时重置时间 + setCurrentTime(getRoundedCurrentTimelineMinutes()); // 重置为当前时间 if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; @@ -514,7 +519,7 @@ const Timeline: React.FC = ({ // return; // } fetchFrameData( - currentTimeToDate(selectedDate, currentTime), + currentTimeToDate(selectedDate, timelineCurrentTime), junctionText, pipeText, schemeName, @@ -526,6 +531,8 @@ const Timeline: React.FC = ({ junctionText, pipeText, currentTime, + currentTimeToDate, + timelineCurrentTime, selectedDate, schemeName, schemeType, @@ -534,11 +541,7 @@ const Timeline: React.FC = ({ // 组件卸载时清理定时器和防抖 useEffect(() => { // 设置为当前时间 - const currentTime = new Date(); - const minutes = currentTime.getHours() * 60 + currentTime.getMinutes(); - // 找到最近的前15分钟刻度 - const roundedMinutes = Math.floor(minutes / 15) * 15; - setCurrentTime(roundedMinutes); // 组件卸载时重置时间 + setCurrentTime(getRoundedCurrentTimelineMinutes()); // 初始化为当前时间 return () => { if (intervalRef.current) { @@ -603,7 +606,7 @@ const Timeline: React.FC = ({ clearCache(linkCacheRef); // 重新获取当前时刻的新数据 fetchFrameData( - currentTimeToDate(selectedDate, currentTime), + currentTimeToDate(selectedDate, timelineCurrentTime), junctionText, pipeText, schemeName, @@ -622,7 +625,7 @@ const Timeline: React.FC = ({ // 提前提取日期和时间值,避免异步操作期间被时间轴拖动改变 const calculationDate = selectedDate; - const calculationTime = currentTime; + const calculationTime = timelineCurrentTime; const calculationDateTime = currentTimeToDate( calculationDate, calculationTime @@ -890,13 +893,13 @@ const Timeline: React.FC = ({ color: "primary.main", }} > - {formatTime(currentTime)} + {formatTime(timelineCurrentTime)} { + it("normalizes invalid minutes before formatting", () => { + expect(formatTimelineTime(-1)).toBe("00:00"); + expect(formatTimelineTime(Number.NaN)).toBe("00:00"); + }); + + it("clamps minutes to the configured range", () => { + expect(normalizeTimelineMinutes(-1, 60, 120)).toBe(60); + expect(normalizeTimelineMinutes(180, 60, 120)).toBe(120); + }); + + it("rounds the current time down to the timeline step", () => { + expect(getRoundedCurrentTimelineMinutes(new Date("2026-07-16T10:29:00"))).toBe(615); + }); +}); diff --git a/src/components/olmap/core/Controls/timelineTime.ts b/src/components/olmap/core/Controls/timelineTime.ts new file mode 100644 index 0000000..42f17b0 --- /dev/null +++ b/src/components/olmap/core/Controls/timelineTime.ts @@ -0,0 +1,32 @@ +export const TIMELINE_MINUTES_PER_DAY = 1440; +export const TIMELINE_STEP_MINUTES = 15; + +export const normalizeTimelineMinutes = ( + minutes: number | undefined, + minTime = 0, + maxTime = TIMELINE_MINUTES_PER_DAY, +) => { + if (typeof minutes !== "number" || !Number.isFinite(minutes)) { + return minTime; + } + + return Math.min(Math.max(minutes, minTime), maxTime); +}; + +export const getRoundedCurrentTimelineMinutes = (date = new Date()) => { + const minutes = date.getHours() * 60 + date.getMinutes(); + return Math.floor(minutes / TIMELINE_STEP_MINUTES) * TIMELINE_STEP_MINUTES; +}; + +export const formatTimelineTime = ( + minutes: number | undefined, + minTime = 0, + maxTime = TIMELINE_MINUTES_PER_DAY, +) => { + const normalizedMinutes = normalizeTimelineMinutes(minutes, minTime, maxTime); + const hours = Math.floor(normalizedMinutes / 60); + const mins = normalizedMinutes % 60; + return `${hours.toString().padStart(2, "0")}:${mins + .toString() + .padStart(2, "0")}`; +}; diff --git a/src/components/olmap/core/MapComponent.tsx b/src/components/olmap/core/MapComponent.tsx index 2936f48..b757a37 100644 --- a/src/components/olmap/core/MapComponent.tsx +++ b/src/components/olmap/core/MapComponent.tsx @@ -32,6 +32,7 @@ import { markMapResourcePersistent, } from "./mapLifecycle"; import { createOperationalMapResources } from "./operationalLayers"; +import { getRoundedCurrentTimelineMinutes } from "./Controls/timelineTime"; interface MapComponentProps { children?: React.ReactNode; @@ -155,7 +156,9 @@ const MapComponent: React.FC = ({ children }) => { const [compareMap, setCompareMap] = useState(); const [compareDeckLayer, setCompareDeckLayer] = useState(); // currentCalData 用于存储当前计算结果 - const [currentTime, setCurrentTime] = useState(-1); // 默认选择当前时间 + const [currentTime, setCurrentTime] = useState( + getRoundedCurrentTimelineMinutes, + ); // 默认选择当前时间 // const [selectedDate, setSelectedDate] = useState(new Date("2025-9-17")); const [selectedDate, setSelectedDate] = useState(new Date()); // 默认今天 const [schemeName, setSchemeName] = useState(""); // 当前方案名称 @@ -775,7 +778,7 @@ const MapComponent: React.FC = ({ children }) => { }, [compareMap, isCompareMode, map]); useEffect(() => { - setCurrentTime(-1); + setCurrentTime(getRoundedCurrentTimelineMinutes()); setSelectedDate(new Date()); setSchemeName(""); setCurrentJunctionCalData([]);