forked from 77media/video-flow
129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
import { Scene } from '../pages/script-overview';
|
|
import Image from 'next/image';
|
|
|
|
interface SceneFilmstripProps {
|
|
scenes: Scene[];
|
|
selectedSceneId?: string;
|
|
onSceneSelect: (sceneId: string) => void;
|
|
}
|
|
|
|
export function SceneFilmstrip({
|
|
scenes,
|
|
selectedSceneId,
|
|
onSceneSelect
|
|
}: SceneFilmstripProps) {
|
|
const [selectedSceneIdState, setSelectedSceneIdState] = useState<string | null>(null);
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 处理滚动
|
|
const handleScroll = (direction: 'left' | 'right') => {
|
|
if (!scrollContainerRef.current) return;
|
|
|
|
const scrollAmount = 300;
|
|
const container = scrollContainerRef.current;
|
|
|
|
container.scrollTo({
|
|
left: container.scrollLeft + (direction === 'left' ? -scrollAmount : scrollAmount),
|
|
behavior: 'smooth'
|
|
});
|
|
};
|
|
|
|
// 处理场景选择
|
|
const handleSceneSelect = (sceneId: string) => {
|
|
setSelectedSceneIdState(sceneId);
|
|
onSceneSelect(sceneId);
|
|
|
|
// 滚动到对应的场景卡片
|
|
const sceneCard = document.getElementById(`scene-card-${sceneId}`);
|
|
if (sceneCard) {
|
|
sceneCard.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center',
|
|
inline: 'center'
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="relative">
|
|
{/* 滚动按钮 */}
|
|
<button
|
|
onClick={() => handleScroll('left')}
|
|
className="absolute left-4 top-1/2 -translate-y-1/2 z-10 w-10 h-10 rounded-full
|
|
bg-black/50 hover:bg-black/70 backdrop-blur-sm flex items-center justify-center
|
|
transition-colors"
|
|
>
|
|
<ChevronLeft className="w-6 h-6" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleScroll('right')}
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 z-10 w-10 h-10 rounded-full
|
|
bg-black/50 hover:bg-black/70 backdrop-blur-sm flex items-center justify-center
|
|
transition-colors"
|
|
>
|
|
<ChevronRight className="w-6 h-6" />
|
|
</button>
|
|
|
|
{/* 胶片打孔效果 */}
|
|
<div className="absolute -top-2 left-0 right-0 flex justify-between px-4">
|
|
{Array.from({ length: 20 }).map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="w-3 h-3 rounded-full bg-black/50 border border-white/10"
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="absolute -bottom-2 left-0 right-0 flex justify-between px-4">
|
|
{Array.from({ length: 20 }).map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="w-3 h-3 rounded-full bg-black/50 border border-white/10"
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* 场景缩略图 */}
|
|
<div
|
|
ref={scrollContainerRef}
|
|
className="flex gap-4 overflow-x-auto py-4 px-2 hide-scrollbar"
|
|
style={{ perspective: '1000px' }}
|
|
>
|
|
{scenes.map((scene, index) => (
|
|
<motion.div
|
|
key={scene.id}
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
animate={{
|
|
scale: selectedSceneId === scene.id ? 1.1 : 1,
|
|
opacity: selectedSceneId === scene.id ? 1 : 0.7,
|
|
}}
|
|
onClick={() => handleSceneSelect(scene.id)}
|
|
className={`
|
|
relative flex-shrink-0 w-32 h-20 rounded-lg overflow-hidden cursor-pointer
|
|
${selectedSceneId === scene.id ? 'ring-2 ring-purple-500' : 'hover:ring-1 hover:ring-white/20'}
|
|
transition-shadow duration-200
|
|
`}
|
|
>
|
|
<Image
|
|
src={scene.imageUrl}
|
|
alt={scene.name}
|
|
fill
|
|
className="object-cover"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
|
<div className="absolute bottom-1 left-2 right-2 text-xs font-medium truncate">
|
|
{scene.name}
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
|
|
{/* 胶片装饰线 */}
|
|
<div className="absolute top-0 left-16 right-16 h-1 bg-white/5" />
|
|
<div className="absolute bottom-0 left-16 right-16 h-1 bg-white/5" />
|
|
</div>
|
|
);
|
|
}
|