"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 { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import "dayjs/locale/zh-cn"; // 引入中文包 import dayjs from "dayjs"; import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material"; import { TbRewindBackward15, TbRewindForward15 } from "react-icons/tb"; import { FiSkipBack, FiSkipForward } from "react-icons/fi"; import { useData } from "../MapComponent"; import { config, NETWORK_NAME } from "@/config/config"; import { useMap } from "../MapComponent"; const backendUrl = config.BACKEND_URL; interface TimelineProps { schemeDate?: Date; timeRange?: { start: Date; end: Date }; disableDateSelection?: boolean; schemeName?: string; } const Timeline: React.FC = ({ schemeDate, timeRange, disableDateSelection = false, schemeName = "", }) => { const data = useData(); if (!data) { return
Loading...
; // 或其他占位符 } const { currentTime, setCurrentTime, selectedDate, setSelectedDate, setCurrentJunctionCalData, setCurrentPipeCalData, junctionText, pipeText, } = data; if ( setCurrentTime === undefined || currentTime === undefined || selectedDate === undefined || setSelectedDate === undefined ) { return
Loading...
; // 或其他占位符 } const { open, close } = useNotification(); const [isPlaying, setIsPlaying] = useState(false); const [playInterval, setPlayInterval] = useState(15000); // 毫秒 const [calculatedInterval, setCalculatedInterval] = useState(15); // 分钟 const [isCalculating, setIsCalculating] = useState(false); // 计算时间轴范围 const minTime = timeRange ? timeRange.start.getHours() * 60 + timeRange.start.getMinutes() : 0; const maxTime = timeRange ? timeRange.end.getHours() * 60 + timeRange.end.getMinutes() : 1440; useEffect(() => { if (schemeDate) { setSelectedDate(schemeDate); } }, [schemeDate]); const intervalRef = useRef(null); const timelineRef = useRef(null); // 添加缓存引用 const nodeCacheRef = useRef>(new Map()); const linkCacheRef = useRef>(new Map()); // 添加防抖引用 const debounceRef = useRef(null); const fetchFrameData = async ( queryTime: Date, junctionProperties: string, pipeProperties: string, schemeName: string ) => { const query_time = queryTime.toISOString(); let nodeRecords: any = { results: [] }; let linkRecords: any = { results: [] }; const requests: Promise[] = []; let nodePromise: Promise | null = null; let linkPromise: Promise | null = null; // 检查node缓存 if (junctionProperties !== "") { const nodeCacheKey = `${query_time}_${junctionProperties}_${schemeName}`; if (nodeCacheRef.current.has(nodeCacheKey)) { nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!; } else { disableDateSelection && schemeName ? (nodePromise = fetch( `${backendUrl}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}&schemename=${schemeName}` )) : (nodePromise = fetch( `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}` )); requests.push(nodePromise); } } // 检查link缓存 if (pipeProperties !== "") { const linkCacheKey = `${query_time}_${pipeProperties}_${schemeName}`; if (linkCacheRef.current.has(linkCacheKey)) { linkRecords = linkCacheRef.current.get(linkCacheKey)!; } else { disableDateSelection && schemeName ? (linkPromise = fetch( `${backendUrl}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}&schemename=${schemeName}` )) : (linkPromise = fetch( `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}` )); requests.push(linkPromise); } } // 等待所有有效请求 const responses = await Promise.all(requests); if (nodePromise) { const nodeResponse = responses.shift()!; if (!nodeResponse.ok) throw new Error(`Node fetch failed: ${nodeResponse.status}`); nodeRecords = await nodeResponse.json(); // 缓存数据(修复键以包含 schemeName) nodeCacheRef.current.set( `${query_time}_${junctionProperties}_${schemeName}`, nodeRecords || [] ); } if (linkPromise) { const linkResponse = responses.shift()!; if (!linkResponse.ok) throw new Error(`Link fetch failed: ${linkResponse.status}`); linkRecords = await linkResponse.json(); // 缓存数据(修复键以包含 schemeName) linkCacheRef.current.set( `${query_time}_${pipeProperties}_${schemeName}`, linkRecords || [] ); } // 更新状态 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"); } }; // 时间刻度数组 (每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: 10000, label: "10秒" }, { value: 15000, label: "15秒" }, { 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 handleSliderChange = useCallback( (event: Event, newValue: number | number[]) => { const value = Array.isArray(newValue) ? newValue[0] : newValue; // 如果有时间范围限制,只允许在范围内拖动 if (timeRange && (value < minTime || value > maxTime)) { return; } // 防抖设置currentTime,避免频繁触发数据获取 if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { setCurrentTime(value); }, 500); // 500ms 防抖延迟 }, [timeRange, minTime, maxTime] ); // 播放控制 const handlePlay = useCallback(() => { if (!isPlaying) { if (junctionText === "" && pipeText === "") { open?.({ type: "error", message: "请至少设定并应用一个图层的样式。", }); // return; } setIsPlaying(true); intervalRef.current = setInterval(() => { setCurrentTime((prev) => { let next = prev + 15; if (timeRange) { if (next > maxTime) next = minTime; } else { if (next >= 1440) next = 0; } return next; }); }, playInterval); } }, [isPlaying, playInterval]); const handlePause = useCallback(() => { setIsPlaying(false); if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); const handleStop = useCallback(() => { setIsPlaying(false); // 设置为当前时间 const currentTime = new Date(); const minutes = currentTime.getHours() * 60 + currentTime.getMinutes(); setCurrentTime(minutes); // 组件卸载时重置时间 if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); // 步进控制 const handleDayStepBackward = useCallback(() => { setSelectedDate((prev) => { const newDate = new Date(prev); newDate.setDate(newDate.getDate() - 1); return newDate; }); }, []); const handleDayStepForward = useCallback(() => { setSelectedDate((prev) => { const newDate = new Date(prev); newDate.setDate(newDate.getDate() + 1); return newDate; }); }, []); const handleStepBackward = useCallback(() => { setCurrentTime((prev) => { let next = prev - 15; if (timeRange) { if (next < minTime) next = maxTime; } else { if (next <= 0) next = 1440; } return next; }); }, [timeRange, minTime, maxTime]); const handleStepForward = useCallback(() => { setCurrentTime((prev) => { let next = prev + 15; if (timeRange) { if (next > maxTime) next = minTime; } else { if (next >= 1440) next = 0; } return next; }); }, [timeRange, minTime, maxTime]); // 日期选择处理 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) => { let next = prev + 15; if (timeRange) { if (next > maxTime) next = minTime; } else { if (next >= 1440) next = 0; } return next; }); }, newInterval); } }, [isPlaying] ); // 计算时间段改变处理 const handleCalculatedIntervalChange = useCallback((event: any) => { const newInterval = event.target.value; setCalculatedInterval(newInterval); }, []); // 添加 useEffect 来监听 currentTime 和 selectedDate 的变化,并获取数据 useEffect(() => { // 首次加载时,如果 selectedDate 或 currentTime 未定义,则跳过执行,避免报错 if (selectedDate && currentTime !== undefined && currentTime !== -1) { // 检查至少一个属性有值 const junctionProperties = junctionText; const pipeProperties = pipeText; if (junctionProperties === "" && pipeProperties === "") { open?.({ type: "error", message: "请至少设定并应用一个图层的样式。", }); return; } fetchFrameData( currentTimeToDate(selectedDate, currentTime), junctionText, pipeText, schemeName ); } }, [junctionText, pipeText, currentTime, selectedDate]); // 组件卸载时清理定时器和防抖 useEffect(() => { // 设置为当前时间 const currentTime = new Date(); const minutes = currentTime.getHours() * 60 + currentTime.getMinutes(); // 找到最近的前15分钟刻度 const roundedMinutes = Math.floor(minutes / 15) * 15; setCurrentTime(roundedMinutes); // 组件卸载时重置时间 return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } if (debounceRef.current) { clearTimeout(debounceRef.current); } }; }, []); // 当 timeRange 改变时,设置 currentTime 到 minTime useEffect(() => { if (timeRange) { setCurrentTime(minTime); } }, [timeRange, minTime]); // 获取地图实例 const map = useMap(); // 这里防止地图缩放时,瓦片重新加载引起的属性更新出错 useEffect(() => { // 监听地图缩放事件,缩放时停止播放 if (map) { const onZoom = () => { handlePause(); }; map.getView().on("change:resolution", onZoom); // 清理事件监听 return () => { map.getView().un("change:resolution", onZoom); }; } }, [map, handlePause]); // 清除当天当前时间点后的缓存并重新获取数据 const clearCacheAndRefetch = (date: Date, timeInMinutes: number) => { const dateStr = date.toISOString().split("T")[0]; const clearCache = ( cacheRef: ReturnType>> ) => { if (!cacheRef.current) return; const cacheKeys = Array.from(cacheRef.current.keys()); cacheKeys.forEach((key) => { const keyParts = key.split("_"); const cacheDate = keyParts[0].split("T")[0]; const cacheTimeStr = keyParts[0].split("T")[1]; if (cacheDate === dateStr && cacheTimeStr) { const [hours, minutes] = cacheTimeStr.split(":"); const cacheTimeInMinutes = (parseInt(hours) + 8) * 60 + parseInt(minutes); if (cacheTimeInMinutes >= timeInMinutes && cacheRef.current) { cacheRef.current.delete(key); } } }); }; clearCache(nodeCacheRef); clearCache(linkCacheRef); // 重新获取当前时刻的新数据 fetchFrameData( currentTimeToDate(selectedDate, currentTime), junctionText, pipeText, schemeName ); }; const handleForceCalculate = async () => { if (!NETWORK_NAME) { open?.({ type: "error", message: "方案名称未设置,无法进行强制计算。", }); return; } // 提前提取日期和时间值,避免异步操作期间被时间轴拖动改变 const calculationDate = selectedDate; const calculationTime = currentTime; const calculationDateStr = calculationDate.toISOString().split("T")[0]; setIsCalculating(true); // 显示处理中的通知 open?.({ type: "progress", message: "正在强制计算,请稍候...", undoableTimeout: 3, }); try { const body = { name: NETWORK_NAME, simulation_date: calculationDateStr, // YYYY-MM-DD start_time: `${formatTime(calculationTime)}:00`, // HH:MM:00 duration: calculatedInterval, }; const response = await fetch( `${backendUrl}/runsimulationmanuallybydate/`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), } ); if (response.ok) { open?.({ type: "success", message: "重新计算成功", }); // 清空当天当前时刻及之后的缓存并重新获取数据 clearCacheAndRefetch(calculationDate, calculationTime); } else { open?.({ type: "error", message: "重新计算失败", }); } } catch (error) { console.error("Recalculation failed:", error); open?.({ type: "error", message: "重新计算时发生错误", }); } finally { setIsCalculating(false); } }; return (
{/* 控制按钮栏 */} {/* 日期选择器 */} value && handleDateChange(value.toDate())} enableAccessibleFieldDOMStructure={false} format="YYYY-MM-DD" sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }} maxDate={dayjs(new Date())} // 禁止选取未来的日期 disabled={disableDateSelection} /> {/* 播放控制按钮 */} {/* 播放间隔选择 */} 播放间隔 {isPlaying ? : } {/* 强制计算时间段 */} 计算时间段 {/* 功能按钮 */} {/* 当前时间显示 */} {formatTime(currentTime)} index % 12 === 0)} // 每小时显示一个标记 onChange={handleSliderChange} valueLabelDisplay="auto" valueLabelFormat={formatTime} sx={{ zIndex: 10, height: 8, "& .MuiSlider-track": { backgroundColor: "primary.main", height: 6, display: timeRange ? "none" : "block", }, "& .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", }, }} /> {/* 禁用区域遮罩 */} {timeRange && ( <> {/* 左侧禁用区域 */} {minTime > 0 && ( )} {/* 右侧禁用区域 */} {maxTime < 1440 && ( )} )}
); }; export default Timeline;