forked from 77media/video-flow
294 lines
9.4 KiB
TypeScript
294 lines
9.4 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;
|
|
sketchCount: number;
|
|
onSketchSelect: (index: number) => void;
|
|
}
|
|
|
|
const MOCK_SKETCH_COUNT = 8;
|
|
|
|
export function ThumbnailGrid({
|
|
isLoading,
|
|
currentStep,
|
|
currentSketchIndex,
|
|
taskSketch,
|
|
taskVideos,
|
|
isGeneratingSketch,
|
|
sketchCount,
|
|
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 = () => (
|
|
<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-cyan-300 via-sky-400 to-blue-500"
|
|
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">场景 {sketchCount + 1}</span>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
|
|
// 渲染视频阶段的缩略图
|
|
const renderVideoThumbnails = () => (
|
|
taskSketch.map((sketch, index) => (
|
|
<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)}
|
|
>
|
|
<ProgressiveReveal
|
|
{...presets.thumbnail}
|
|
delay={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={{
|
|
...presets.thumbnail.loadingBgConfig,
|
|
glowOpacity: 0.4,
|
|
duration: 4
|
|
}}
|
|
>
|
|
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
|
{taskVideos[index] ? (
|
|
<video
|
|
className="w-full h-full object-cover"
|
|
src={taskVideos[index].url}
|
|
muted
|
|
playsInline
|
|
loop
|
|
poster={sketch.url}
|
|
/>
|
|
) : (
|
|
<img
|
|
className="w-full h-full object-cover"
|
|
src={sketch.url}
|
|
alt={`缩略图 ${index + 1}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
</ProgressiveReveal>
|
|
<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">场景 {index + 1}</span>
|
|
</div>
|
|
</div>
|
|
))
|
|
);
|
|
|
|
// 渲染分镜草图阶段的缩略图
|
|
const renderSketchThumbnails = () => (
|
|
<>
|
|
{taskSketch.map((sketch, index) => (
|
|
<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
|
|
{...presets.thumbnail}
|
|
delay={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], // cubic-bezier
|
|
opacity: { duration: 0.6, ease: "easeInOut" },
|
|
scale: { duration: 1, ease: "easeOut" },
|
|
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
|
|
}
|
|
}
|
|
}}
|
|
loadingBgConfig={{
|
|
...presets.thumbnail.loadingBgConfig,
|
|
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={`缩略图 ${index + 1}`}
|
|
/>
|
|
</div>
|
|
</ProgressiveReveal>
|
|
<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">场景 {index + 1}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && renderGeneratingThumbnail()}
|
|
</>
|
|
);
|
|
|
|
return (
|
|
<div
|
|
ref={thumbnailsRef}
|
|
className="w-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hidden-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>
|
|
);
|
|
}
|