"use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; import { useNotification } from "@refinedev/core"; import { Box, Button, Slider, Typography, Paper, MenuItem, Select, FormControl, InputLabel, IconButton, Stack, Tooltip, } from "@mui/material"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; import { zhCN } from "date-fns/locale"; import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; import { TbRewindBackward15, TbRewindForward15 } from "react-icons/tb"; import { useData } from "../MapComponent"; import { config } from "@/config/config"; import { useMap } from "../MapComponent"; const backendUrl = config.backendUrl; const Timeline: React.FC = () => { const data = useData(); if (!data) { return
Loading...
; // 或其他占位符 } const { setCurrentJunctionCalData, setCurrentPipeCalData, junctionText, pipeText, } = data; const { open, close } = useNotification(); const [currentTime, setCurrentTime] = useState(0); // 分钟数 (0-1439) const [selectedDate, setSelectedDate] = useState(new Date("2025-9-17")); // const [selectedDate, setSelectedDate] = useState(new Date()); // 默认今天 const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(5000); // 毫秒 const [calculatedInterval, setCalculatedInterval] = useState(1440); // 分钟 const [sliderValue, setSliderValue] = useState(0); const intervalRef = useRef(null); const timelineRef = useRef(null); // 添加缓存引用 const cacheRef = useRef< Map >(new Map()); // 添加防抖引用 const debounceRef = useRef(null); const fetchFrameData = async (queryTime: Date) => { const query_time = queryTime.toISOString(); const cacheKey = query_time; // console.log("Fetching data for time:", query_time); // console.log("Junction Property:", junctionText); // console.log("Pipe Property:", pipeText); // 检查缓存 if (cacheRef.current.has(cacheKey)) { const { nodeRecords, linkRecords } = cacheRef.current.get(cacheKey)!; // 使用缓存数据更新状态 updateDataStates(nodeRecords, linkRecords); return; } try { // 定义需要查询的属性 const junctionProperties = junctionText; const pipeProperties = pipeText; // 如果属性未定义或为空,直接返回 if (junctionProperties === "" || pipeProperties === "") { return; } console.log( "Query Time:", queryTime.toLocaleDateString() + " " + queryTime.toLocaleTimeString() ); // 同时查询节点和管道数据 const [nodeResponse, linkResponse] = await Promise.all([ fetch( `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}` ), fetch( `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}` ), ]); const nodeRecords = await nodeResponse.json(); const linkRecords = await linkResponse.json(); // 缓存数据 cacheRef.current.set(cacheKey, { nodeRecords: nodeRecords.results, linkRecords: linkRecords.results, }); // 更新状态 updateDataStates(nodeRecords.results, linkRecords.results); } catch (error) { console.error("Error fetching data:", error); } }; // 提取更新状态的逻辑 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"); } }; // 时间刻度数组 (每5分钟一个刻度) const timeMarks = Array.from({ length: 288 }, (_, i) => ({ value: i * 5, label: i % 24 === 0 ? formatTime(i * 5) : "", })); // 格式化时间显示 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")}`; } function currentTimeToDate(selectedDate: Date, minutes: number): Date { const date = new Date(selectedDate); const hours = Math.floor(minutes / 60); const mins = minutes % 60; date.setHours(hours, mins, 0, 0); return date; } // 播放时间间隔选项 const intervalOptions = [ // { value: 1000, label: "1秒" }, { value: 2000, label: "2秒" }, { value: 5000, label: "5秒" }, { value: 10000, label: "10秒" }, ]; // 强制计算时间段选项 const calculatedIntervalOptions = [ { value: 1440, label: "1 天" }, { value: 60, label: "1 小时" }, { value: 30, label: "30 分钟" }, { value: 15, label: "15 分钟" }, { value: 5, label: "5 分钟" }, ]; // 处理时间轴滑动 const handleSliderChange = useCallback( (event: Event, newValue: number | number[]) => { const value = Array.isArray(newValue) ? newValue[0] : newValue; setSliderValue(value); // 防抖设置currentTime,避免频繁触发数据获取 if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { setCurrentTime(value); }, 300); // 300ms 防抖延迟 }, [] ); // 播放控制 const handlePlay = useCallback(() => { if (!isPlaying) { if (junctionText === "" || pipeText === "") { open?.({ type: "error", message: "请先设置节点和管道的属性。", }); return; } setIsPlaying(true); intervalRef.current = setInterval(() => { setCurrentTime((prev) => { const next = prev >= 1440 ? 0 : prev + 15; // 到达24:00后回到00:00 setSliderValue(next); return next; }); }, playInterval); } }, [isPlaying, playInterval]); const handlePause = useCallback(() => { setIsPlaying(false); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); const handleStop = useCallback(() => { setIsPlaying(false); setCurrentTime(0); setSliderValue(0); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); // 步进控制 const handleStepBackward = useCallback(() => { setCurrentTime((prev) => { const next = prev <= 0 ? 1440 : prev - 15; setSliderValue(next); return next; }); }, []); const handleStepForward = useCallback(() => { setCurrentTime((prev) => { const next = prev >= 1440 ? 0 : prev + 15; setSliderValue(next); return next; }); }, []); // 日期选择处理 const handleDateChange = useCallback((newDate: Date | null) => { if (newDate) { setSelectedDate(newDate); } }, []); // 播放间隔改变处理 const handleIntervalChange = useCallback( (event: any) => { const newInterval = event.target.value; setPlayInterval(newInterval); // 如果正在播放,重新启动定时器 if (isPlaying && intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setCurrentTime((prev) => { const next = prev >= 1440 ? 0 : prev + 15; setSliderValue(next); return next; }); }, newInterval); } }, [isPlaying] ); // 计算时间段改变处理 const handleCalculatedIntervalChange = useCallback((event: any) => { const newInterval = event.target.value; setCalculatedInterval(newInterval); }, []); // 添加 useEffect 来监听 currentTime 和 selectedDate 的变化,并获取数据 useEffect(() => { fetchFrameData(currentTimeToDate(selectedDate, currentTime)); }, [currentTime, selectedDate]); // 组件卸载时清理定时器和防抖 useEffect(() => { return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } if (debounceRef.current) { clearTimeout(debounceRef.current); } }; }, []); // 获取地图实例 const map = useMap(); // 这里防止地图缩放时,瓦片重新加载引起的属性更新出错 useEffect(() => { // 监听地图缩放事件,缩放时停止播放 if (map) { const onZoom = () => { handlePause(); }; map.getView().on("change:resolution", onZoom); // 清理事件监听 return () => { map.getView().un("change:resolution", onZoom); }; } }, [map, handlePause]); return ( {/* 控制按钮栏 */} {/* 日期选择器 */} handleDateChange( newValue && "toDate" in newValue ? newValue.toDate() : (newValue as Date | null) ) } enableAccessibleFieldDOMStructure={false} format="yyyy-MM-dd" sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }} maxDate={new Date()} // 禁止选取未来的日期 /> {/* 播放控制按钮 */} {/* 播放间隔选择 */} 播放间隔 {isPlaying ? : } {/* 强制计算时间段 */} 计算时间段 {/* 功能按钮 */} {/* 当前时间显示 */} {formatTime(currentTime)} {/* 时间轴滑块 */} index % 12 === 0)} // 每小时显示一个标记 onChange={handleSliderChange} valueLabelDisplay="auto" valueLabelFormat={formatTime} sx={{ height: 8, "& .MuiSlider-track": { backgroundColor: "primary.main", height: 6, }, "& .MuiSlider-rail": { backgroundColor: "grey.300", height: 6, }, "& .MuiSlider-thumb": { height: 20, width: 20, backgroundColor: "primary.main", border: "2px solid #fff", boxShadow: "0 2px 8px rgba(0,0,0,0.2)", "&:hover": { boxShadow: "0 4px 12px rgba(0,0,0,0.3)", }, }, "& .MuiSlider-mark": { backgroundColor: "grey.400", height: 4, width: 2, }, "& .MuiSlider-markActive": { backgroundColor: "primary.main", }, "& .MuiSlider-markLabel": { fontSize: "0.75rem", color: "grey.600", }, }} /> ); }; export default Timeline;