完成管网在线模拟页面组件基本样式和布局

This commit is contained in:
JIANG
2025-09-30 17:55:15 +08:00
parent fc84b255ea
commit 5c888b60f0
13 changed files with 2028 additions and 54 deletions

View File

@@ -0,0 +1,383 @@
"use client";
import React, { useState, useEffect, useRef, useCallback } from "react";
import {
Box,
Button,
Slider,
Typography,
Paper,
TextField,
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 { TbRewindBackward5, TbRewindForward5 } from "react-icons/tb";
interface TimelineProps {
onTimeChange?: (time: string) => void;
onDateChange?: (date: Date) => void;
onPlay?: () => void;
onPause?: () => void;
onStop?: () => void;
onRefresh?: () => void;
onFetch?: () => void;
}
const Timeline: React.FC<TimelineProps> = ({
onTimeChange,
onDateChange,
onPlay,
onPause,
onStop,
onRefresh,
onFetch,
}) => {
const [currentTime, setCurrentTime] = useState<number>(0); // 分钟数 (0-1439)
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);
// 时间刻度数组 (每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")}`;
}
// 播放时间间隔选项
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);
setCurrentTime(value);
onTimeChange?.(formatTime(value));
},
[onTimeChange]
);
// 播放控制
const handlePlay = useCallback(() => {
if (!isPlaying) {
setIsPlaying(true);
onPlay?.();
intervalRef.current = setInterval(() => {
setCurrentTime((prev) => {
const next = prev >= 1435 ? 0 : prev + 5; // 到达23:55后回到00:00
setSliderValue(next);
onTimeChange?.(formatTime(next));
return next;
});
}, playInterval);
}
}, [isPlaying, playInterval, onPlay, onTimeChange]);
const handlePause = useCallback(() => {
setIsPlaying(false);
onPause?.();
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [onPause]);
const handleStop = useCallback(() => {
setIsPlaying(false);
setCurrentTime(0);
setSliderValue(0);
onStop?.();
onTimeChange?.(formatTime(0));
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [onStop, onTimeChange]);
// 步进控制
const handleStepBackward = useCallback(() => {
setCurrentTime((prev) => {
const next = prev <= 0 ? 1435 : prev - 5;
setSliderValue(next);
onTimeChange?.(formatTime(next));
return next;
});
}, [onTimeChange]);
const handleStepForward = useCallback(() => {
setCurrentTime((prev) => {
const next = prev >= 1435 ? 0 : prev + 5;
setSliderValue(next);
onTimeChange?.(formatTime(next));
return next;
});
}, [onTimeChange]);
// 日期选择处理
const handleDateChange = useCallback(
(newDate: Date | null) => {
if (newDate) {
setSelectedDate(newDate);
onDateChange?.(newDate);
}
},
[onDateChange]
);
// 播放间隔改变处理
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 >= 1435 ? 0 : prev + 5;
setSliderValue(next);
onTimeChange?.(formatTime(next));
return next;
});
}, newInterval);
}
},
[isPlaying, onTimeChange]
);
// 计算时间段改变处理
const handleCalculatedIntervalChange = useCallback((event: any) => {
const newInterval = event.target.value;
setCalculatedInterval(newInterval);
}, []);
// 组件卸载时清理定时器
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
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)}
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"
>
<TbRewindBackward5 />
</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"
>
<TbRewindForward5 />
</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={1435} // 23:55 = 1435分钟
step={5}
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;