This commit is contained in:
Daniel
2026-03-04 16:48:17 +08:00
parent 64f4c438c3
commit 26938449f0
34 changed files with 956 additions and 500 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { Play, Pause, SkipBack, SkipForward, History } from 'lucide-react'
import { usePlaybackStore, REPLAY_TICKS, REPLAY_START, REPLAY_END } from '@/store/playbackStore'
import { usePlaybackStore, getTicks, REPLAY_START, REPLAY_END, type ReplayScale } from '@/store/playbackStore'
import { useSituationStore } from '@/store/situationStore'
import { NewsTicker } from './NewsTicker'
import { config } from '@/config'
@@ -33,16 +33,20 @@ export function TimelinePanel() {
const {
isReplayMode,
playbackTime,
replayScale,
isPlaying,
speedSecPerTick,
setReplayMode,
setPlaybackTime,
setReplayScale,
setIsPlaying,
stepForward,
stepBack,
setSpeed,
} = usePlaybackStore()
const replayTicks = getTicks(replayScale)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
useEffect(() => {
@@ -62,27 +66,30 @@ export function TimelinePanel() {
return
}
timerRef.current = setInterval(() => {
const current = usePlaybackStore.getState().playbackTime
const i = REPLAY_TICKS.indexOf(current)
if (i >= REPLAY_TICKS.length - 1) {
const { playbackTime: current, replayScale: scale } = usePlaybackStore.getState()
const ticks = getTicks(scale)
const i = ticks.indexOf(current)
if (i >= ticks.length - 1) {
setIsPlaying(false)
return
}
setPlaybackTime(REPLAY_TICKS[i + 1])
setPlaybackTime(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 index = replayTicks.indexOf(playbackTime)
const value = index >= 0 ? index : replayTicks.length - 1
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const i = parseInt(e.target.value, 10)
setPlaybackTime(REPLAY_TICKS[i])
setPlaybackTime(replayTicks[i])
}
const scaleLabels: Record<ReplayScale, string> = { '30m': '30分钟', '1h': '1小时', '1d': '1天' }
return (
<div className="relative shrink-0 border-b border-military-border bg-military-panel/95 px-3 py-2">
{!isReplayMode && (
@@ -142,7 +149,7 @@ export function TimelinePanel() {
<button
type="button"
onClick={stepForward}
disabled={index >= REPLAY_TICKS.length - 1}
disabled={index >= replayTicks.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="下一步"
>
@@ -150,21 +157,45 @@ export function TimelinePanel() {
</button>
</div>
<div className="flex min-w-0 flex-1 items-center gap-2 lg:min-w-[320px]">
<div className="flex items-center gap-1 text-[11px] text-military-text-secondary">
<span></span>
<select
value={replayScale}
onChange={(e) => setReplayScale(e.target.value as ReplayScale)}
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"
title="回放刻度"
>
{(['30m', '1h', '1d'] as const).map((s) => (
<option key={s} value={s}>{scaleLabels[s]}</option>
))}
</select>
</div>
<div className="flex min-w-0 flex-1 flex-col gap-0.5 lg:min-w-[320px]">
<input
type="range"
min={0}
max={REPLAY_TICKS.length - 1}
max={replayTicks.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"
className="h-1.5 w-full 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>
{/* 按刻度划分的时间轴:均匀取约 56 个刻度标签 */}
<div className="flex justify-between text-[10px] tabular-nums text-military-text-secondary">
{replayTicks.length <= 1 ? (
<span>{formatTick(replayTicks[0] ?? REPLAY_START)}</span>
) : (
(() => {
const n = replayTicks.length - 1
const maxLabels = 6
const step = Math.max(1, Math.floor(n / (maxLabels - 1)))
const indices = [0, ...Array.from({ length: maxLabels - 2 }, (_, j) => Math.min((j + 1) * step, n)), n]
return [...new Set(indices)].map((i) => (
<span key={i}>{formatTick(replayTicks[i])}</span>
))
})()
)}
</div>
</div>
<select