video-flow-b/components/pages/work-flow/thumbnail-grid.tsx
2025-07-07 23:06:40 +08:00

359 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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;
isPlaying: 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,
isPlaying,
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 transition-all duration-300 ${
(!taskVideos[index] && !isPlaying) ? 'filter blur-sm opacity-60' : ''
}`}
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>
);
}