161 lines
6.1 KiB
TypeScript
161 lines
6.1 KiB
TypeScript
'use client'
|
|
|
|
import React, { useCallback } from 'react'
|
|
import { Slider } from '@/components/ui/slider'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
import {
|
|
Play,
|
|
Pause,
|
|
SkipBack,
|
|
SkipForward,
|
|
Clock,
|
|
Calendar
|
|
} from 'lucide-react'
|
|
import { format, addDays, subDays, startOfDay } from 'date-fns'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface TimelineProps {
|
|
className?: string
|
|
startDate?: Date
|
|
endDate?: Date
|
|
currentDate?: Date
|
|
onDateChange?: (date: Date) => void
|
|
onPlay?: () => void
|
|
onPause?: () => void
|
|
isPlaying?: boolean
|
|
speed?: 'slow' | 'normal' | 'fast'
|
|
onSpeedChange?: (speed: 'slow' | 'normal' | 'fast') => void
|
|
}
|
|
|
|
export function Timeline({
|
|
className,
|
|
startDate = subDays(new Date(), 30),
|
|
endDate = new Date(),
|
|
currentDate = new Date(),
|
|
onDateChange,
|
|
onPlay,
|
|
onPause,
|
|
isPlaying = false,
|
|
speed = 'normal',
|
|
onSpeedChange
|
|
}: TimelineProps) {
|
|
// 计算时间轴进度 (0-100)
|
|
const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
const currentDays = Math.ceil((currentDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
const progress = Math.max(0, Math.min(100, (currentDays / totalDays) * 100))
|
|
|
|
const handleSliderChange = useCallback((value: number[]) => {
|
|
const newProgress = value[0]
|
|
const newDays = Math.round((newProgress / 100) * totalDays)
|
|
const newDate = addDays(startDate, newDays)
|
|
onDateChange?.(newDate)
|
|
}, [startDate, totalDays, onDateChange])
|
|
|
|
const handlePlayPause = useCallback(() => {
|
|
if (isPlaying) {
|
|
onPause?.()
|
|
} else {
|
|
onPlay?.()
|
|
}
|
|
}, [isPlaying, onPlay, onPause])
|
|
|
|
const handleSkipBack = useCallback(() => {
|
|
const newDate = subDays(currentDate, 1)
|
|
onDateChange?.(newDate)
|
|
}, [currentDate, onDateChange])
|
|
|
|
const handleSkipForward = useCallback(() => {
|
|
const newDate = addDays(currentDate, 1)
|
|
onDateChange?.(newDate)
|
|
}, [currentDate, onDateChange])
|
|
|
|
const speedOptions = [
|
|
{ value: 'slow', label: '慢速', interval: 2000 },
|
|
{ value: 'normal', label: '正常', interval: 1000 },
|
|
{ value: 'fast', label: '快速', interval: 500 }
|
|
]
|
|
|
|
return (
|
|
<Card className={cn("w-full max-w-4xl mx-auto", className)}>
|
|
<CardContent className="p-4">
|
|
<div className="flex flex-col gap-4">
|
|
{/* 时间显示 */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Calendar className="w-4 h-4" />
|
|
<span>{format(startDate, 'yyyy-MM-dd')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
<Clock className="w-4 h-4" />
|
|
<span>{format(currentDate, 'yyyy-MM-dd')}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Calendar className="w-4 h-4" />
|
|
<span>{format(endDate, 'yyyy-MM-dd')}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 时间轴滑块 */}
|
|
<div className="flex items-center gap-4">
|
|
<Slider
|
|
value={[progress]}
|
|
onValueChange={handleSliderChange}
|
|
max={100}
|
|
step={1}
|
|
className="flex-1"
|
|
/>
|
|
</div>
|
|
|
|
{/* 控制按钮 */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleSkipBack}
|
|
disabled={currentDate <= startDate}
|
|
>
|
|
<SkipBack className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handlePlayPause}
|
|
>
|
|
{isPlaying ? (
|
|
<Pause className="w-4 h-4" />
|
|
) : (
|
|
<Play className="w-4 h-4" />
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleSkipForward}
|
|
disabled={currentDate >= endDate}
|
|
>
|
|
<SkipForward className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 速度控制 */}
|
|
<div className="flex items-center gap-1">
|
|
{speedOptions.map((option) => (
|
|
<Button
|
|
key={option.value}
|
|
variant={speed === option.value ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => onSpeedChange?.(option.value as 'slow' | 'normal' | 'fast')}
|
|
className="text-xs px-2"
|
|
>
|
|
{option.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|