forked from 77media/video-flow
371 lines
13 KiB
TypeScript
371 lines
13 KiB
TypeScript
'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';
|
|
|
|
interface SceneEnvironment {
|
|
time: {
|
|
period: '清晨' | '上午' | '中午' | '下午' | '傍晚' | '夜晚' | '深夜';
|
|
specific?: string;
|
|
};
|
|
location: {
|
|
main: string;
|
|
detail?: string;
|
|
};
|
|
weather: {
|
|
type: '晴天' | '多云' | '雨天' | '雪天' | '雷暴' | '阴天';
|
|
description?: string;
|
|
};
|
|
atmosphere: {
|
|
lighting: string;
|
|
mood: string;
|
|
};
|
|
}
|
|
|
|
interface SceneTabContentProps {
|
|
taskSketch: any[];
|
|
currentSketchIndex: number;
|
|
onSketchSelect: (index: number) => void;
|
|
}
|
|
|
|
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({
|
|
taskSketch = [],
|
|
currentSketchIndex = 0,
|
|
onSketchSelect
|
|
}: SceneTabContentProps) {
|
|
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
|
const scriptsRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 确保 taskSketch 是数组
|
|
const sketches = Array.isArray(taskSketch) ? taskSketch : [];
|
|
|
|
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(taskSketch[currentSketchIndex]);
|
|
|
|
// 天气图标映射
|
|
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 !== taskSketch[currentSketchIndex]?.url && !ignoreReplace) {
|
|
// 提示 场景已修改,弹出替换场景面板
|
|
if (isReplacePanelOpen) {
|
|
setReplacePanelKey(replacePanelKey + 1);
|
|
} else {
|
|
setIsReplacePanelOpen(true);
|
|
}
|
|
return;
|
|
}
|
|
onSketchSelect(index);
|
|
setCurrentScene(taskSketch[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">
|
|
<div
|
|
ref={thumbnailsRef}
|
|
className="relative flex gap-4 overflow-x-auto p-2 hide-scrollbar"
|
|
>
|
|
<div className="flex gap-4 min-w-fit">
|
|
{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'
|
|
)}
|
|
onClick={() => handleChangeScene(index)}
|
|
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>
|
|
))}
|
|
</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> */}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 脚本预览行 - 单行滚动 */}
|
|
<div className="relative group">
|
|
<div
|
|
ref={scriptsRef}
|
|
className="flex overflow-x-auto hide-scrollbar py-2 gap-1"
|
|
>
|
|
{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'
|
|
)}
|
|
onClick={() => handleChangeScene(index)}
|
|
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>
|
|
);
|
|
})}
|
|
</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-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={() => console.log('Replace')}
|
|
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>
|
|
);
|
|
}
|