2026-04-29-21-27-10 - 组件目录扁平化重构
This commit is contained in:
82
src/components/FrameTimeline.tsx
Normal file
82
src/components/FrameTimeline.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Play, Pause } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
export function FrameTimeline() {
|
||||
const [currentFrame, setCurrentFrame] = useState(142);
|
||||
const totalFrames = 2400;
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
// show frames around current frame
|
||||
const frameWindow = 20;
|
||||
const frames = Array.from({ length: 41 }, (_, i) => currentFrame - frameWindow + i);
|
||||
|
||||
return (
|
||||
<div className="h-32 bg-[#111] border-t border-white/5 flex flex-col shrink-0 z-20">
|
||||
<div className="h-4 bg-[#0d0d0d] flex items-center group relative">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max={totalFrames}
|
||||
value={currentFrame}
|
||||
onChange={(e) => setCurrentFrame(parseInt(e.target.value))}
|
||||
className="w-full absolute inset-0 opacity-0 cursor-ew-resize z-20"
|
||||
/>
|
||||
<div className="h-1 bg-white/10 w-full relative group-hover:h-2 transition-all">
|
||||
<div
|
||||
className="h-full bg-cyan-500 absolute left-0"
|
||||
style={{ width: `${(currentFrame / totalFrames) * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 bg-white rounded-full absolute top-1/2 -translate-y-1/2 -ml-1.5 shadow-sm transform scale-0 group-hover:scale-100 transition-transform shadow-cyan-500/50"
|
||||
style={{ left: `${(currentFrame / totalFrames) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex items-center px-4 gap-6">
|
||||
<div className="flex flex-col items-center gap-2 px-4 border-r border-white/10 shrink-0">
|
||||
<button
|
||||
className="p-2 rounded-full bg-white/5 text-white hover:bg-white/10"
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
>
|
||||
{isPlaying ? <Pause size={20} fill="currentColor" /> : <Play size={20} fill="currentColor" />}
|
||||
</button>
|
||||
<span className="text-[10px] font-mono text-gray-500">{isPlaying ? '暂停 (SPACE)' : '播放序列 (F5)'}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex-1 flex gap-px flex-nowrap items-center overflow-hidden justify-center"
|
||||
style={{ WebkitMaskImage: 'linear-gradient(to right, transparent, black 10%, black 90%, transparent)' }}
|
||||
>
|
||||
{frames.map((frameIdx) => {
|
||||
if (frameIdx < 1 || frameIdx > totalFrames) {
|
||||
return <div key={`empty-${frameIdx}`} className="w-16 h-12 opacity-0 shrink-0" />
|
||||
}
|
||||
const isCurrent = frameIdx === currentFrame;
|
||||
return (
|
||||
<div
|
||||
key={frameIdx}
|
||||
onClick={() => setCurrentFrame(frameIdx)}
|
||||
className={cn(
|
||||
"relative shrink-0 rounded-sm transition-all cursor-pointer flex items-center justify-center overflow-hidden group mx-0.5",
|
||||
isCurrent ? "w-28 h-16 border-2 border-cyan-500 bg-gray-700 shadow-[0_0_15px_rgba(6,182,212,0.3)] z-10" : "w-16 h-12 border border-white/5 bg-gray-800/50 opacity-40 hover:opacity-100"
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-gray-800 to-gray-900 opacity-20" />
|
||||
<span className={cn("text-[9px] font-mono text-white text-center z-10", isCurrent ? "text-[10px] font-bold" : "")}>
|
||||
{frameIdx.toString().padStart(4, '0')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="w-48 text-right shrink-0">
|
||||
<div className="text-2xl font-mono text-white">{currentFrame}<span className="text-xs text-gray-500"> / {totalFrames}</span></div>
|
||||
<div className="text-[10px] text-gray-500 uppercase tracking-widest mt-1">底层时序视频图层截帧导航轴</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user