143 lines
5.4 KiB
TypeScript
143 lines
5.4 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import { Play, Pause, SkipBack, SkipForward, History } from 'lucide-react'
|
|
import { usePlaybackStore, REPLAY_TICKS, REPLAY_START, REPLAY_END } from '@/store/playbackStore'
|
|
|
|
function formatTick(iso: string): string {
|
|
const d = new Date(iso)
|
|
return d.toLocaleString('zh-CN', {
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
})
|
|
}
|
|
|
|
export function TimelinePanel() {
|
|
const {
|
|
isReplayMode,
|
|
playbackTime,
|
|
isPlaying,
|
|
speedSecPerTick,
|
|
setReplayMode,
|
|
setPlaybackTime,
|
|
setIsPlaying,
|
|
stepForward,
|
|
stepBack,
|
|
setSpeed,
|
|
} = usePlaybackStore()
|
|
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (!isPlaying || !isReplayMode) {
|
|
if (timerRef.current) {
|
|
clearInterval(timerRef.current)
|
|
timerRef.current = null
|
|
}
|
|
return
|
|
}
|
|
timerRef.current = setInterval(() => {
|
|
const current = usePlaybackStore.getState().playbackTime
|
|
const i = REPLAY_TICKS.indexOf(current)
|
|
if (i >= REPLAY_TICKS.length - 1) {
|
|
setIsPlaying(false)
|
|
return
|
|
}
|
|
setPlaybackTime(REPLAY_TICKS[i + 1])
|
|
}, speedSecPerTick * 1000)
|
|
return () => {
|
|
if (timerRef.current) clearInterval(timerRef.current)
|
|
}
|
|
}, [isPlaying, isReplayMode, speedSecPerTick, setPlaybackTime, setIsPlaying])
|
|
|
|
const index = REPLAY_TICKS.indexOf(playbackTime)
|
|
const value = index >= 0 ? index : REPLAY_TICKS.length - 1
|
|
|
|
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const i = parseInt(e.target.value, 10)
|
|
setPlaybackTime(REPLAY_TICKS[i])
|
|
}
|
|
|
|
return (
|
|
<div className="shrink-0 border-b border-military-border bg-military-panel/95 px-3 py-2">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setReplayMode(!isReplayMode)}
|
|
className={`flex items-center gap-1.5 rounded px-2 py-1 text-xs font-medium transition-colors ${
|
|
isReplayMode
|
|
? 'bg-military-accent/30 text-military-accent'
|
|
: 'bg-military-border/50 text-military-text-secondary hover:bg-military-border hover:text-military-text-primary'
|
|
}`}
|
|
>
|
|
<History className="h-3.5 w-3.5" />
|
|
数据回放
|
|
</button>
|
|
|
|
{isReplayMode && (
|
|
<>
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
onClick={stepBack}
|
|
disabled={index <= 0}
|
|
className="rounded p-1 text-military-text-secondary hover:bg-military-border hover:text-military-text-primary disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-military-text-secondary"
|
|
title="上一步"
|
|
>
|
|
<SkipBack className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsPlaying(!isPlaying)}
|
|
className="rounded p-1 text-military-text-secondary hover:bg-military-border hover:text-military-text-primary"
|
|
title={isPlaying ? '暂停' : '播放'}
|
|
>
|
|
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={stepForward}
|
|
disabled={index >= REPLAY_TICKS.length - 1}
|
|
className="rounded p-1 text-military-text-secondary hover:bg-military-border hover:text-military-text-primary disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-military-text-secondary"
|
|
title="下一步"
|
|
>
|
|
<SkipForward className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex min-w-0 flex-1 items-center gap-2 lg:min-w-[320px]">
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={REPLAY_TICKS.length - 1}
|
|
value={value}
|
|
onChange={handleSliderChange}
|
|
className="h-1.5 flex-1 cursor-pointer appearance-none rounded-full bg-military-border [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-military-accent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 text-[11px] tabular-nums text-military-text-secondary">
|
|
<span>{formatTick(REPLAY_START)}</span>
|
|
<span className="font-medium text-military-accent">{formatTick(playbackTime)}</span>
|
|
<span>{formatTick(REPLAY_END)}</span>
|
|
</div>
|
|
|
|
<select
|
|
value={speedSecPerTick}
|
|
onChange={(e) => setSpeed(Number(e.target.value))}
|
|
className="rounded border border-military-border bg-military-dark/80 px-2 py-1 text-[11px] text-military-text-secondary focus:border-military-accent focus:outline-none"
|
|
>
|
|
<option value={0.5}>0.5 秒/刻度</option>
|
|
<option value={1}>1 秒/刻度</option>
|
|
<option value={2}>2 秒/刻度</option>
|
|
<option value={3}>3 秒/刻度</option>
|
|
<option value={5}>5 秒/刻度</option>
|
|
</select>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|