800 lines
26 KiB
TypeScript
800 lines
26 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||
import { useNotification } from "@refinedev/core";
|
||
import Draggable from "react-draggable";
|
||
|
||
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<TimelineProps> = ({
|
||
schemeDate,
|
||
timeRange,
|
||
disableDateSelection = false,
|
||
schemeName = "",
|
||
}) => {
|
||
const data = useData();
|
||
if (!data) {
|
||
return <div>Loading...</div>; // 或其他占位符
|
||
}
|
||
const {
|
||
currentTime,
|
||
setCurrentTime,
|
||
selectedDate,
|
||
setSelectedDate,
|
||
setCurrentJunctionCalData,
|
||
setCurrentPipeCalData,
|
||
junctionText,
|
||
pipeText,
|
||
} = data;
|
||
if (
|
||
setCurrentTime === undefined ||
|
||
currentTime === undefined ||
|
||
selectedDate === undefined ||
|
||
setSelectedDate === undefined
|
||
) {
|
||
return <div>Loading...</div>; // 或其他占位符
|
||
}
|
||
const { open } = useNotification();
|
||
|
||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||
const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒
|
||
const [calculatedInterval, setCalculatedInterval] = useState<number>(15); // 分钟
|
||
const [isCalculating, setIsCalculating] = useState<boolean>(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]);
|
||
// 新增:用于 Draggable 的 nodeRef
|
||
const draggableRef = useRef<HTMLDivElement>(null);
|
||
|
||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
const timelineRef = useRef<HTMLDivElement>(null);
|
||
// 添加缓存引用
|
||
const nodeCacheRef = useRef<Map<string, any[]>>(new Map());
|
||
const linkCacheRef = useRef<Map<string, any[]>>(new Map());
|
||
// 添加防抖引用
|
||
const debounceRef = useRef<NodeJS.Timeout | null>(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<Response>[] = [];
|
||
let nodePromise: Promise<any> | null = null;
|
||
let linkPromise: Promise<any> | 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}`
|
||
`${backendUrl}/timescaledb/scheme/query/by-scheme-time-property?scheme_type=burst_Analysis&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}`
|
||
))
|
||
: (nodePromise = fetch(
|
||
// `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}`
|
||
`${backendUrl}/timescaledb/realtime/query/by-time-property?query_time=${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}`
|
||
`${backendUrl}/timescaledb/scheme/query/by-scheme-time-property?scheme_type=burst_Analysis&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${pipeProperties}`
|
||
))
|
||
: (linkPromise = fetch(
|
||
// `${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}`
|
||
`${backendUrl}/timescaledb/realtime/query/by-time-property?query_time=${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: 5000, label: "5秒" },
|
||
{ 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<typeof useRef<Map<string, any[]>>>
|
||
) => {
|
||
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 (
|
||
<Draggable nodeRef={draggableRef}>
|
||
<div
|
||
ref={draggableRef}
|
||
className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 w-[920px] opacity-90 hover:opacity-100 transition-opacity duration-300"
|
||
>
|
||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-cn">
|
||
<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 }}
|
||
>
|
||
<Tooltip title="后退一天">
|
||
<IconButton
|
||
color="primary"
|
||
onClick={handleDayStepBackward}
|
||
size="small"
|
||
disabled={disableDateSelection}
|
||
>
|
||
<FiSkipBack />
|
||
</IconButton>
|
||
</Tooltip>
|
||
{/* 日期选择器 */}
|
||
<DatePicker
|
||
label="模拟数据日期选择"
|
||
value={dayjs(selectedDate)}
|
||
onChange={(value) =>
|
||
value && handleDateChange(value.toDate())
|
||
}
|
||
enableAccessibleFieldDOMStructure={false}
|
||
format="YYYY-MM-DD"
|
||
sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }}
|
||
maxDate={dayjs(new Date())} // 禁止选取未来的日期
|
||
disabled={disableDateSelection}
|
||
/>
|
||
<Tooltip title="前进一天">
|
||
<IconButton
|
||
color="primary"
|
||
onClick={handleDayStepForward}
|
||
size="small"
|
||
disabled={
|
||
disableDateSelection ||
|
||
selectedDate.toDateString() === new Date().toDateString()
|
||
}
|
||
>
|
||
<FiSkipForward />
|
||
</IconButton>
|
||
</Tooltip>
|
||
{/* 播放控制按钮 */}
|
||
<Box sx={{ display: "flex", gap: 1 }} className="ml-4">
|
||
{/* 播放间隔选择 */}
|
||
<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 }} className="ml-4">
|
||
{/* 强制计算时间段 */}
|
||
<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={handleForceCalculate}
|
||
disabled={isCalculating}
|
||
>
|
||
强制计算
|
||
</Button>
|
||
</Tooltip>
|
||
</Box>
|
||
|
||
{/* 当前时间显示 */}
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
ml: "auto",
|
||
fontWeight: "bold",
|
||
color: "primary.main",
|
||
}}
|
||
>
|
||
{formatTime(currentTime)}
|
||
</Typography>
|
||
</Stack>
|
||
|
||
<Box ref={timelineRef} sx={{ px: 2, position: "relative" }}>
|
||
<Slider
|
||
value={currentTime}
|
||
min={0}
|
||
max={1440} // 24:00 = 1440分钟
|
||
step={15} // 每15分钟一个步进
|
||
marks={timeMarks.filter((_, index) => 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 && (
|
||
<Box
|
||
sx={{
|
||
position: "absolute",
|
||
left: "14px",
|
||
top: "30%",
|
||
transform: "translateY(-50%)",
|
||
width: `${(minTime / 1440) * 856 + 2}px`,
|
||
height: "20px",
|
||
backgroundColor: "rgba(189, 189, 189, 0.4)",
|
||
pointerEvents: "none",
|
||
backdropFilter: "blur(1px)",
|
||
borderRadius: "2.5px",
|
||
rounded: "true",
|
||
}}
|
||
/>
|
||
)}
|
||
{/* 右侧禁用区域 */}
|
||
{maxTime < 1440 && (
|
||
<Box
|
||
sx={{
|
||
position: "absolute",
|
||
left: `${16 + (maxTime / 1440) * 856}px`,
|
||
top: "30%",
|
||
transform: "translateY(-50%)",
|
||
width: `${((1440 - maxTime) / 1440) * 856}px`,
|
||
height: "20px",
|
||
backgroundColor: "rgba(189, 189, 189, 0.4)",
|
||
pointerEvents: "none",
|
||
backdropFilter: "blur(1px)",
|
||
borderRadius: "2.5px",
|
||
}}
|
||
/>
|
||
)}
|
||
</>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
</Paper>
|
||
</LocalizationProvider>
|
||
</div>
|
||
</Draggable>
|
||
);
|
||
};
|
||
|
||
export default Timeline;
|