video-flow-b/components/pages/work-flow/thumbnail-grid.tsx

322 lines
13 KiB
TypeScript

'use client';
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { motion } from 'framer-motion';
import { Skeleton } from '@/components/ui/skeleton';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { Loader2, X, SquareUserRound, MapPinHouse, Clapperboard, Video, RotateCcw } from 'lucide-react';
import { TaskObject } from '@/api/DTO/movieEdit';
interface ThumbnailGridProps {
isDisabledFocus: boolean;
taskObject: TaskObject;
currentSketchIndex: number;
onSketchSelect: (index: number) => void;
onRetryVideo: (video_id: string) => void;
}
export function ThumbnailGrid({
isDisabledFocus,
taskObject,
currentSketchIndex,
onSketchSelect,
onRetryVideo
}: ThumbnailGridProps) {
const thumbnailsRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [isFocused, setIsFocused] = useState(false);
// 监听当前选中索引变化,自动滚动到对应位置
useEffect(() => {
if (thumbnailsRef.current) {
const container = thumbnailsRef.current;
const thumbnails = container.children;
if (currentSketchIndex >= 0 && currentSketchIndex < thumbnails.length) {
const thumbnail = thumbnails[currentSketchIndex] as HTMLElement;
const containerLeft = container.getBoundingClientRect().left;
const thumbnailLeft = thumbnail.getBoundingClientRect().left;
const scrollPosition = container.scrollLeft + (thumbnailLeft - containerLeft);
container.scrollTo({
left: scrollPosition,
behavior: 'smooth'
});
}
}
}, [currentSketchIndex]);
// 获取当前阶段的数据数组
const getCurrentData = useCallback(() => {
if (taskObject.currentStage === 'video') {
return taskObject.videos.data;
} else if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
// 为 roles 和 scenes 数据添加唯一标识前缀,避免重复
const rolesWithPrefix = taskObject.roles.data.map((role, index) => ({
...role,
uniqueId: `role_${index}`
}));
const scenesWithPrefix = taskObject.scenes.data.map((scene, index) => ({
...scene,
uniqueId: `scene_${index}`
}));
return [...rolesWithPrefix, ...scenesWithPrefix];
}
return [];
}, [taskObject.currentStage, taskObject.videos.data, taskObject.roles.data, taskObject.scenes.data]);
// 使用 useRef 存储前一次的数据,避免触发重渲染
const prevDataRef = useRef<any[]>([]);
useEffect(() => {
const currentData = getCurrentData();
if (currentData && currentData.length > 0) {
const currentDataStr = JSON.stringify(currentData);
const prevDataStr = JSON.stringify(prevDataRef.current);
// 只有当数据真正发生变化时才进行处理
if (currentDataStr !== prevDataStr) {
// 找到最新更新的数据项的索引
const changedIndex = currentData.findIndex((item, index) => {
// 检查是否是新增的数据
if (index >= prevDataRef.current.length) return true;
// 检查数据是否发生变化(包括状态变化)
return JSON.stringify(item) !== JSON.stringify(prevDataRef.current[index]);
});
console.log('changedIndex_thumbnail-grid', changedIndex, 'currentData:', currentData, 'prevData:', prevDataRef.current);
// 如果找到变化的项,自动选择该项
if (changedIndex !== -1) {
onSketchSelect(changedIndex);
}
// 更新前一次的数据快照
prevDataRef.current = JSON.parse(JSON.stringify(currentData));
}
}
}, [taskObject, getCurrentData, onSketchSelect]);
// 处理键盘左右键事件
const handleKeyDown = useCallback((e: KeyboardEvent) => {
const currentData = getCurrentData();
const maxIndex = currentData.length - 1;
console.log('handleKeyDown', maxIndex, 'isFocused:', isFocused);
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && maxIndex >= 0) {
e.preventDefault();
let newIndex = currentSketchIndex;
if (e.key === 'ArrowLeft') {
// 向左循环
newIndex = currentSketchIndex === 0 ? maxIndex : currentSketchIndex - 1;
} else {
// 向右循环
newIndex = currentSketchIndex === maxIndex ? 0 : currentSketchIndex + 1;
}
console.log('切换索引:', currentSketchIndex, '->', newIndex, '最大索引:', maxIndex);
onSketchSelect(newIndex);
}
}, [isFocused, currentSketchIndex, onSketchSelect, getCurrentData]);
// 监听键盘事件
useEffect(() => {
// 组件挂载时自动聚焦
if (thumbnailsRef.current && !isDisabledFocus) {
window.addEventListener('keydown', handleKeyDown);
}
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown, isDisabledFocus]);
// 处理鼠标/触摸拖动事件
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
// 阻止默认的拖拽行为
e.preventDefault();
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;
};
// 监听阶段变化
useEffect(() => {
console.log('taskObject.currentStage_thumbnail-grid', taskObject.currentStage);
}, [taskObject.currentStage]);
// 粗剪/精剪最终成片阶段不显示缩略图
if (taskObject.currentStage === 'final_video') {
return null;
}
// 渲染视频阶段的缩略图
const renderVideoThumbnails = () => (
taskObject.videos.data.map((video, index) => {
const urls: string = video.urls ? video.urls.join(',') : '';
return (
<div
key={`video-${urls}-${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="relative w-full h-full transform hover:scale-105 transition-transform duration-500">
{taskObject.videos.data[index].video_status === 0 && (
<div className="absolute inset-0 bg-black/10 flex items-center justify-center z-20">
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
<Loader2 className="w-10 h-10 animate-spin" />
</div>
</div>
)}
{taskObject.videos.data[index].video_status === 2 && (
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
<div className="text-[#813b9dcc] text-xl font-bold flex items-center gap-2">
<RotateCcw className="w-10 h-10 cursor-pointer" />
</div>
</div>
)}
{taskObject.videos.data[index].urls && taskObject.videos.data[index].urls.length > 0 ? (
// <video
// className="w-full h-full object-cover"
// src={taskObject.videos.data[index].urls[0]}
// playsInline
// loop
// muted
// />
<img
className="w-full h-full object-cover"
src={`${taskObject.videos.data[index].urls[0]}?x-oss-process=video/snapshot,t_1000,f_jpg`}
draggable="false"
/>
) : (
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
</div>
)}
</div>
<div className='absolute bottom-0 left-0 right-0 p-2'>
<div className="inline-flex items-center px-2 py-1 rounded-full bg-green-500/20 backdrop-blur-sm">
<Video className="w-3 h-3 text-green-400 mr-1" />
<span className="text-xs text-green-400">Shot {index + 1}</span>
</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 = (sketchData: any[]) => (
<>
{sketchData.map((sketch, index) => {
return (
<div
key={sketch.uniqueId || `sketch-${sketch.url}-${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)}
>
{/* 状态 */}
{sketch.status === 0 && (
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
<Loader2 className="w-10 h-10 animate-spin" />
</div>
</div>
)}
{sketch.status === 2 && (
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
<X className="w-10 h-10" />
</div>
</div>
)}
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
{(sketch.status === 1) && (
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
<img
className="w-full h-full object-cover select-none"
src={sketch.url}
draggable="false"
/>
</div>
)}
<div className='absolute bottom-0 left-0 right-0 p-2'>
{/* 角色类型 */}
{sketch.type === 'role' && (
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
<SquareUserRound className="w-3 h-3 text-purple-400 mr-1" />
<span className="text-xs text-purple-400">Role</span>
</div>
)}
{/* 场景类型 */}
{sketch.type === 'scene' && (
<div className="inline-flex items-center px-2 py-1 rounded-full bg-purple-500/20 backdrop-blur-sm">
<MapPinHouse className="w-3 h-3 text-purple-400 mr-1" />
<span className="text-xs text-purple-400">Scene</span>
</div>
)}
{/* 分镜类型 */}
{(!sketch.type || sketch.type === 'shot_sketch') && (
<div className="inline-flex items-center px-2 py-1 rounded-full bg-cyan-500/20 backdrop-blur-sm">
<Clapperboard className="w-3 h-3 text-cyan-400 mr-1" />
<span className="text-xs text-cyan-400">Shot {index + 1}</span>
</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">{sketch.type === 'role' ? 'Role' : (sketch.type === 'scene' ? 'Scene' : 'Shot')} {index + 1}</span>
</div> */}
</div>
);
})}
</>
);
return (
<div
ref={thumbnailsRef}
tabIndex={0}
className="w-full h-full grid grid-flow-col auto-cols-[20%] gap-2 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none"
autoFocus
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={() => setIsDragging(false)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
>
{taskObject.currentStage === 'video' && renderVideoThumbnails()}
{(taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') && renderSketchThumbnails(getCurrentData())}
</div>
);
}