forked from 77media/video-flow
355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
'use client';
|
||
|
||
import React, { useRef, useEffect, useState } from 'react';
|
||
import { motion } from 'framer-motion';
|
||
import { Skeleton } from '@/components/ui/skeleton';
|
||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||
|
||
interface ThumbnailGridProps {
|
||
isLoading: boolean;
|
||
currentStep: string;
|
||
currentSketchIndex: number;
|
||
taskSketch: any[];
|
||
taskVideos: any[];
|
||
isGeneratingSketch: boolean;
|
||
isGeneratingVideo: boolean;
|
||
sketchCount: number;
|
||
totalSketchCount: number;
|
||
onSketchSelect: (index: number) => void;
|
||
}
|
||
|
||
export function ThumbnailGrid({
|
||
isLoading,
|
||
currentStep,
|
||
currentSketchIndex,
|
||
taskSketch,
|
||
taskVideos,
|
||
isGeneratingSketch,
|
||
isGeneratingVideo,
|
||
sketchCount,
|
||
totalSketchCount,
|
||
onSketchSelect
|
||
}: ThumbnailGridProps) {
|
||
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
const [startX, setStartX] = useState(0);
|
||
const [scrollLeft, setScrollLeft] = useState(0);
|
||
|
||
// 监听当前选中索引变化,自动滚动到对应位置
|
||
useEffect(() => {
|
||
if (thumbnailsRef.current && taskSketch.length > 0) {
|
||
const container = thumbnailsRef.current;
|
||
const thumbnailWidth = container.offsetWidth / 4; // 每个缩略图宽度(包含间距)
|
||
const scrollPosition = currentSketchIndex * thumbnailWidth;
|
||
|
||
container.scrollTo({
|
||
left: scrollPosition,
|
||
behavior: 'smooth'
|
||
});
|
||
}
|
||
}, [currentSketchIndex, taskSketch.length]);
|
||
|
||
// 处理鼠标/触摸拖动事件
|
||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||
setIsDragging(true);
|
||
setStartX(e.pageX - thumbnailsRef.current!.offsetLeft);
|
||
setScrollLeft(thumbnailsRef.current!.scrollLeft);
|
||
};
|
||
|
||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||
if (!isDragging) return;
|
||
e.preventDefault();
|
||
const x = e.pageX - thumbnailsRef.current!.offsetLeft;
|
||
const walk = (x - startX) * 2;
|
||
thumbnailsRef.current!.scrollLeft = scrollLeft - walk;
|
||
};
|
||
|
||
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
|
||
setIsDragging(false);
|
||
if (!isDragging) return;
|
||
|
||
const container = thumbnailsRef.current!;
|
||
const thumbnailWidth = container.offsetWidth / 4;
|
||
const currentScroll = container.scrollLeft;
|
||
const nearestIndex = Math.round(currentScroll / thumbnailWidth);
|
||
|
||
// 只有在拖动距离较小时才触发选中
|
||
const x = e.pageX - container.offsetLeft;
|
||
const walk = Math.abs(x - startX);
|
||
if (walk < 10) {
|
||
return; // 如果拖动距离太小,保持原有的点击选中逻辑
|
||
}
|
||
|
||
onSketchSelect(Math.min(Math.max(0, nearestIndex), taskSketch.length - 1));
|
||
};
|
||
|
||
// 渲染加载状态
|
||
if (isLoading) {
|
||
return (
|
||
<>
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
</>
|
||
);
|
||
}
|
||
|
||
// 最终成片阶段不显示缩略图
|
||
if (Number(currentStep) === 6) {
|
||
return null;
|
||
}
|
||
|
||
// 渲染生成中的缩略图
|
||
const renderGeneratingThumbnail = () => {
|
||
const currentSketch = taskSketch[currentSketchIndex];
|
||
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
|
||
const bgColors = currentSketch?.bg_rgb || defaultBgColors;
|
||
|
||
return (
|
||
<motion.div
|
||
className="relative aspect-video rounded-lg overflow-hidden"
|
||
initial={{ opacity: 0, scale: 0.8 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
{/* 动态渐变背景 */}
|
||
<motion.div
|
||
className={`absolute inset-0 bg-gradient-to-r from-[${bgColors[0]}] via-[${bgColors[1]}] to-[${bgColors[2]}]`}
|
||
animate={{
|
||
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
||
}}
|
||
transition={{
|
||
duration: 5,
|
||
repeat: Infinity,
|
||
ease: "linear"
|
||
}}
|
||
style={{
|
||
backgroundSize: "200% 200%",
|
||
}}
|
||
/>
|
||
{/* 动态光效 */}
|
||
<motion.div
|
||
className="absolute inset-0 opacity-50"
|
||
style={{
|
||
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
||
}}
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
}}
|
||
transition={{
|
||
duration: 2,
|
||
repeat: Infinity,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
<div className="absolute inset-0 flex items-center justify-center">
|
||
<div className="relative">
|
||
<motion.div
|
||
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
rotate: [0, 180, 360],
|
||
}}
|
||
transition={{
|
||
duration: 4,
|
||
repeat: Infinity,
|
||
ease: "linear"
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||
<span className="text-xs text-white/90">Scene {sketchCount + 1}</span>
|
||
</div>
|
||
</motion.div>
|
||
);
|
||
};
|
||
|
||
// 渲染视频阶段的缩略图
|
||
const renderVideoThumbnails = () => (
|
||
taskSketch.map((sketch, index) => {
|
||
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
|
||
const bgColors = sketch?.bg_rgb || defaultBgColors;
|
||
|
||
return (
|
||
<div
|
||
key={`video-${index}`}
|
||
className={`relative aspect-video rounded-lg overflow-hidden
|
||
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||
onClick={() => !isDragging && onSketchSelect(index)}
|
||
>
|
||
{/* 底层草图,始终显示 */}
|
||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||
<img
|
||
className="w-full h-full object-cover"
|
||
src={sketch.url}
|
||
alt={`Thumbnail ${index + 1}`}
|
||
/>
|
||
</div>
|
||
|
||
{/* 视频层,只在有视频时用ProgressiveReveal动画显示 */}
|
||
{taskVideos[index] && (
|
||
<div className="absolute inset-0">
|
||
{isGeneratingVideo ? (
|
||
<ProgressiveReveal
|
||
key={`video-thumbnail-generating-${index}`}
|
||
revealDuration={0.8}
|
||
blurDuration={0.3}
|
||
initialBlur={10}
|
||
delay={index === currentSketchIndex ? 0 : index * 0.1}
|
||
customVariants={{
|
||
hidden: {
|
||
opacity: 0,
|
||
scale: 0.95,
|
||
filter: "blur(10px)"
|
||
},
|
||
visible: {
|
||
opacity: 1,
|
||
scale: 1,
|
||
filter: "blur(0px)",
|
||
transition: {
|
||
duration: 0.8,
|
||
ease: [0.23, 1, 0.32, 1],
|
||
opacity: { duration: 0.6, ease: "easeInOut" },
|
||
scale: { duration: 1, ease: "easeOut" },
|
||
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
|
||
}
|
||
}
|
||
}}
|
||
loadingBgConfig={{
|
||
fromColor: `from-[${bgColors[0]}]`,
|
||
viaColor: `via-[${bgColors[1]}]`,
|
||
toColor: `to-[${bgColors[2]}]`,
|
||
glowOpacity: 0.4,
|
||
duration: 4
|
||
}}
|
||
>
|
||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||
<video
|
||
className="w-full h-full object-cover"
|
||
src={taskVideos[index].url}
|
||
playsInline
|
||
loop
|
||
muted
|
||
poster={sketch.url}
|
||
/>
|
||
</div>
|
||
</ProgressiveReveal>
|
||
) : (
|
||
/* 生成完成后直接显示视频,不使用ProgressiveReveal */
|
||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||
<video
|
||
className="w-full h-full object-cover"
|
||
src={taskVideos[index].url}
|
||
playsInline
|
||
loop
|
||
muted
|
||
poster={sketch.url}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent z-10">
|
||
<span className="text-xs text-white/90">Scene {index + 1}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})
|
||
);
|
||
|
||
// 渲染分镜草图阶段的缩略图
|
||
const renderSketchThumbnails = () => (
|
||
<>
|
||
{taskSketch.map((sketch, index) => {
|
||
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
|
||
const bgColors = sketch?.bg_rgb || defaultBgColors;
|
||
|
||
return (
|
||
<div
|
||
key={`sketch-${index}`}
|
||
className={`relative aspect-video rounded-lg overflow-hidden
|
||
${currentSketchIndex === index ? 'ring-2 ring-blue-500 z-10' : 'hover:ring-2 hover:ring-blue-500/50'}`}
|
||
onClick={() => !isDragging && onSketchSelect(index)}
|
||
>
|
||
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
||
{(isGeneratingSketch || !sketch) ? (
|
||
<ProgressiveReveal
|
||
key={`sketch-thumbnail-generating-${index}`}
|
||
revealDuration={0.8}
|
||
blurDuration={0.3}
|
||
initialBlur={10}
|
||
delay={index === currentSketchIndex ? 0 : index * 0.1}
|
||
customVariants={{
|
||
hidden: {
|
||
opacity: 0,
|
||
scale: 0.95,
|
||
filter: "blur(10px)"
|
||
},
|
||
visible: {
|
||
opacity: 1,
|
||
scale: 1,
|
||
filter: "blur(0px)",
|
||
transition: {
|
||
duration: 0.8,
|
||
ease: [0.23, 1, 0.32, 1],
|
||
opacity: { duration: 0.6, ease: "easeInOut" },
|
||
scale: { duration: 1, ease: "easeOut" },
|
||
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
|
||
}
|
||
}
|
||
}}
|
||
loadingBgConfig={{
|
||
fromColor: `from-[${bgColors[0]}]`,
|
||
viaColor: `via-[${bgColors[1]}]`,
|
||
toColor: `to-[${bgColors[2]}]`,
|
||
glowOpacity: 0.4,
|
||
duration: 4
|
||
}}
|
||
>
|
||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||
<img
|
||
className="w-full h-full object-cover"
|
||
src={sketch.url}
|
||
alt={`Thumbnail ${index + 1}`}
|
||
/>
|
||
</div>
|
||
</ProgressiveReveal>
|
||
) : (
|
||
/* 生成完成后直接显示,不使用ProgressiveReveal */
|
||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||
<img
|
||
className="w-full h-full object-cover"
|
||
src={sketch.url}
|
||
alt={`Thumbnail ${index + 1}`}
|
||
/>
|
||
</div>
|
||
)}
|
||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||
<span className="text-xs text-white/90">Scene {index + 1}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
{isGeneratingSketch && sketchCount < totalSketchCount && renderGeneratingThumbnail()}
|
||
</>
|
||
);
|
||
|
||
return (
|
||
<div
|
||
ref={thumbnailsRef}
|
||
className="w-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing"
|
||
onMouseDown={handleMouseDown}
|
||
onMouseMove={handleMouseMove}
|
||
onMouseUp={handleMouseUp}
|
||
onMouseLeave={() => setIsDragging(false)}
|
||
>
|
||
{Number(currentStep) > 2 && Number(currentStep) < 6
|
||
? renderVideoThumbnails()
|
||
: renderSketchThumbnails()
|
||
}
|
||
</div>
|
||
);
|
||
}
|