video-flow-b/components/filmstrip-stepper.tsx
2025-06-26 20:12:55 +08:00

224 lines
7.6 KiB
TypeScript

import { useState, useRef, useEffect } from 'react';
import { motion, useAnimation, useMotionValue, useTransform, useSpring } from 'framer-motion';
import { ChevronLeft, ChevronRight, Upload, BookOpen, Brain, Users, Film, Flag } from 'lucide-react';
// 定义步骤数据结构
interface Step {
id: string;
icon: JSX.Element;
title: string;
subtitle: string;
description: string;
}
// 步骤配置
const STEPS: Step[] = [
{
id: 'overview',
icon: <BookOpen className="w-6 h-6" />,
title: '剧本大纲',
subtitle: 'Script Overview',
description: '提取剧本结构和关键要素'
},
{
id: 'storyboard',
icon: <Brain className="w-6 h-6" />,
title: '分镜草图',
subtitle: 'Storyboard',
description: '可视化场景设计和转场'
},
{
id: 'character',
icon: <Users className="w-6 h-6" />,
title: '演员角色',
subtitle: 'Character Design',
description: '定制角色形象和个性'
},
{
id: 'post',
icon: <Film className="w-6 h-6" />,
title: '后期制作',
subtitle: 'Post Production',
description: '音效配乐和特效处理'
},
{
id: 'output',
icon: <Flag className="w-6 h-6" />,
title: '最终成品',
subtitle: 'Final Output',
description: '预览和导出作品'
}
];
interface FilmstripStepperProps {
currentStep: string;
onStepChange: (stepId: string) => void;
}
export function FilmstripStepper({ currentStep, onStepChange }: FilmstripStepperProps) {
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const controls = useAnimation();
// 滚动位置状态
const x = useMotionValue(0);
const springX = useSpring(x, { stiffness: 300, damping: 30 });
// 处理滚动边界
useEffect(() => {
if (!containerRef.current || !scrollRef.current) return;
const container = containerRef.current;
const scroll = scrollRef.current;
const maxScroll = -(scroll.scrollWidth - container.clientWidth);
x.set(Math.max(Math.min(x.get(), 0), maxScroll));
}, [x]);
// 处理拖拽结束
const handleDragEnd = () => {
setIsDragging(false);
if (!containerRef.current || !scrollRef.current) return;
const container = containerRef.current;
const scroll = scrollRef.current;
const maxScroll = -(scroll.scrollWidth - container.clientWidth);
// 确保不会过度滚动
if (x.get() > 0) {
controls.start({ x: 0 });
} else if (x.get() < maxScroll) {
controls.start({ x: maxScroll });
}
};
// 滚动到指定步骤
const scrollToStep = (stepId: string) => {
if (!containerRef.current || !scrollRef.current) return;
const stepElement = document.getElementById(`step-${stepId}`);
if (!stepElement) return;
const container = containerRef.current;
const stepLeft = stepElement.offsetLeft;
const stepWidth = stepElement.offsetWidth;
const containerWidth = container.clientWidth;
const targetX = -(stepLeft - (containerWidth - stepWidth) / 2);
controls.start({ x: targetX });
};
return (
<div className="relative w-full">
{/* 滚动箭头 */}
<button
className="absolute left-4 top-1/2 -translate-y-1/2 z-10 w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 backdrop-blur-sm flex items-center justify-center transition-colors"
onClick={() => controls.start({ x: x.get() + 300 })}
>
<ChevronLeft className="w-6 h-6" />
</button>
<button
className="absolute right-4 top-1/2 -translate-y-1/2 z-10 w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 backdrop-blur-sm flex items-center justify-center transition-colors"
onClick={() => controls.start({ x: x.get() - 300 })}
>
<ChevronRight className="w-6 h-6" />
</button>
{/* 胶片容器 */}
<div
ref={containerRef}
className="w-full overflow-hidden px-20"
style={{ perspective: '1000px' }}
>
<motion.div
ref={scrollRef}
drag="x"
dragConstraints={containerRef}
dragElastic={0.1}
onDragStart={() => setIsDragging(true)}
onDragEnd={handleDragEnd}
animate={controls}
style={{ x: springX }}
className="flex gap-6 px-4 py-8"
>
{STEPS.map((step, index) => {
const isActive = currentStep === step.id;
return (
<motion.div
key={step.id}
id={`step-${step.id}`}
className={`
relative flex-shrink-0 w-64 h-40 rounded-lg cursor-pointer
${isActive ? 'z-10' : 'opacity-70'}
`}
whileHover={{ scale: 1.05, y: -5 }}
whileTap={{ scale: 0.95 }}
animate={isActive ? {
scale: 1.1,
y: -10,
transition: { type: 'spring', stiffness: 300, damping: 25 }
} : {
scale: 1,
y: 0
}}
onClick={() => {
if (!isDragging) {
onStepChange(step.id);
scrollToStep(step.id);
}
}}
>
{/* 胶片打孔效果 */}
<div className="absolute -left-2 top-1/2 -translate-y-1/2 space-y-4">
<div className="w-4 h-4 rounded-full bg-black/20 backdrop-blur-sm border border-white/10" />
<div className="w-4 h-4 rounded-full bg-black/20 backdrop-blur-sm border border-white/10" />
</div>
<div className="absolute -right-2 top-1/2 -translate-y-1/2 space-y-4">
<div className="w-4 h-4 rounded-full bg-black/20 backdrop-blur-sm border border-white/10" />
<div className="w-4 h-4 rounded-full bg-black/20 backdrop-blur-sm border border-white/10" />
</div>
{/* 卡片内容 */}
<div
className={`
relative w-full h-full rounded-lg p-4 overflow-hidden
bg-gradient-to-br from-white/10 to-white/5
${isActive ? 'ring-2 ring-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.5)]' : ''}
`}
>
<div className="absolute top-0 left-0 w-full h-full bg-black/20 backdrop-blur-[2px]" />
<div className="relative z-10 flex flex-col h-full">
<div className="flex items-center gap-3 mb-2">
<div className={`
p-2 rounded-lg
${isActive ? 'bg-blue-500/20 text-blue-400' : 'bg-white/10 text-white/60'}
`}>
{step.icon}
</div>
<div>
<h3 className="text-sm font-medium">{step.title}</h3>
<p className="text-xs text-white/50">{step.subtitle}</p>
</div>
</div>
<p className="text-sm text-white/70">{step.description}</p>
</div>
</div>
{/* 步骤序号 */}
<div className={`
absolute -top-3 -right-3 w-8 h-8 rounded-full
flex items-center justify-center text-sm font-medium
${isActive ? 'bg-blue-500 text-white' : 'bg-white/10 text-white/60'}
`}>
{index + 1}
</div>
</motion.div>
);
})}
</motion.div>
</div>
</div>
);
}