forked from 77media/video-flow
1031 lines
54 KiB
TypeScript
1031 lines
54 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useRef } from 'react';
|
||
import { Card } from '@/components/ui/card';
|
||
import { Button } from '@/components/ui/button';
|
||
import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw } from 'lucide-react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import './style/create-to-video.css';
|
||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
|
||
// 添加自定义滚动条样式
|
||
const scrollbarStyles = `
|
||
.custom-scrollbar::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
.custom-scrollbar::-webkit-scrollbar-track {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 2px;
|
||
}
|
||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 2px;
|
||
}
|
||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
`;
|
||
|
||
interface SceneVideo {
|
||
id: number;
|
||
video_url: string;
|
||
script: any;
|
||
}
|
||
|
||
export function CreateToVideo() {
|
||
const router = useRouter();
|
||
const [isUploading, setIsUploading] = useState(false);
|
||
const [isExpanded, setIsExpanded] = useState(false);
|
||
const [videoUrl, setVideoUrl] = useState('');
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [loadingText, setLoadingText] = useState('Generating...');
|
||
const [isPaused, setIsPaused] = useState(false);
|
||
const [showMoreSettings, setShowMoreSettings] = useState(false);
|
||
const [generateObj, setGenerateObj] = useState<any>({
|
||
scripts: null,
|
||
frame_urls: null,
|
||
video_info: null,
|
||
scene_videos: null,
|
||
cut_video_url: null,
|
||
audio_video_url: null,
|
||
final_video_url: null
|
||
});
|
||
const [showAllFrames, setShowAllFrames] = useState(false);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const [showScrollNav, setShowScrollNav] = useState(false);
|
||
const [selectedVideoIndex, setSelectedVideoIndex] = useState<number | null>(null);
|
||
const videosContainerRef = useRef<HTMLDivElement>(null);
|
||
const scriptsContainerRef = useRef<HTMLDivElement>(null);
|
||
const [activeTab, setActiveTab] = useState('clone');
|
||
const [editingField, setEditingField] = useState<{
|
||
type: 'shot' | 'frame' | 'atmosphere' | null;
|
||
value: string;
|
||
}>({ type: null, value: '' });
|
||
const [alternativeVideos, setAlternativeVideos] = useState<{ [key: number]: string[] }>({
|
||
0: [
|
||
'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4',
|
||
'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
|
||
],
|
||
1: [
|
||
'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4',
|
||
'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4',
|
||
]
|
||
});
|
||
const [currentVideoIndex, setCurrentVideoIndex] = useState<{ [key: number]: number }>({});
|
||
const [volume, setVolume] = useState<{ [key: number]: number }>({});
|
||
const [transition, setTransition] = useState<{ [key: number]: string }>({});
|
||
const [buttonPosition, setButtonPosition] = useState({ x: window.innerWidth - 100, y: window.innerHeight / 2 });
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||
const buttonRef = useRef<HTMLDivElement>(null);
|
||
|
||
// 计算每行可以显示的图片数量(基于图片高度100px和容器宽度)
|
||
const imagesPerRow = Math.floor(1080 / (100 * 16/9 + 8)); // 假设图片宽高比16:9,间距8px
|
||
// 计算三行可以显示的最大图片数量
|
||
const maxVisibleImages = imagesPerRow * 3;
|
||
|
||
// 生成分镜视频后默认选中第一个
|
||
useEffect(() => {
|
||
if (generateObj.scene_videos?.length > 0 && selectedVideoIndex === null) {
|
||
setSelectedVideoIndex(0);
|
||
}
|
||
}, [generateObj.scene_videos]);
|
||
|
||
// 处理暂停/继续
|
||
const handlePauseResume = () => {
|
||
setIsPaused(!isPaused);
|
||
// TODO: 实现具体的暂停/继续逻辑
|
||
|
||
};
|
||
|
||
const handleUploadVideo = () => {
|
||
console.log('upload video');
|
||
// 打开文件选择器
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = 'video/*';
|
||
input.onchange = (e) => {
|
||
const file = (e.target as HTMLInputElement).files?.[0];
|
||
if (file) {
|
||
setVideoUrl(URL.createObjectURL(file));
|
||
}
|
||
}
|
||
input.click();
|
||
}
|
||
|
||
const generateSences = async () => {
|
||
try {
|
||
generateObj.scene_videos = [];
|
||
const videoUrls = [
|
||
'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4',
|
||
'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
|
||
'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4',
|
||
'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4',
|
||
'https://cdn.qikongjian.com/videos/1750303377_8c3c4ca6-c4ea-4376-8583-de3afa5681d8_text_to_video_0.mp4',
|
||
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4'
|
||
];
|
||
setGenerateObj({...generateObj, scene_videos: []});
|
||
|
||
// 使用 Promise.all 和 Array.map 来处理异步操作
|
||
const promises = generateObj.scripts.map((element: any, index: number) => {
|
||
return new Promise<void>((resolveVideo) => {
|
||
setTimeout(() => {
|
||
generateObj.scene_videos.push({
|
||
id: index,
|
||
video_url: videoUrls[index],
|
||
script: element
|
||
});
|
||
setGenerateObj({...generateObj});
|
||
setLoadingText(`生成第 ${index + 1} 个分镜视频...`);
|
||
resolveVideo();
|
||
}, index * 3000); // 每个视频间隔3秒
|
||
});
|
||
});
|
||
|
||
// 等待所有视频生成完成
|
||
await Promise.all(promises);
|
||
} catch (error) {
|
||
console.error('生成分镜视频失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
const handleCreateVideo = async () => {
|
||
try {
|
||
// 清空所有数据
|
||
setGenerateObj({
|
||
scripts: null,
|
||
frame_urls: null,
|
||
video_info: null,
|
||
scene_videos: null,
|
||
cut_video_url: null,
|
||
audio_video_url: null,
|
||
final_video_url: null
|
||
});
|
||
console.log('create video');
|
||
setIsLoading(true);
|
||
setIsExpanded(true);
|
||
|
||
// 提取帧
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
setLoadingText('提取帧...');
|
||
|
||
// 生成帧
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
generateObj.frame_urls = [
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000001.jpg/1750507511_tmphfb431oc_000001.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000002.jpg/1750507511_tmphfb431oc_000002.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000003.jpg/1750507511_tmphfb431oc_000003.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000004.jpg/1750507512_tmphfb431oc_000004.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000005.jpg/1750507512_tmphfb431oc_000005.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000006.jpg/1750507513_tmphfb431oc_000006.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000007.jpg/1750507514_tmphfb431oc_000007.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000008.jpg/1750507515_tmphfb431oc_000008.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000009.jpg/1750507515_tmphfb431oc_000009.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000010.jpg/1750507516_tmphfb431oc_000010.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000011.jpg/1750507516_tmphfb431oc_000011.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000012.jpg/1750507516_tmphfb431oc_000012.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000013.jpg/1750507517_tmphfb431oc_000013.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000014.jpg/1750507517_tmphfb431oc_000014.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000015.jpg/1750507517_tmphfb431oc_000015.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000016.jpg/1750507517_tmphfb431oc_000016.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000017.jpg/1750507517_tmphfb431oc_000017.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000018.jpg/1750507518_tmphfb431oc_000018.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000019.jpg/1750507520_tmphfb431oc_000019.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000020.jpg/1750507521_tmphfb431oc_000020.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000021.jpg/1750507523_tmphfb431oc_000021.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000022.jpg/1750507523_tmphfb431oc_000022.jpg",
|
||
"https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000023.jpg/1750507524_tmphfb431oc_000023.jpg",
|
||
];
|
||
setGenerateObj({...generateObj});
|
||
setLoadingText('分析视频...');
|
||
|
||
// 生成视频信息
|
||
await new Promise(resolve => setTimeout(resolve, 6000));
|
||
generateObj.video_info = {
|
||
roles: [
|
||
{
|
||
name: '雪 (YUKI)',
|
||
core_identity: '一位接近二十岁或二十出头的年轻女性,东亚裔,拥有深色长发和刘海,五官柔和且富有表现力。',
|
||
avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg',
|
||
},{
|
||
name: '春 (HARU)',
|
||
core_identity: '一位接近二十岁或二十出头的年轻男性,东亚裔,拥有深色、发型整洁的中长发和深思的气质。',
|
||
avatar: 'https://smart-video-ai.oss-cn-beijing.aliyuncs.com/frames/d877fa43-4856-4acb-9a3b-627c28275343/frame_000000.jpg/1750507510_tmphfb431oc_000000.jpg',
|
||
}
|
||
],
|
||
sence: '叙事在两个不同的时间段展开。现在时空设定在一个安静的乡下小镇,冬季时被大雪覆盖。记忆则设定在春夏两季,一个日本高中的校园内外,天气温暖而晴朗。',
|
||
style: '电影感,照片般逼真,带有柔和、梦幻般的质感。其美学风格让人联想到日本的浪漫剧情片。现在时空的场景使用冷色、蓝色调的调色板,而记忆序列则沐浴在温暖的黄金时刻光晕中。大量使用浅景深、微妙的镜头光晕,以及平滑、富有情感的节奏。8K分辨率。'
|
||
};
|
||
setGenerateObj({...generateObj});
|
||
setLoadingText('提取分镜脚本...');
|
||
|
||
// 生成分镜脚本
|
||
await new Promise(resolve => setTimeout(resolve, 6000));
|
||
generateObj.scripts = [
|
||
{
|
||
shot: '面部特写,拉远至广角镜头。',
|
||
frame: '序列以雪(YUKI)面部的特写开场,她闭着眼睛躺在一片纯净的雪地里。柔和的雪花轻轻飘落在她的黑发和苍白的皮肤上。摄像机缓慢拉远,形成一幅令人惊叹的广角画面,揭示出在黄昏时分,她是在一片广阔、寂静、白雪覆盖的景观中的一个渺小、孤独的身影。',
|
||
atmosphere: '忧郁、宁静、寂静且寒冷。'
|
||
}, {
|
||
shot: '切至室内中景,随后是一个透过窗户的主观视角镜头。',
|
||
frame: '我们切到雪(YUKI)在一个舒适、光线温暖的卧室里醒来。她穿着一件舒适的毛衣,从床上下来,走向一扇窗户。她的呼吸在冰冷的玻璃上凝成雾气。她的主观视角镜头揭示了外面的雪景,远处有一座红色的房子,以及一封信被放入邮箱的记忆,从而触发了一段闪回。',
|
||
atmosphere: '怀旧、温暖、内省。'
|
||
}, {
|
||
shot: '跟踪镜头,随后是一系列切出镜头和特写。',
|
||
frame: '记忆开始。一个跟踪镜头跟随着一群学生,包括雪(YUKI),他们在飘落的樱花花瓣构成的华盖下走路上学。场景切到一个阳光普照的教室。雪(YUKI)穿着校服,坐在她的课桌前,害羞地瞥了一眼坐在前几排的春(HARU)。他仿佛感觉到她的凝视,巧妙地转过头来。',
|
||
atmosphere: '充满青春气息、怀旧、温暖,带有一种萌芽的、未言明的浪漫感。'
|
||
}, {
|
||
shot: '静态中景,切到另一个中景,营造出共享空间的感觉。',
|
||
frame: '记忆转移到学校图书馆,充满了金色的光束。一个中景镜头显示春(HARU)靠在一个书架上,全神贯注地读一本书。然后摄像机切到雪(YUKI),她坐在附近的一张桌子旁,专注于画架上的一幅小画,当她感觉到他的存在时,嘴唇上泛起一丝淡淡的、秘密的微笑。',
|
||
atmosphere: '平和、亲密、书卷气、温暖。'
|
||
}, {
|
||
shot: '雪(YUKI)的中景,过渡到通过相机镜头的特写主观视角。',
|
||
frame: '在一个阳光明媚的运动日,雪(YUKI)站在运动场边缘,拿着一台老式相机。她举起相机,镜头推进到她透过取景器看的眼睛的特写。我们切到她的主观视角:除了站在运动场上、表情专注而坚定的春(HARU)之外,整个世界都是失焦的。',
|
||
atmosphere: '充满活力、专注,一种投入的观察和遥远的钦佩感。'
|
||
},
|
||
];
|
||
setGenerateObj({...generateObj}); // 使用展开运算符创建新对象,确保触发更新
|
||
setLoadingText('生成分镜视频...');
|
||
|
||
// 生成分镜视频
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
await generateSences();
|
||
setLoadingText('分镜剪辑...');
|
||
|
||
// 生成剪辑后的视频
|
||
await new Promise(resolve => setTimeout(resolve, 6000));
|
||
generateObj.cut_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
|
||
setGenerateObj({...generateObj});
|
||
setLoadingText('口型同步...');
|
||
|
||
// 口型同步后生成视频
|
||
await new Promise(resolve => setTimeout(resolve, 6000));
|
||
generateObj.audio_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
|
||
setGenerateObj({...generateObj});
|
||
setLoadingText('一致化处理...');
|
||
|
||
// 最终完成
|
||
await new Promise(resolve => setTimeout(resolve, 6000));
|
||
generateObj.final_video_url = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4';
|
||
setGenerateObj({...generateObj});
|
||
setLoadingText('完成');
|
||
setIsLoading(false);
|
||
} catch (error) {
|
||
console.error('视频生成过程出错:', error);
|
||
setLoadingText('生成失败,请重试');
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
|
||
// 处理视频选中
|
||
const handleVideoSelect = (index: number) => {
|
||
setSelectedVideoIndex(index);
|
||
// 滚动脚本到对应位置
|
||
const scriptElement = document.getElementById(`script-${index}`);
|
||
scriptElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||
};
|
||
|
||
// 处理脚本选中
|
||
const handleScriptSelect = (index: number) => {
|
||
setSelectedVideoIndex(index);
|
||
// 滚动视频到对应位置
|
||
const videoElement = document.getElementById(`video-${index}`);
|
||
videoElement?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||
};
|
||
|
||
// 处理脚本编辑
|
||
const handleEditScript = (type: 'shot' | 'frame' | 'atmosphere', value: string) => {
|
||
setEditingField({ type, value });
|
||
};
|
||
|
||
// 处理脚本点击
|
||
const handleScriptClick = (type: 'shot' | 'frame' | 'atmosphere', value: string) => {
|
||
if (editingField.type !== type) {
|
||
handleEditScript(type, value);
|
||
}
|
||
};
|
||
|
||
// 自动保存脚本编辑
|
||
useEffect(() => {
|
||
if (editingField.type && editingField.value && selectedVideoIndex !== null) {
|
||
const timeoutId = setTimeout(() => {
|
||
const updatedScripts = [...generateObj.scripts];
|
||
const fieldType = editingField.type as keyof typeof updatedScripts[number];
|
||
updatedScripts[selectedVideoIndex] = {
|
||
...updatedScripts[selectedVideoIndex],
|
||
[fieldType]: editingField.value
|
||
};
|
||
setGenerateObj({
|
||
...generateObj,
|
||
scripts: updatedScripts
|
||
});
|
||
}, 500); // 500ms 防抖
|
||
|
||
return () => clearTimeout(timeoutId);
|
||
}
|
||
}, [editingField.value, editingField.type, selectedVideoIndex, generateObj]);
|
||
|
||
// 刷新分镜脚本
|
||
const handleRefreshScript = () => {
|
||
if (selectedVideoIndex === null) return;
|
||
// TODO: 实现刷新脚本的逻辑
|
||
console.log('Refresh script for scene', selectedVideoIndex + 1);
|
||
};
|
||
|
||
// 处理删除分镜视频
|
||
const handleDeleteScene = (index: number) => {
|
||
if (generateObj.scene_videos) {
|
||
const newSceneVideos = generateObj.scene_videos.filter((_: SceneVideo, i: number) => i !== index);
|
||
setGenerateObj({
|
||
...generateObj,
|
||
scene_videos: newSceneVideos
|
||
});
|
||
setSelectedVideoIndex(null);
|
||
}
|
||
};
|
||
|
||
// 处理重新生成分镜视频
|
||
const handleRegenerateScene = async (index: number) => {
|
||
// TODO: 实现重新生成逻辑
|
||
console.log('Regenerate scene', index);
|
||
};
|
||
|
||
// 处理切换其他生成的视频
|
||
const handleSwitchVideo = (sceneIndex: number, videoIndex: number) => {
|
||
if (alternativeVideos[sceneIndex]?.[videoIndex]) {
|
||
const newSceneVideos = [...generateObj.scene_videos];
|
||
newSceneVideos[sceneIndex] = {
|
||
...newSceneVideos[sceneIndex],
|
||
video_url: alternativeVideos[sceneIndex][videoIndex]
|
||
};
|
||
setGenerateObj({
|
||
...generateObj,
|
||
scene_videos: newSceneVideos
|
||
});
|
||
setCurrentVideoIndex({
|
||
...currentVideoIndex,
|
||
[sceneIndex]: videoIndex
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理音量调节
|
||
const handleVolumeChange = (index: number, value: number) => {
|
||
setVolume({
|
||
...volume,
|
||
[index]: value
|
||
});
|
||
// TODO: 实现音量调节逻辑
|
||
};
|
||
|
||
// 处理转场设置
|
||
const handleTransitionChange = (index: number, value: string) => {
|
||
setTransition({
|
||
...transition,
|
||
[index]: value
|
||
});
|
||
// TODO: 实现转场效果逻辑
|
||
};
|
||
|
||
// 处理拖动开始
|
||
const handleDragStart = (e: React.MouseEvent) => {
|
||
setIsDragging(true);
|
||
setDragStart({
|
||
x: e.clientX - buttonPosition.x,
|
||
y: e.clientY - buttonPosition.y
|
||
});
|
||
};
|
||
|
||
// 处理拖动
|
||
const handleDrag = (e: MouseEvent) => {
|
||
if (isDragging && buttonRef.current) {
|
||
const newX = Math.min(
|
||
Math.max(0, e.clientX - dragStart.x),
|
||
window.innerWidth - buttonRef.current.offsetWidth
|
||
);
|
||
const newY = Math.min(
|
||
Math.max(0, e.clientY - dragStart.y),
|
||
window.innerHeight - buttonRef.current.offsetHeight
|
||
);
|
||
|
||
setButtonPosition({ x: newX, y: newY });
|
||
}
|
||
};
|
||
|
||
// 处理拖动结束
|
||
const handleDragEnd = () => {
|
||
setIsDragging(false);
|
||
};
|
||
|
||
// 添加拖动事件监听
|
||
useEffect(() => {
|
||
if (isDragging) {
|
||
window.addEventListener('mousemove', handleDrag);
|
||
window.addEventListener('mouseup', handleDragEnd);
|
||
}
|
||
|
||
return () => {
|
||
window.removeEventListener('mousemove', handleDrag);
|
||
window.removeEventListener('mouseup', handleDragEnd);
|
||
};
|
||
}, [isDragging, dragStart]);
|
||
|
||
return (
|
||
<div
|
||
ref={containerRef}
|
||
className="container mx-auto overflow-hidden custom-scrollbar"
|
||
style={isExpanded ? {height: 'calc(100vh - 12rem)'} : {height: 'calc(100vh - 20rem)'}}
|
||
>
|
||
{/* 展示创作详细过程 */}
|
||
{generateObj && (
|
||
<div className='video-creation-process-container'>
|
||
<ResizablePanelGroup
|
||
direction="vertical"
|
||
className="min-h-[800px] rounded-lg border bg-white/5 backdrop-blur-sm"
|
||
>
|
||
{/* 第一行 - 占2/3高度 */}
|
||
<ResizablePanel defaultSize={66}>
|
||
<ResizablePanelGroup direction="horizontal">
|
||
{/* 第一列 - 角色档案卡 */}
|
||
<ResizablePanel defaultSize={20}>
|
||
<div className="h-full flex flex-col">
|
||
<div className="p-4 border-b border-white/10">
|
||
<h3 className="text-base font-medium">角色档案</h3>
|
||
</div>
|
||
<div className="flex-1 overflow-y-auto p-4">
|
||
{generateObj.video_info ? (
|
||
<div className="space-y-4">
|
||
{generateObj.video_info.roles.map((role: any, index: number) => (
|
||
<div
|
||
key={index}
|
||
className='flex items-start gap-3 p-3 rounded-lg bg-white/[0.03] hover:bg-white/[0.05] transition-colors duration-200 group'
|
||
>
|
||
<div className='flex-shrink-0'>
|
||
<div className='w-[48px] h-[48px] rounded-full overflow-hidden border-2 border-white/10 group-hover:border-white/20 transition-colors duration-200'>
|
||
<img
|
||
src={role.avatar}
|
||
alt={role.name}
|
||
className='w-full h-full object-cover'
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className='flex-1 min-w-0'>
|
||
<div className='text-base font-medium mb-1 text-white/90'>{role.name}</div>
|
||
<div className='text-sm text-white/60 line-clamp-2'>{role.core_identity}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{[1, 2].map((i) => (
|
||
<div key={i} className="p-3 rounded-lg bg-white/[0.03]">
|
||
<div className="flex items-start gap-3">
|
||
<Skeleton className="w-12 h-12 rounded-full" />
|
||
<div className="flex-1 space-y-2">
|
||
<Skeleton className="h-4 w-24" />
|
||
<Skeleton className="h-3 w-full" />
|
||
<Skeleton className="h-3 w-3/4" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</ResizablePanel>
|
||
<ResizableHandle withHandle />
|
||
|
||
{/* 第二列 - 视频预览和加载状态 */}
|
||
<ResizablePanel defaultSize={60}>
|
||
<div className="h-full flex flex-col">
|
||
<div className="p-4 border-b border-white/10">
|
||
<h3 className="text-base font-medium">视频预览</h3>
|
||
</div>
|
||
<div className="flex-1 overflow-y-auto">
|
||
<div className="h-full flex items-center justify-center bg-white/[0.03] rounded-lg">
|
||
{isLoading ? (
|
||
<div className='relative'>
|
||
<div className='w-[40px] h-[40px] rounded-full bg-white/[0.05] flex items-center justify-center animate-bounce'>
|
||
<div className='w-[20px] h-[20px] rounded-full bg-white/[0.05] flex items-center justify-center animate-pulse'>
|
||
<div className='w-[10px] h-[10px] rounded-full bg-white/[0.05] flex items-center justify-center animate-ping'>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className='absolute top-[50px] left-1/2 -translate-x-1/2 whitespace-nowrap'>
|
||
<span className='text-white/70 text-sm animate-pulse inline-block'>{loadingText}</span>
|
||
<span className='inline-block ml-1 animate-bounce'>
|
||
<span className='inline-block animate-bounce delay-100'>.</span>
|
||
<span className='inline-block animate-bounce delay-200'>.</span>
|
||
<span className='inline-block animate-bounce delay-300'>.</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
) : generateObj.final_video_url ? (
|
||
<video
|
||
src={generateObj.final_video_url}
|
||
className="w-full h-full object-contain"
|
||
controls
|
||
/>
|
||
) : (
|
||
<div className="text-white/40">等待视频生成...</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</ResizablePanel>
|
||
<ResizableHandle withHandle />
|
||
|
||
{/* 第三列 - 概要信息 */}
|
||
<ResizablePanel defaultSize={20}>
|
||
<div className="h-full flex flex-col">
|
||
<div className="p-4 border-b border-white/10">
|
||
<h3 className="text-base font-medium">概要信息</h3>
|
||
</div>
|
||
<div className="flex-1 overflow-y-auto p-4">
|
||
{generateObj.video_info ? (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'>场景</span>
|
||
<p className='text-sm text-white/80 leading-relaxed'>{generateObj.video_info.sence}</p>
|
||
</div>
|
||
<div>
|
||
<span className='inline-block text-sm font-medium text-blue-400/90 mb-1'>风格</span>
|
||
<p className='text-sm text-white/80 leading-relaxed'>{generateObj.video_info.style}</p>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<Skeleton className="h-4 w-16 mb-2" />
|
||
<Skeleton className="h-20 w-full" />
|
||
</div>
|
||
<div>
|
||
<Skeleton className="h-4 w-16 mb-2" />
|
||
<Skeleton className="h-20 w-full" />
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</ResizablePanel>
|
||
</ResizablePanelGroup>
|
||
</ResizablePanel>
|
||
<ResizableHandle withHandle />
|
||
|
||
{/* 第二行 - 占1/3高度 */}
|
||
<ResizablePanel defaultSize={34}>
|
||
<ResizablePanelGroup direction="horizontal">
|
||
{/* 第一列 - 分镜脚本 */}
|
||
<ResizablePanel defaultSize={20}>
|
||
<div className="h-full flex flex-col">
|
||
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<h3 className="text-base font-medium">分镜脚本</h3>
|
||
{selectedVideoIndex !== null && (
|
||
<span className="px-2 py-1 rounded-full bg-blue-400/10 text-xs text-blue-400">
|
||
Scene {selectedVideoIndex + 1}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8 hover:bg-white/5"
|
||
onClick={handleRefreshScript}
|
||
>
|
||
<RefreshCw className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
<div className="flex-1 overflow-y-auto p-4">
|
||
{generateObj.scripts && selectedVideoIndex !== null ? (
|
||
<div className="space-y-6">
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className='text-sm font-medium text-blue-400/90'>镜头</span>
|
||
</div>
|
||
{editingField.type === 'shot' ? (
|
||
<Textarea
|
||
value={editingField.value}
|
||
onChange={(e) => setEditingField({ ...editingField, value: e.target.value })}
|
||
className="min-h-[80px] resize-none bg-transparent border-white/10 focus:border-white/20 focus-visible:ring-0 focus-visible:ring-offset-0 transition-colors"
|
||
placeholder="输入镜头描述..."
|
||
autoFocus
|
||
onBlur={() => setEditingField({ type: null, value: '' })}
|
||
/>
|
||
) : (
|
||
<div
|
||
className='text-sm text-white/80 leading-relaxed p-2 rounded hover:bg-white/5 cursor-text transition-colors'
|
||
onClick={() => handleScriptClick('shot', generateObj.scripts[selectedVideoIndex].shot)}
|
||
>
|
||
{generateObj.scripts[selectedVideoIndex].shot}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className='text-sm font-medium text-blue-400/90'>场景</span>
|
||
</div>
|
||
{editingField.type === 'frame' ? (
|
||
<Textarea
|
||
value={editingField.value}
|
||
onChange={(e) => setEditingField({ ...editingField, value: e.target.value })}
|
||
className="min-h-[120px] resize-none bg-transparent border-white/10 focus:border-white/20 focus-visible:ring-0 focus-visible:ring-offset-0 transition-colors"
|
||
placeholder="输入场景描述..."
|
||
autoFocus
|
||
onBlur={() => setEditingField({ type: null, value: '' })}
|
||
/>
|
||
) : (
|
||
<div
|
||
className='text-sm text-white/80 leading-relaxed p-2 rounded hover:bg-white/5 cursor-text transition-colors'
|
||
onClick={() => handleScriptClick('frame', generateObj.scripts[selectedVideoIndex].frame)}
|
||
>
|
||
{generateObj.scripts[selectedVideoIndex].frame}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className='text-sm font-medium text-blue-400/90'>氛围</span>
|
||
</div>
|
||
{editingField.type === 'atmosphere' ? (
|
||
<Input
|
||
value={editingField.value}
|
||
onChange={(e) => setEditingField({ ...editingField, value: e.target.value })}
|
||
className="bg-transparent border-white/10 focus:border-white/20 focus-visible:ring-0 focus-visible:ring-offset-0 transition-colors"
|
||
placeholder="输入氛围描述..."
|
||
autoFocus
|
||
onBlur={() => setEditingField({ type: null, value: '' })}
|
||
/>
|
||
) : (
|
||
<div
|
||
className='text-sm text-white/80 leading-relaxed p-2 rounded hover:bg-white/5 cursor-text transition-colors'
|
||
onClick={() => handleScriptClick('atmosphere', generateObj.scripts[selectedVideoIndex].atmosphere)}
|
||
>
|
||
{generateObj.scripts[selectedVideoIndex].atmosphere}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<Skeleton className="h-4 w-16 mb-2" />
|
||
<Skeleton className="h-20 w-full" />
|
||
</div>
|
||
<div>
|
||
<Skeleton className="h-4 w-16 mb-2" />
|
||
<Skeleton className="h-20 w-full" />
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</ResizablePanel>
|
||
<ResizableHandle withHandle />
|
||
|
||
{/* 第二列 - 分镜视频 */}
|
||
<ResizablePanel defaultSize={60}>
|
||
<div className="h-full flex flex-col">
|
||
<div className="p-4 border-b border-white/10">
|
||
<h3 className="text-base font-medium">分镜视频</h3>
|
||
</div>
|
||
<div className="flex-1 overflow-y-auto p-4">
|
||
<div
|
||
ref={videosContainerRef}
|
||
className='flex gap-4'
|
||
>
|
||
{generateObj.scripts ? (
|
||
generateObj.scripts.map((script: any, index: number) => {
|
||
const video = generateObj.scene_videos?.find((v: any) => v.id === index);
|
||
const isSelected = selectedVideoIndex === index;
|
||
|
||
return (
|
||
<div
|
||
key={index}
|
||
id={`video-${index}`}
|
||
className={`flex-shrink-0 w-[320px] aspect-video rounded-lg overflow-hidden relative cursor-pointer transition-all duration-300
|
||
${isSelected ? 'ring-2 ring-blue-400 ring-offset-2 ring-offset-[#191B1E]' : 'hover:ring-2 hover:ring-white/20'}`}
|
||
onClick={() => handleVideoSelect(index)}
|
||
>
|
||
{video ? (
|
||
<>
|
||
<video
|
||
src={video.video_url}
|
||
className="w-full h-full object-cover"
|
||
controls={isSelected}
|
||
/>
|
||
{!isSelected && (
|
||
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
|
||
<Play className="w-8 h-8 text-white/90" />
|
||
</div>
|
||
)}
|
||
</>
|
||
) : (
|
||
<div className="w-full h-full bg-white/5 flex items-center justify-center">
|
||
<Loader2 className="w-8 h-8 text-white/40 animate-spin" />
|
||
</div>
|
||
)}
|
||
<div className="absolute top-2 right-2 px-2 py-1 rounded-full bg-black/60 text-xs text-white/80">
|
||
Scene {index + 1}
|
||
</div>
|
||
</div>
|
||
);
|
||
})
|
||
) : (
|
||
<div className="flex-1 flex items-center justify-center">
|
||
<div className="grid grid-cols-3 gap-4">
|
||
{[1, 2, 3].map((i) => (
|
||
<Skeleton key={i} className="w-[320px] aspect-video rounded-lg" />
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</ResizablePanel>
|
||
<ResizableHandle withHandle />
|
||
|
||
{/* 第三列 - 属性设置 */}
|
||
<ResizablePanel defaultSize={20}>
|
||
<div className="h-full flex flex-col">
|
||
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<h3 className="text-base font-medium">属性设置</h3>
|
||
{selectedVideoIndex !== null && (
|
||
<span className="px-2 py-1 rounded-full bg-blue-400/10 text-xs text-blue-400">
|
||
Scene {selectedVideoIndex + 1}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<Sheet>
|
||
<SheetTrigger asChild>
|
||
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-white/5">
|
||
<MoreHorizontal className="h-4 w-4" />
|
||
</Button>
|
||
</SheetTrigger>
|
||
<SheetContent className="w-[400px] sm:w-[540px] bg-gradient-to-b from-gray-900/95 to-black/95 border-gray-800">
|
||
<SheetHeader>
|
||
<SheetTitle className="text-xl font-semibold text-white">高级设置</SheetTitle>
|
||
<div className="text-sm text-white/60">
|
||
{selectedVideoIndex !== null ? `Scene ${selectedVideoIndex + 1} 的高级设置选项` : '全局高级设置选项'}
|
||
</div>
|
||
</SheetHeader>
|
||
<div className="mt-6 space-y-6">
|
||
<div className="space-y-4">
|
||
<h4 className="text-sm font-medium text-white/90">视觉效果</h4>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
|
||
<div className="text-sm font-medium mb-1">色调</div>
|
||
<div className="text-xs text-white/60">调整画面整体色调</div>
|
||
</div>
|
||
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
|
||
<div className="text-sm font-medium mb-1">对比度</div>
|
||
<div className="text-xs text-white/60">调整画面明暗对比</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<h4 className="text-sm font-medium text-white/90">转场效果</h4>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
|
||
<div className="text-sm font-medium mb-1">转场类型</div>
|
||
<div className="text-xs text-white/60">选择画面切换效果</div>
|
||
</div>
|
||
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
|
||
<div className="text-sm font-medium mb-1">转场时长</div>
|
||
<div className="text-xs text-white/60">设置转场持续时间</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<h4 className="text-sm font-medium text-white/90">音频设置</h4>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
|
||
<div className="text-sm font-medium mb-1">背景音乐</div>
|
||
<div className="text-xs text-white/60">添加或调整背景音乐</div>
|
||
</div>
|
||
<div className="p-4 rounded-lg bg-white/5 hover:bg-white/10 transition-colors cursor-pointer">
|
||
<div className="text-sm font-medium mb-1">音量调节</div>
|
||
<div className="text-xs text-white/60">调整各音轨音量</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</SheetContent>
|
||
</Sheet>
|
||
</div>
|
||
<div className="flex-1 overflow-y-auto p-4">
|
||
{generateObj.scripts && selectedVideoIndex !== null ? (
|
||
<div className="space-y-4">
|
||
{/* 基础操作按钮 */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<Button
|
||
variant="ghost"
|
||
className="w-full h-20 rounded-lg bg-white/5 hover:bg-white/10 transition-colors flex flex-col items-center justify-center gap-2"
|
||
onClick={() => handleDeleteScene(selectedVideoIndex)}
|
||
>
|
||
<X className="w-5 h-5" />
|
||
<span className="text-sm">删除分镜</span>
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
className="w-full h-20 rounded-lg bg-white/5 hover:bg-white/10 transition-colors flex flex-col items-center justify-center gap-2"
|
||
onClick={() => handleRegenerateScene(selectedVideoIndex)}
|
||
>
|
||
<RefreshCw className="w-5 h-5" />
|
||
<span className="text-sm">重新生成</span>
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 音量调节 */}
|
||
<div className="p-4 rounded-lg bg-white/5">
|
||
<div className="text-sm font-medium mb-3">音量调节</div>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="100"
|
||
value={volume[selectedVideoIndex] || 100}
|
||
onChange={(e) => handleVolumeChange(selectedVideoIndex, Number(e.target.value))}
|
||
className="w-full h-1 bg-white/10 rounded-lg appearance-none cursor-pointer"
|
||
/>
|
||
<div className="mt-2 text-right text-sm text-white/60">
|
||
{volume[selectedVideoIndex] || 100}%
|
||
</div>
|
||
</div>
|
||
|
||
{/* 转场效果 */}
|
||
<div className="p-4 rounded-lg bg-white/5">
|
||
<div className="text-sm font-medium mb-3">转场效果</div>
|
||
<div className="grid grid-cols-3 gap-2">
|
||
{['淡入淡出', '溶解', '划入', '百叶窗', '放大', '旋转'].map((effect) => (
|
||
<div
|
||
key={effect}
|
||
className={`p-2 rounded text-center text-sm cursor-pointer transition-colors ${
|
||
transition[selectedVideoIndex] === effect
|
||
? 'bg-blue-400/20 text-blue-400'
|
||
: 'bg-white/10 hover:bg-white/20'
|
||
}`}
|
||
onClick={() => handleTransitionChange(selectedVideoIndex, effect)}
|
||
>
|
||
{effect}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 其他生成的视频 */}
|
||
{alternativeVideos[selectedVideoIndex]?.length > 0 && (
|
||
<div className="p-4 rounded-lg bg-white/5">
|
||
<div className="text-sm font-medium mb-3">其他生成的视频</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{alternativeVideos[selectedVideoIndex].map((videoUrl, index) => (
|
||
<div
|
||
key={index}
|
||
className={`relative aspect-video rounded-lg overflow-hidden cursor-pointer ${
|
||
currentVideoIndex[selectedVideoIndex] === index
|
||
? 'ring-2 ring-blue-400'
|
||
: ''
|
||
}`}
|
||
onClick={() => handleSwitchVideo(selectedVideoIndex, index)}
|
||
>
|
||
<video
|
||
src={videoUrl}
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
<div className="absolute inset-0 flex items-center justify-center bg-black/40">
|
||
<Play className="w-8 h-8 text-white/90" />
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
<Skeleton className="h-8 w-full" />
|
||
<Skeleton className="h-8 w-3/4" />
|
||
<Skeleton className="h-8 w-1/2" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</ResizablePanel>
|
||
</ResizablePanelGroup>
|
||
</ResizablePanel>
|
||
</ResizablePanelGroup>
|
||
</div>
|
||
)}
|
||
|
||
{/* 点击生成按钮后,暂停/继续按钮 */}
|
||
{isLoading && (
|
||
<div
|
||
ref={buttonRef}
|
||
className={`fixed z-50 cursor-move ${isDragging ? 'select-none' : ''}`}
|
||
style={{
|
||
left: `${buttonPosition.x}px`,
|
||
top: `${buttonPosition.y}px`,
|
||
transform: 'translate(-50%, -50%)',
|
||
transition: isDragging ? 'none' : 'all 0.2s ease'
|
||
}}
|
||
onMouseDown={handleDragStart}
|
||
>
|
||
<button
|
||
className={`
|
||
flex items-center justify-center w-12 h-12 rounded-full
|
||
bg-gradient-to-br from-blue-400/20 to-blue-600/20 backdrop-blur-lg
|
||
hover:from-blue-400/30 hover:to-blue-600/30
|
||
transition-all duration-300
|
||
${isDragging ? 'scale-90' : 'scale-100 hover:scale-110'}
|
||
`}
|
||
onClick={handlePauseResume}
|
||
>
|
||
{isPaused ? (
|
||
<Play className="w-5 h-5 text-white/70 hover:text-white/90 transition-colors" />
|
||
) : (
|
||
<Pause className="w-5 h-5 text-white/70 hover:text-white/90 transition-colors" />
|
||
)}
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
|
||
{/* 工具栏 */}
|
||
<div className='video-tool-component relative w-[1080px]'>
|
||
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-[#fff3] backdrop-blur-[15px]'>
|
||
{isExpanded ? (
|
||
<div className='absolute top-0 bottom-0 left-0 right-0 z-[9] grid justify-items-center place-content-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer' onClick={() => setIsExpanded(false)}>
|
||
{/* 图标 展开按钮 */}
|
||
<ChevronUp className='w-4 h-4' />
|
||
<span className='text-sm'>Click to create</span>
|
||
</div>
|
||
) : (
|
||
<div className='absolute top-[-8px] left-[50%] z-[10] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]' onClick={() => setIsExpanded(true)}>
|
||
{/* 图标 折叠按钮 */}
|
||
<ChevronDown className='w-4 h-4' />
|
||
</div>
|
||
)}
|
||
|
||
<div className='storyboard-tools-tab relative flex gap-8 px-4 py-[10px]'>
|
||
<div className={`tab-item ${activeTab === 'clone' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('clone')}>
|
||
<span className='text-sm opacity-60'>复刻视频</span>
|
||
</div>
|
||
<div className={`tab-item ${activeTab === 'script' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('script')}>
|
||
<span className='text-sm opacity-60'>剧本生成视频</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className={`flex-shrink-0 p-4 overflow-hidden transition-all duration-300 pt-0 ${isExpanded ? 'h-[16px]' : 'h-[162px]'}`}>
|
||
<div className='video-creation-tool-container flex flex-col gap-4'>
|
||
{activeTab === 'clone' && (
|
||
<div className='relative flex items-center gap-4 h-[94px]'>
|
||
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger' onClick={handleUploadVideo}>
|
||
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
|
||
{/* 图标 添加视频 */}
|
||
<Video className='w-4 h-4' />
|
||
</div>
|
||
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'>
|
||
<span className='text-xs cursor-[inherit]'>Add Video</span>
|
||
</div>
|
||
</div>
|
||
{videoUrl && (
|
||
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger'>
|
||
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
|
||
<video src={videoUrl} className='w-full h-full object-cover' />
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
{activeTab === 'script' && (
|
||
<div className='relative flex items-center gap-4 h-[94px]'>
|
||
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className='flex gap-3 justify-end'>
|
||
<div className='flex items-center gap-3'>
|
||
<div className='disabled tool-submit-button'>Stop</div>
|
||
<div className={`tool-submit-button ${videoUrl ? '' : 'disabled'}`} onClick={handleCreateVideo}>Create</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|