video-flow-b/components/ui/scene-tab-content.tsx
2025-08-16 13:31:45 +08:00

370 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, AnimatePresence } from 'framer-motion';
import { Trash2, RefreshCw, Sun, Moon, Cloud, CloudRain, CloudSnow, CloudLightning, Sparkles, Clock, MapPin, Palette, Check, Plus, ReplaceAll } from 'lucide-react';
import { cn } from '@/public/lib/utils';
import SceneEditor from './scene-editor';
import FloatingGlassPanel from './FloatingGlassPanel';
import { ReplaceScenePanel, mockShots } from './replace-scene-panel';
import HorizontalScroller from './HorizontalScroller';
interface SceneEnvironment {
time: {
period: '' | '' | '' | '' | '' | '' | '';
specific?: string;
};
location: {
main: string;
detail?: string;
};
weather: {
type: '' | '' | '' | '' | '' | '';
description?: string;
};
atmosphere: {
lighting: string;
mood: string;
};
}
interface SceneTabContentProps {
currentSketchIndex: number;
}
interface SceneSketch {
id: string;
script: string;
environment: SceneEnvironment;
}
// Mock 数据
const mockSketch: SceneSketch = {
id: '1',
script: '线绿',
environment: {
time: {
period: '',
specific: '2'
},
location: {
main: '',
detail: ''
},
weather: {
type: '',
description: ''
},
atmosphere: {
lighting: '线',
mood: ''
}
}
};
export function SceneTabContent({
currentSketchIndex = 0
}: SceneTabContentProps) {
const thumbnailsRef = useRef<HTMLDivElement>(null);
const scriptsRef = useRef<HTMLDivElement>(null);
// 确保 taskSketch 是数组
const sketches: any[] = [];
const [localSketch, setLocalSketch] = useState(mockSketch);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
const [replacePanelKey, setReplacePanelKey] = useState(0);
const [ignoreReplace, setIgnoreReplace] = useState(false);
const [currentScene, setCurrentScene] = useState({
url: '',
script: ''
});
// 天气图标映射
const weatherIcons = {
'': Sun,
'': Cloud,
'': CloudRain,
'': CloudSnow,
'': CloudLightning,
'': Cloud
};
// 时间选项
const timeOptions = ['', '', '', '', '', '', ''];
// 处理脚本更新
const handleScriptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setLocalSketch({
...localSketch,
script: e.target.value
});
// 自动调整文本框高度
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px';
}
};
// 处理智能优化
const handleSmartOptimize = () => {
console.log('Optimizing scene description...');
// TODO: 调用 AI 优化接口
};
// 自动滚动到选中项
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]);
const handleReplaceScene = (url: string) => {
// setCurrentScene({
// ...currentScene,
// url: url
// });
// setIsReplacePanelOpen(true);
};
const handleConfirmReplace = (selectedShots: string[], addToLibrary: boolean) => {
// 处理替换确认逻辑
console.log('Selected shots:', selectedShots);
console.log('Add to library:', addToLibrary);
setIsReplacePanelOpen(false);
};
const handleCloseReplacePanel = () => {
setIsReplacePanelOpen(false);
setIgnoreReplace(true);
};
const handleChangeScene = (index: number) => {
// if (currentScene?.url !== sketches[currentSketchIndex]?.url && !ignoreReplace) {
// // 提示 场景已修改,弹出替换场景面板
// if (isReplacePanelOpen) {
// setReplacePanelKey(replacePanelKey + 1);
// } else {
// setIsReplacePanelOpen(true);
// }
// return;
// }
// setCurrentScene(sketches[index]);
};
// 如果没有数据,显示空状态
if (sketches.length === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<p>No scene data</p>
</div>
);
}
return (
<div className="flex flex-col gap-2">
{/* 上部分 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{/* 分镜缩略图行 */}
<div className="relative">
<HorizontalScroller
itemWidth={128}
gap={0}
selectedIndex={currentSketchIndex}
onItemClick={(i: number) => handleChangeScene(i)}
>
{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 group',
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<img
src={sketch.url}
alt={`Sketch ${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">Scene {index + 1}</span>
</div>
{/* 鼠标悬浮/移出 显示/隐藏 删除图标 */}
{/* <div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<button
onClick={(e) => {
e.stopPropagation();
console.log('Delete sketch');
}}
className="text-red-500"
>
<Trash2 className="w-4 h-4" />
</button>
</div> */}
</motion.div>
))}
{/* 新增占位符 */}
{/* <motion.div
className={cn(
'relative flex-shrink-0 w-32 aspect-video rounded-lg cursor-pointer',
'bg-white/5 hover:bg-white/10 transition-colors',
'flex items-center justify-center',
'border-2 border-dashed border-white/20 hover:border-white/30'
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => console.log('Add new sketch')}
>
<div className="flex flex-col items-center gap-2 text-white/50">
<Plus className="w-6 h-6" />
<span className="text-xs">添加场景</span>
</div>
</motion.div> */}
</HorizontalScroller>
</div>
{/* 脚本预览行 - 单行滚动 */}
<div className="relative group">
<HorizontalScroller
itemWidth={'auto'}
gap={0}
selectedIndex={currentSketchIndex}
onItemClick={(i: number) => handleChangeScene(i)}
>
{sketches.map((script, index) => {
const isActive = currentSketchIndex === index;
return (
<motion.div
key={index}
className={cn(
'flex-shrink-0 cursor-pointer transition-all duration-300',
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
)}
initial={false}
animate={{
scale: isActive ? 1.02 : 1,
}}
>
<div className="flex items-center gap-2">
<span className="text-sm whitespace-nowrap">
{script.script}
</span>
{index < sketches.length - 1 && (
<span className="text-white/20">|</span>
)}
</div>
</motion.div>
);
})}
</HorizontalScroller>
{/* 渐变遮罩 */}
<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-4">
<motion.div
className="aspect-video rounded-lg overflow-hidden relative"
layoutId={`sketch-preview-${currentSketchIndex}`}
>
<img
src={currentScene?.url || sketches[currentSketchIndex]?.url}
alt={`Scene ${currentSketchIndex + 1}`}
className="w-full h-full object-cover"
/>
</motion.div>
</div>
{/* 右列:脚本编辑器 */}
<div className="space-y-4">
<SceneEditor
initialDescription={localSketch.script}
onDescriptionChange={(description) => {
setLocalSketch({
...localSketch,
script: description
});
}}
onReplaceScene={handleReplaceScene}
className="min-h-[200px]"
/>
{/* 重新生成按钮、替换形象按钮 */}
<div className="grid grid-cols-2 gap-2">
<motion.button
onClick={() => handleReplaceScene('https://c.huiying.video/images/5740cb7c-6e08-478f-9e7c-bca7f78a2bf6.jpg')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-pink-500/10 hover:bg-pink-500/20
text-pink-500 rounded-lg transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<ReplaceAll className="w-4 h-4" />
<span>Replace</span>
</motion.button>
<motion.button
onClick={() => console.log('Regenerate')}
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"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<RefreshCw className="w-4 h-4" />
<span>Regenerate</span>
</motion.button>
</div>
</div>
</motion.div>
{/* 替换场景面板 */}
<FloatingGlassPanel open={isReplacePanelOpen} width='500px' r_key={replacePanelKey}>
<ReplaceScenePanel
shots={mockShots}
scene={{
id: currentSketchIndex.toString(),
name: `场景 ${currentSketchIndex + 1}`,
avatarUrl: currentScene?.url || sketches[currentSketchIndex]?.url
}}
onClose={handleCloseReplacePanel}
onConfirm={handleConfirmReplace}
/>
</FloatingGlassPanel>
</div>
);
}