video-flow-b/components/ui/script-tab-content.tsx
2025-06-28 10:09:28 +08:00

202 lines
7.4 KiB
TypeScript

'use client';
import React, { useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Trash2, RefreshCw } from 'lucide-react';
import { GlassIconButton } from './glass-icon-button';
import { cn } from '@/lib/utils';
interface ScriptTabContentProps {
taskSketch: any[];
currentSketchIndex: number;
onSketchSelect: (index: number) => void;
}
export function ScriptTabContent({
taskSketch = [],
currentSketchIndex = 0,
onSketchSelect
}: ScriptTabContentProps) {
const thumbnailsRef = useRef<HTMLDivElement>(null);
const scriptsRef = useRef<HTMLDivElement>(null);
// 确保 taskSketch 是数组
const sketches = Array.isArray(taskSketch) ? taskSketch : [];
// 模拟脚本数据
const mockScripts = sketches.map((_, index) => ({
id: `script-${index}`,
content: `这是第 ${index + 1} 个分镜的脚本内容,描述了场景中的主要动作和对话。这里可以添加更多细节来丰富场景表现。`
}));
// 自动滚动到选中项
useEffect(() => {
if (thumbnailsRef.current && scriptsRef.current) {
const thumbnailContainer = thumbnailsRef.current;
const scriptContainer = scriptsRef.current;
// 计算缩略图滚动位置
const thumbnailWidth = thumbnailContainer.children[0]?.clientWidth ?? 0;
const thumbnailGap = 16; // gap-4 = 16px
const thumbnailScrollPosition = (thumbnailWidth + thumbnailGap) * currentSketchIndex;
// 计算脚本文字滚动位置
const scriptElement = scriptContainer.children[currentSketchIndex] as HTMLElement;
const scriptScrollPosition = scriptElement?.offsetLeft ?? 0;
// 平滑滚动到目标位置
thumbnailContainer.scrollTo({
left: thumbnailScrollPosition - thumbnailContainer.clientWidth / 2 + thumbnailWidth / 2,
behavior: 'smooth'
});
scriptContainer.scrollTo({
left: scriptScrollPosition - scriptContainer.clientWidth / 2 + scriptElement?.clientWidth / 2,
behavior: 'smooth'
});
}
}, [currentSketchIndex]);
// 如果没有数据,显示空状态
if (sketches.length === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<p></p>
</div>
);
}
return (
<div className="flex flex-col gap-6">
{/* 上部分 */}
<motion.div
className="space-y-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{/* 分镜缩略图行 */}
<div className="relative">
<div
ref={thumbnailsRef}
className="flex gap-4 overflow-x-auto pb-2 pt-2 hide-scrollbar"
>
{sketches.map((sketch, index) => (
<motion.div
key={sketch.id || index}
className={cn(
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer',
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
)}
onClick={() => onSketchSelect(index)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<img
src={sketch.url}
alt={`分镜 ${index + 1}`}
className="w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90"> {index + 1}</span>
</div>
</motion.div>
))}
</div>
</div>
{/* 脚本预览行 - 单行滚动 */}
<div className="relative group">
<div
ref={scriptsRef}
className="flex overflow-x-auto hide-scrollbar py-2 gap-1"
>
{mockScripts.map((script, index) => {
const isActive = currentSketchIndex === index;
return (
<motion.div
key={script.id}
className={cn(
'flex-shrink-0 cursor-pointer transition-all duration-300',
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
)}
onClick={() => onSketchSelect(index)}
initial={false}
animate={{
scale: isActive ? 1.02 : 1,
}}
>
<div className="flex items-center gap-2">
<span className="text-sm whitespace-nowrap">
{script.content}
</span>
{index < mockScripts.length - 1 && (
<span className="text-white/20">|</span>
)}
</div>
</motion.div>
);
})}
</div>
{/* 渐变遮罩 */}
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</motion.div>
{/* 下部分 */}
<motion.div
className="grid grid-cols-2 gap-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
{/* 左列:脚本编辑器 */}
<div className="space-y-2">
<motion.textarea
className="w-full h-full p-4 rounded-lg bg-white/5 backdrop-blur-sm border border-white/10
text-white/90 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
value={mockScripts[currentSketchIndex]?.content}
onChange={() => {}}
layoutId="script-editor"
/>
</div>
{/* 右列:预览和操作 */}
<div className="space-y-4">
{/* 选中的分镜预览 */}
<motion.div
className="aspect-video rounded-lg overflow-hidden"
layoutId={`sketch-preview-${currentSketchIndex}`}
>
<img
src={sketches[currentSketchIndex]?.url}
alt={`分镜 ${currentSketchIndex + 1}`}
className="w-full h-full object-cover"
/>
</motion.div>
{/* 操作按钮 */}
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => console.log('删除分镜')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-red-500/10 hover:bg-red-500/20
text-red-500 rounded-lg transition-colors"
>
<Trash2 className="w-4 h-4" />
<span></span>
</button>
<button
onClick={() => console.log('重新生成')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
text-blue-500 rounded-lg transition-colors"
>
<RefreshCw className="w-4 h-4" />
<span></span>
</button>
</div>
</div>
</motion.div>
</div>
);
}