Files
TJWaterFrontend_Refine/src/app/OlMap/Controls/Timeline.tsx
2025-10-16 17:38:52 +08:00

495 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 <div>Loading...</div>; // 或其他占位符
}
const {
setCurrentJunctionCalData,
setCurrentPipeCalData,
junctionText,
pipeText,
} = data;
const { open, close } = useNotification();
const [currentTime, setCurrentTime] = useState<number>(0); // 分钟数 (0-1439)
const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17"));
// const [selectedDate, setSelectedDate] = useState<Date>(new Date()); // 默认今天
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒
const [calculatedInterval, setCalculatedInterval] = useState<number>(1440); // 分钟
const [sliderValue, setSliderValue] = useState<number>(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const timelineRef = useRef<HTMLDivElement>(null);
// 添加缓存引用
const cacheRef = useRef<
Map<string, { nodeRecords: any[]; linkRecords: any[] }>
>(new Map());
// 添加防抖引用
const debounceRef = useRef<NodeJS.Timeout | null>(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 (
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={zhCN}>
<Paper
elevation={3}
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
zIndex: 1000,
p: 2,
backgroundColor: "rgba(255, 255, 255, 0.95)",
backdropFilter: "blur(10px)",
}}
>
<Box sx={{ width: "100%" }}>
{/* 控制按钮栏 */}
<Stack
direction="row"
spacing={2}
alignItems="center"
sx={{ mb: 2, flexWrap: "wrap", gap: 1 }}
>
{/* 日期选择器 */}
<DatePicker
label="模拟数据日期选择"
value={selectedDate}
onChange={(newValue) =>
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()} // 禁止选取未来的日期
/>
{/* 播放控制按钮 */}
<Box sx={{ display: "flex", gap: 1 }}>
{/* 播放间隔选择 */}
<FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel></InputLabel>
<Select
value={playInterval}
label="播放间隔"
onChange={handleIntervalChange}
>
{intervalOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
<Tooltip title="后退一步">
<IconButton
color="primary"
onClick={handleStepBackward}
size="small"
>
<TbRewindBackward15 />
</IconButton>
</Tooltip>
<Tooltip title={isPlaying ? "暂停" : "播放"}>
<IconButton
color="primary"
onClick={isPlaying ? handlePause : handlePlay}
size="small"
>
{isPlaying ? <Pause /> : <PlayArrow />}
</IconButton>
</Tooltip>
<Tooltip title="前进一步">
<IconButton
color="primary"
onClick={handleStepForward}
size="small"
>
<TbRewindForward15 />
</IconButton>
</Tooltip>
<Tooltip title="停止">
<IconButton color="secondary" onClick={handleStop} size="small">
<Stop />
</IconButton>
</Tooltip>
</Box>
<Box sx={{ display: "flex", gap: 1 }}>
{/* 强制计算时间段 */}
<FormControl size="small" sx={{ minWidth: 100 }}>
<InputLabel></InputLabel>
<Select
value={calculatedInterval}
label="强制计算时间段"
onChange={handleCalculatedIntervalChange}
>
{calculatedIntervalOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
{/* 功能按钮 */}
<Tooltip title="强制计算">
<Button
variant="outlined"
size="small"
startIcon={<Refresh />}
// onClick={onRefresh}
>
</Button>
</Tooltip>
</Box>
{/* 当前时间显示 */}
<Typography
variant="h6"
sx={{
ml: "auto",
fontWeight: "bold",
color: "primary.main",
}}
>
{formatTime(currentTime)}
</Typography>
</Stack>
{/* 时间轴滑块 */}
<Box ref={timelineRef} sx={{ px: 2 }}>
<Slider
value={sliderValue}
min={0}
max={1440} // 24:00 = 1440分钟
step={15} // 每15分钟一个步进
marks={timeMarks.filter((_, index) => 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",
},
}}
/>
</Box>
</Box>
</Paper>
</LocalizationProvider>
);
};
export default Timeline;