forked from 77media/video-flow
1212 lines
46 KiB
TypeScript
1212 lines
46 KiB
TypeScript
"use client"
|
||
import React, { useEffect, useState, useRef, useMemo, useCallback } from "react";
|
||
import { Play, ChevronUp, Loader2, Edit3, FileText, Pause } from "lucide-react";
|
||
import "./style/work-flow.css";
|
||
import LiquidGlass from '@/plugins/liquid-glass';
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import { AISuggestionBar } from "@/components/ai-suggestion-bar";
|
||
import { motion, AnimatePresence } from "framer-motion";
|
||
import { debounce } from "lodash";
|
||
import { GlassIconButton } from "@/components/ui/glass-icon-button";
|
||
import { EditModal } from "@/components/ui/edit-modal";
|
||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||
|
||
const MOCK_SKETCH_URLS = [
|
||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg',
|
||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg',
|
||
'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg',
|
||
];
|
||
const MOCK_SKETCH_SCRIPT = [
|
||
'script-123',
|
||
'script-123',
|
||
'script-123',
|
||
'script-123',
|
||
];
|
||
const MOCK_VIDEO_URLS = [
|
||
'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',
|
||
];
|
||
|
||
const MOCK_SKETCH_COUNT = 8;
|
||
|
||
const MOCK_FINAL_VIDEO = {
|
||
id: 'final-video',
|
||
url: 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4',
|
||
thumbnail: 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg',
|
||
};
|
||
|
||
export default function WorkFlow() {
|
||
const [taskObject, setTaskObject] = useState<any>(null);
|
||
const [projectObject, setProjectObject] = useState<any>(null);
|
||
const [taskSketch, setTaskSketch] = useState<any[]>([]);
|
||
const [sketchCount, setSketchCount] = useState(0);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [isAIBarVisible, setIsAIBarVisible] = useState(true);
|
||
const [currentStep, setCurrentStep] = useState('0');
|
||
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
|
||
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
|
||
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
const [startX, setStartX] = useState(0);
|
||
const [scrollLeft, setScrollLeft] = useState(0);
|
||
const [showControls, setShowControls] = useState(false);
|
||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||
const [activeEditTab, setActiveEditTab] = useState('1');
|
||
const [taskVideos, setTaskVideos] = useState<any[]>([]);
|
||
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||
const [isPlaying, setIsPlaying] = useState(false);
|
||
const playTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||
const [isVideoPlaying, setIsVideoPlaying] = useState(true);
|
||
const videoPlayTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||
const [currentLoadingText, setCurrentLoadingText] = useState('加载中...');
|
||
|
||
// 模拟 AI 建议
|
||
const mockSuggestions = [
|
||
"优化场景转场效果",
|
||
"调整画面构图",
|
||
"改进角色动作设计",
|
||
"增加环境氛围",
|
||
"调整镜头语言"
|
||
];
|
||
|
||
useEffect(() => {
|
||
const taskId = localStorage.getItem("taskId") || "taskId-123";
|
||
getTaskDetail(taskId).then(async (data) => {
|
||
setTaskObject(data);
|
||
setIsLoading(false);
|
||
setCurrentStep('1');
|
||
// 只在任务详情加载完成后获取分镜草图
|
||
await getTaskSketch(taskId);
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
// 首先修改 taskObject 下的 taskStatus 为 '2'
|
||
setTaskObject((prev: any) => ({
|
||
...prev,
|
||
taskStatus: '2'
|
||
}));
|
||
setCurrentStep('2');
|
||
// 获取分镜草图后,开始绘制角色
|
||
await getTaskRole(taskId);
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
// 首先修改 taskObject 下的 taskStatus 为 '3'
|
||
setTaskObject((prev: any) => ({
|
||
...prev,
|
||
taskStatus: '3'
|
||
}));
|
||
setCurrentStep('3');
|
||
// 获取绘制角色后,开始获取分镜视频
|
||
await getTaskVideo(taskId);
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
// 首先修改 taskObject 下的 taskStatus 为 '4'
|
||
setTaskObject((prev: any) => ({
|
||
...prev,
|
||
taskStatus: '4'
|
||
}));
|
||
setCurrentStep('4');
|
||
// 获取分镜视频后,开始获取背景音
|
||
await getTaskBackgroundAudio(taskId);
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
// 首先修改 taskObject 下的 taskStatus 为 '5'
|
||
setTaskObject((prev: any) => ({
|
||
...prev,
|
||
taskStatus: '5'
|
||
}));
|
||
setCurrentStep('5');
|
||
// 获取背景音后,开始获取最终成品
|
||
await getTaskFinalProduct(taskId);
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
// 首先修改 taskObject 下的 taskStatus 为 '6'
|
||
setTaskObject((prev: any) => ({
|
||
...prev,
|
||
taskStatus: '6'
|
||
}));
|
||
setCurrentStep('6');
|
||
// 获取最终成品后,任务完成
|
||
});
|
||
}, []);
|
||
|
||
// 监听当前选中索引变化,自动滚动到对应位置
|
||
useEffect(() => {
|
||
if (thumbnailsRef.current && taskSketch.length > 0) {
|
||
const container = thumbnailsRef.current;
|
||
const thumbnailWidth = container.offsetWidth / 4; // 每个缩略图宽度(包含间距)
|
||
const scrollPosition = currentSketchIndex * thumbnailWidth;
|
||
|
||
container.scrollTo({
|
||
left: scrollPosition,
|
||
behavior: 'smooth'
|
||
});
|
||
}
|
||
}, [currentSketchIndex, taskSketch.length]);
|
||
|
||
const handleEditModalOpen = (tab: string) => {
|
||
// 停止循环播放
|
||
setIsPlaying(false);
|
||
// 停止分镜视频播放
|
||
setIsVideoPlaying(false);
|
||
setActiveEditTab(tab);
|
||
setIsEditModalOpen(true);
|
||
}
|
||
|
||
// 处理鼠标/触摸拖动事件
|
||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||
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;
|
||
|
||
const container = thumbnailsRef.current!;
|
||
const thumbnailWidth = container.offsetWidth / 4;
|
||
const currentScroll = container.scrollLeft;
|
||
const nearestIndex = Math.round(currentScroll / thumbnailWidth);
|
||
|
||
// 只有在拖动距离较小时才触发选中
|
||
const x = e.pageX - container.offsetLeft;
|
||
const walk = Math.abs(x - startX);
|
||
if (walk < 10) {
|
||
return; // 如果拖动距离太小,保持原有的点击选中逻辑
|
||
}
|
||
|
||
setCurrentSketchIndex(Math.min(Math.max(0, nearestIndex), taskSketch.length - 1));
|
||
};
|
||
|
||
// 模拟接口请求 获取任务详情
|
||
const getTaskDetail = async (taskId: string) => {
|
||
// const response = await fetch(`/api/task/${taskId}`);
|
||
// const data = await response.json();
|
||
// mock data
|
||
const data = {
|
||
projectId: 'projectId-123',
|
||
projectName: "Project 1",
|
||
taskId: taskId,
|
||
taskName: "Task 1",
|
||
taskDescription: "Task 1 Description",
|
||
taskStatus: "1", // '1' 绘制分镜、'2' 绘制角色、'3' 生成分镜视频、'4' 视频后期制作、'5' 最终成品
|
||
taskProgress: 0,
|
||
mode: 'auto', // 全自动模式、人工干预模式
|
||
resolution: '1080p', // 1080p、2160p
|
||
};
|
||
return data;
|
||
}
|
||
|
||
// 模拟接口请求 每次获取一个分镜草图 轮询获取
|
||
const getTaskSketch = async (taskId: string) => {
|
||
// 避免重复调用
|
||
if (isGeneratingSketch || taskSketch.length > 0) return;
|
||
|
||
setIsGeneratingSketch(true);
|
||
setTaskSketch([]);
|
||
|
||
// 模拟分批获取分镜草图
|
||
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
|
||
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||
|
||
const newSketch = {
|
||
id: `sketch-${i}`,
|
||
url: MOCK_SKETCH_URLS[i % MOCK_SKETCH_URLS.length],
|
||
script: MOCK_SKETCH_SCRIPT[i % MOCK_SKETCH_SCRIPT.length],
|
||
status: 'done'
|
||
};
|
||
|
||
setTaskSketch(prev => {
|
||
// 避免重复添加相同id的sketch
|
||
if (prev.find(sketch => sketch.id === newSketch.id)) {
|
||
return prev;
|
||
}
|
||
return [...prev, newSketch];
|
||
});
|
||
setCurrentSketchIndex(i);
|
||
setSketchCount(i + 1);
|
||
}
|
||
|
||
setIsGeneratingSketch(false);
|
||
}
|
||
|
||
// 模拟接口请求 每次获取一个角色 轮询获取
|
||
const getTaskRole = async (taskId: string) => {
|
||
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||
}
|
||
|
||
// 模拟接口请求 获取背景音
|
||
const getTaskBackgroundAudio = async (taskId: string) => {
|
||
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||
}
|
||
|
||
// 模拟接口请求 获取最终成品
|
||
const getTaskFinalProduct = async (taskId: string) => {
|
||
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||
}
|
||
|
||
// 模拟接口请求 每次获取一个分镜视频 轮询获取
|
||
const getTaskVideo = async (taskId: string) => {
|
||
setIsGeneratingVideo(true);
|
||
setTaskVideos([]);
|
||
|
||
// 模拟分批获取分镜视频
|
||
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
|
||
await new Promise(resolve => setTimeout(resolve, 5000)); // 模拟5秒延迟
|
||
|
||
const newVideo = {
|
||
id: `video-${i}`,
|
||
url: MOCK_VIDEO_URLS[i % MOCK_VIDEO_URLS.length],
|
||
script: MOCK_SKETCH_SCRIPT[i % MOCK_SKETCH_SCRIPT.length],
|
||
status: 'done'
|
||
};
|
||
|
||
setTaskVideos(prev => {
|
||
// 避免重复添加相同id的video
|
||
if (prev.find(video => video.id === newVideo.id)) {
|
||
return prev;
|
||
}
|
||
return [...prev, newVideo];
|
||
});
|
||
setCurrentSketchIndex(i);
|
||
}
|
||
|
||
setIsGeneratingVideo(false);
|
||
};
|
||
|
||
const handleSuggestionClick = (suggestion: string) => {
|
||
console.log('Selected suggestion:', suggestion);
|
||
};
|
||
|
||
const handleSubmit = (text: string) => {
|
||
console.log('Submitted text:', text);
|
||
};
|
||
|
||
// 缓存渲染的缩略图列表
|
||
const renderedSketches = useMemo(() =>
|
||
taskSketch.map((sketch, index) => (
|
||
<div
|
||
key={`sketch-${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 && setCurrentSketchIndex(index)}
|
||
>
|
||
<ProgressiveReveal
|
||
{...presets.thumbnail}
|
||
delay={index * 0.1}
|
||
customVariants={{
|
||
hidden: {
|
||
opacity: 0,
|
||
scale: 0.95,
|
||
filter: "blur(10px)"
|
||
},
|
||
visible: {
|
||
opacity: 1,
|
||
scale: 1,
|
||
filter: "blur(0px)",
|
||
transition: {
|
||
duration: 0.8,
|
||
ease: [0.23, 1, 0.32, 1], // cubic-bezier
|
||
opacity: { duration: 0.6, ease: "easeInOut" },
|
||
scale: { duration: 1, ease: "easeOut" },
|
||
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
|
||
}
|
||
}
|
||
}}
|
||
loadingBgConfig={{
|
||
...presets.thumbnail.loadingBgConfig,
|
||
glowOpacity: 0.4,
|
||
duration: 4
|
||
}}
|
||
>
|
||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||
<img
|
||
className="w-full h-full object-cover"
|
||
src={sketch.url}
|
||
alt={`缩略图 ${index + 1}`}
|
||
/>
|
||
</div>
|
||
</ProgressiveReveal>
|
||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||
<span className="text-xs text-white/90">场景 {index + 1}</span>
|
||
</div>
|
||
</div>
|
||
)), [taskSketch, currentSketchIndex, isDragging]
|
||
);
|
||
|
||
// 缓存渲染的视频缩略图列表
|
||
const renderedVideos = useMemo(() =>
|
||
taskVideos.map((video, index) => (
|
||
<div
|
||
key={`video-${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 && setCurrentSketchIndex(index)}
|
||
>
|
||
<ProgressiveReveal
|
||
{...presets.thumbnail}
|
||
delay={index * 0.1}
|
||
customVariants={{
|
||
hidden: {
|
||
opacity: 0,
|
||
scale: 0.95,
|
||
filter: "blur(10px)"
|
||
},
|
||
visible: {
|
||
opacity: 1,
|
||
scale: 1,
|
||
filter: "blur(0px)",
|
||
transition: {
|
||
duration: 0.8,
|
||
ease: [0.23, 1, 0.32, 1], // cubic-bezier
|
||
opacity: { duration: 0.6, ease: "easeInOut" },
|
||
scale: { duration: 1, ease: "easeOut" },
|
||
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
|
||
}
|
||
}
|
||
}}
|
||
loadingBgConfig={{
|
||
...presets.thumbnail.loadingBgConfig,
|
||
glowOpacity: 0.4,
|
||
duration: 4
|
||
}}
|
||
>
|
||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||
<video
|
||
className="w-full h-full object-cover"
|
||
src={video.url}
|
||
muted
|
||
playsInline
|
||
loop
|
||
poster={taskSketch[index]?.url}
|
||
/>
|
||
</div>
|
||
</ProgressiveReveal>
|
||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||
<span className="text-xs text-white/90">场景 {index + 1}</span>
|
||
</div>
|
||
</div>
|
||
)), [taskVideos, currentSketchIndex, isDragging, taskSketch]
|
||
);
|
||
|
||
// 处理播放/暂停
|
||
const togglePlay = useCallback(() => {
|
||
setIsPlaying(prev => !prev);
|
||
}, []);
|
||
|
||
// 自动播放逻辑
|
||
useEffect(() => {
|
||
if (isPlaying && taskSketch.length > 0) {
|
||
playTimerRef.current = setInterval(() => {
|
||
setCurrentSketchIndex(prev => {
|
||
const nextIndex = (prev + 1) % taskSketch.length;
|
||
return nextIndex;
|
||
});
|
||
}, 2000); // 每2秒切换一次
|
||
} else if (playTimerRef.current) {
|
||
clearInterval(playTimerRef.current);
|
||
}
|
||
|
||
return () => {
|
||
if (playTimerRef.current) {
|
||
clearInterval(playTimerRef.current);
|
||
}
|
||
};
|
||
}, [isPlaying, taskSketch.length]);
|
||
|
||
// 当切换到视频模式时,停止播放
|
||
useEffect(() => {
|
||
if (currentStep === '3') {
|
||
setIsPlaying(false);
|
||
}
|
||
}, [currentStep]);
|
||
|
||
// 处理视频播放/暂停
|
||
const toggleVideoPlay = useCallback(() => {
|
||
if (mainVideoRef.current) {
|
||
if (isVideoPlaying) {
|
||
mainVideoRef.current.pause();
|
||
} else {
|
||
// 从暂停位置继续播放
|
||
mainVideoRef.current.play();
|
||
}
|
||
}
|
||
setIsVideoPlaying(prev => !prev);
|
||
}, []);
|
||
|
||
// 视频自动播放逻辑
|
||
useEffect(() => {
|
||
if (isVideoPlaying && taskVideos.length > 0) {
|
||
// 确保当前视频开始播放
|
||
if (mainVideoRef.current) {
|
||
mainVideoRef.current.play().catch(error => {
|
||
console.log('视频播放失败:', error);
|
||
setIsVideoPlaying(false);
|
||
});
|
||
}
|
||
} else {
|
||
// 暂停当前视频
|
||
if (mainVideoRef.current) {
|
||
mainVideoRef.current.pause();
|
||
}
|
||
// 清除定时器
|
||
if (videoPlayTimerRef.current) {
|
||
clearInterval(videoPlayTimerRef.current);
|
||
}
|
||
}
|
||
|
||
return () => {
|
||
if (videoPlayTimerRef.current) {
|
||
clearInterval(videoPlayTimerRef.current);
|
||
}
|
||
};
|
||
}, [isVideoPlaying, taskVideos.length]);
|
||
|
||
// 当切换视频时重置视频播放
|
||
useEffect(() => {
|
||
if (mainVideoRef.current) {
|
||
// 只有在切换视频时才重置时间
|
||
mainVideoRef.current.currentTime = 0;
|
||
if (isVideoPlaying) {
|
||
mainVideoRef.current.play().catch(error => {
|
||
console.log('视频播放失败:', error);
|
||
setIsVideoPlaying(false);
|
||
});
|
||
}
|
||
}
|
||
}, [currentSketchIndex]);
|
||
|
||
// 当切换到分镜草图模式时,停止视频播放
|
||
useEffect(() => {
|
||
if (currentStep !== '3') {
|
||
setIsVideoPlaying(false);
|
||
}
|
||
}, [currentStep]);
|
||
|
||
// 更新加载文字
|
||
useEffect(() => {
|
||
if (isLoading) {
|
||
setCurrentLoadingText('正在加载任务信息...');
|
||
return;
|
||
}
|
||
|
||
if (currentStep === '1') {
|
||
if (isGeneratingSketch) {
|
||
setCurrentLoadingText(`正在生成分镜草图 ${sketchCount + 1}/${MOCK_SKETCH_COUNT}...`);
|
||
} else {
|
||
setCurrentLoadingText('分镜草图生成完成');
|
||
}
|
||
} else if (currentStep === '2') {
|
||
setCurrentLoadingText('正在绘制角色...');
|
||
} else if (currentStep === '3') {
|
||
if (isGeneratingVideo) {
|
||
setCurrentLoadingText(`正在生成分镜视频 ${taskVideos.length + 1}/${taskSketch.length}...`);
|
||
} else {
|
||
setCurrentLoadingText('分镜视频生成完成');
|
||
}
|
||
} else if (currentStep === '4') {
|
||
setCurrentLoadingText('正在生成背景音...');
|
||
} else if (currentStep === '5') {
|
||
setCurrentLoadingText('正在生成最终成品...');
|
||
} else {
|
||
setCurrentLoadingText('任务完成');
|
||
}
|
||
}, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length]);
|
||
|
||
const renderSketchContent = () => {
|
||
|
||
// 展示最终成片
|
||
if (Number(currentStep) === 6) {
|
||
return (
|
||
<div className="relative w-full h-full rounded-lg overflow-hidden" onMouseEnter={() => setShowControls(true)} onMouseLeave={() => setShowControls(false)}>
|
||
<div className="relative w-full h-full">
|
||
{/* 背景模糊的视频 */}
|
||
<motion.div
|
||
className="absolute inset-0 overflow-hidden"
|
||
initial={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
|
||
animate={{ filter: "blur(20px)", scale: 1.1, opacity: 0.5 }}
|
||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||
>
|
||
<video
|
||
className="w-full h-full rounded-lg object-cover object-center"
|
||
src={taskVideos[currentSketchIndex]?.url}
|
||
autoPlay
|
||
loop
|
||
muted
|
||
playsInline
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* 最终成片视频 */}
|
||
<motion.div
|
||
initial={{ clipPath: "inset(0 50% 0 50%)", filter: "blur(10px)" }}
|
||
animate={{ clipPath: "inset(0 0% 0 0%)", filter: "blur(0px)" }}
|
||
transition={{
|
||
clipPath: { duration: 1.2, ease: [0.43, 0.13, 0.23, 0.96] },
|
||
filter: { duration: 0.6, delay: 0.3 }
|
||
}}
|
||
className="relative z-10"
|
||
>
|
||
<video
|
||
className="w-full h-full object-cover rounded-lg"
|
||
src={MOCK_FINAL_VIDEO.url}
|
||
poster={MOCK_FINAL_VIDEO.thumbnail}
|
||
autoPlay
|
||
loop
|
||
muted
|
||
playsInline
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* 操作按钮组 */}
|
||
<AnimatePresence>
|
||
{showControls && (
|
||
<>
|
||
{/* 顶部按钮组 */}
|
||
<motion.div
|
||
className="absolute top-4 right-4 z-10 flex gap-2"
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<GlassIconButton
|
||
icon={Edit3}
|
||
tooltip="编辑分镜"
|
||
onClick={() => handleEditModalOpen('4')}
|
||
/>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* 视频信息浮层 */}
|
||
<motion.div
|
||
className="absolute bottom-0 left-0 right-0 z-10 p-4 bg-gradient-to-t from-black/80 via-black/40 to-transparent"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 1, duration: 0.6 }}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<motion.div
|
||
className="w-2 h-2 rounded-full bg-emerald-500"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
opacity: [1, 0.6, 1]
|
||
}}
|
||
transition={{
|
||
duration: 2,
|
||
repeat: Infinity,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
<span className="text-sm font-medium text-white/90">最终成片</span>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* 完成标记 */}
|
||
<motion.div
|
||
className="absolute top-4 right-4 px-3 py-1.5 rounded-full bg-emerald-500/20 backdrop-blur-sm
|
||
border border-emerald-500/30 text-emerald-400 text-sm font-medium"
|
||
initial={{ opacity: 0, scale: 0.8, x: 20 }}
|
||
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||
transition={{ delay: 1.2, duration: 0.6 }}
|
||
>
|
||
制作完成
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 展示分镜视频
|
||
if (Number(currentStep) > 2 && Number(currentStep) < 6) {
|
||
return (
|
||
<div
|
||
className="relative w-full h-full rounded-lg"
|
||
onMouseEnter={() => setShowControls(true)}
|
||
onMouseLeave={() => setShowControls(false)}
|
||
>
|
||
{taskVideos[currentSketchIndex] ? (
|
||
<ProgressiveReveal
|
||
className="w-full h-full rounded-lg"
|
||
customVariants={{
|
||
hidden: {
|
||
opacity: 0,
|
||
filter: "blur(20px)",
|
||
clipPath: "inset(0 100% 0 0)"
|
||
},
|
||
visible: {
|
||
opacity: 1,
|
||
filter: "blur(0px)",
|
||
clipPath: "inset(0 0% 0 0)",
|
||
transition: {
|
||
duration: 1,
|
||
ease: [0.43, 0.13, 0.23, 0.96],
|
||
opacity: { duration: 0.8, ease: "easeOut" },
|
||
filter: { duration: 0.6, ease: "easeOut" },
|
||
clipPath: { duration: 0.8, ease: "easeInOut" }
|
||
}
|
||
}
|
||
}}
|
||
>
|
||
<div className="relative w-full h-full">
|
||
{/* 背景模糊的图片 */}
|
||
<div className="absolute inset-0 overflow-hidden">
|
||
<img
|
||
className="w-full h-full object-cover filter blur-lg scale-110 opacity-50"
|
||
src={taskSketch[currentSketchIndex]?.url}
|
||
alt="background"
|
||
/>
|
||
</div>
|
||
|
||
{/* 视频 */}
|
||
<motion.div
|
||
initial={{ clipPath: "inset(0 100% 0 0)" }}
|
||
animate={{ clipPath: "inset(0 0% 0 0)" }}
|
||
transition={{ duration: 0.8, ease: [0.43, 0.13, 0.23, 0.96] }}
|
||
className="relative z-10"
|
||
>
|
||
<video
|
||
ref={mainVideoRef}
|
||
key={taskVideos[currentSketchIndex].url}
|
||
className="w-full h-full rounded-lg object-cover object-center"
|
||
src={taskVideos[currentSketchIndex].url}
|
||
autoPlay={isVideoPlaying}
|
||
muted
|
||
loop={false}
|
||
playsInline
|
||
poster={taskSketch[currentSketchIndex]?.url}
|
||
onEnded={() => {
|
||
if (isVideoPlaying) {
|
||
setCurrentSketchIndex(prev => (prev + 1) % taskVideos.length);
|
||
}
|
||
}}
|
||
/>
|
||
</motion.div>
|
||
</div>
|
||
</ProgressiveReveal>
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
|
||
{/* 保持显示当前分镜草图 */}
|
||
<img
|
||
className="absolute inset-0 w-full h-full object-cover"
|
||
src={taskSketch[currentSketchIndex]?.url}
|
||
alt={`分镜草图 ${currentSketchIndex + 1}`}
|
||
/>
|
||
|
||
</div>
|
||
)}
|
||
|
||
{/* 操作按钮组 */}
|
||
<AnimatePresence>
|
||
{showControls && (
|
||
<>
|
||
{/* 顶部按钮组 */}
|
||
<motion.div
|
||
className="absolute top-4 right-4 flex gap-2"
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<GlassIconButton
|
||
icon={Edit3}
|
||
tooltip="编辑分镜"
|
||
onClick={() => handleEditModalOpen('3')}
|
||
/>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* 底部播放按钮 */}
|
||
<AnimatePresence>
|
||
<motion.div
|
||
className="absolute bottom-4 left-4"
|
||
initial={{ opacity: 0, scale: 0.8 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
exit={{ opacity: 0, scale: 0.8 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<motion.div
|
||
whileHover={{ scale: 1.1 }}
|
||
whileTap={{ scale: 0.9 }}
|
||
>
|
||
<GlassIconButton
|
||
icon={isVideoPlaying ? Pause : Play}
|
||
tooltip={isVideoPlaying ? "暂停播放" : "自动播放"}
|
||
onClick={toggleVideoPlay}
|
||
size="sm"
|
||
/>
|
||
</motion.div>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 展示分镜草图
|
||
return (
|
||
<div
|
||
className="relative w-full h-full rounded-lg"
|
||
onMouseEnter={() => setShowControls(true)}
|
||
onMouseLeave={() => setShowControls(false)}
|
||
>
|
||
{taskSketch[currentSketchIndex] ? (
|
||
<ProgressiveReveal
|
||
className="w-full h-full rounded-lg"
|
||
{...presets.main}
|
||
>
|
||
<img
|
||
key={currentSketchIndex}
|
||
src={taskSketch[currentSketchIndex].url}
|
||
alt={`分镜草图 ${currentSketchIndex + 1}`}
|
||
className="w-full h-full rounded-lg object-cover"
|
||
/>
|
||
</ProgressiveReveal>
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
|
||
{/* 动态渐变背景 */}
|
||
<motion.div
|
||
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
|
||
animate={{
|
||
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
||
}}
|
||
transition={{
|
||
duration: 5,
|
||
repeat: Infinity,
|
||
ease: "linear"
|
||
}}
|
||
style={{
|
||
backgroundSize: "200% 200%",
|
||
}}
|
||
/>
|
||
{/* 动态光效 */}
|
||
<motion.div
|
||
className="absolute inset-0 opacity-50"
|
||
style={{
|
||
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
||
}}
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
}}
|
||
transition={{
|
||
duration: 2,
|
||
repeat: Infinity,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
<motion.div
|
||
className="flex flex-col items-center gap-4 relative z-10"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.5 }}
|
||
>
|
||
<div className="relative">
|
||
<motion.div
|
||
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
rotate: [0, 180, 360],
|
||
}}
|
||
transition={{
|
||
duration: 4,
|
||
repeat: Infinity,
|
||
ease: "linear"
|
||
}}
|
||
/>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 操作按钮组 */}
|
||
<AnimatePresence>
|
||
{showControls && (
|
||
<>
|
||
{/* 顶部按钮组 */}
|
||
<motion.div
|
||
className="absolute top-4 right-4 flex gap-2"
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<GlassIconButton
|
||
icon={Edit3}
|
||
tooltip="编辑分镜"
|
||
onClick={() => handleEditModalOpen('1')}
|
||
/>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* 底部播放按钮 */}
|
||
<AnimatePresence>
|
||
<motion.div
|
||
className="absolute bottom-4 left-4"
|
||
initial={{ opacity: 0, scale: 0.8 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
exit={{ opacity: 0, scale: 0.8 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<motion.div
|
||
whileHover={{ scale: 1.1 }}
|
||
whileTap={{ scale: 0.9 }}
|
||
>
|
||
<GlassIconButton
|
||
icon={isPlaying ? Pause : Play}
|
||
tooltip={isPlaying ? "暂停播放" : "自动播放"}
|
||
onClick={togglePlay}
|
||
size="sm"
|
||
/>
|
||
</motion.div>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
|
||
{/* 播放进度指示器 */}
|
||
<AnimatePresence>
|
||
{isPlaying && (
|
||
<motion.div
|
||
className="absolute bottom-0 left-0 right-0 h-1 bg-blue-500/20"
|
||
initial={{ scaleX: 0 }}
|
||
animate={{ scaleX: 1 }}
|
||
exit={{ scaleX: 0 }}
|
||
transition={{ duration: 2, repeat: Infinity }}
|
||
style={{ transformOrigin: "left" }}
|
||
/>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="w-full h-full overflow-hidden">
|
||
<div className="flex h-full flex-col p-6 justify-center items-center pt-0">
|
||
<div className="container-H2sRZG">
|
||
<div className="splashContainer-otuV_A">
|
||
<div className="content-vPGYx8">
|
||
<div className="info-UUGkPJ">
|
||
{isLoading ? (
|
||
<>
|
||
<Skeleton className="h-8 w-64 mb-2" />
|
||
<Skeleton className="h-4 w-96" />
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="title-JtMejk">{taskObject?.projectName}:{taskObject?.taskName}</div>
|
||
{/* 实时反馈当前 currentLoadingText */}
|
||
{currentLoadingText === '任务完成' ? (
|
||
<motion.div
|
||
className="flex items-center gap-3 justify-center"
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.5 }}
|
||
>
|
||
<motion.div
|
||
className="w-2 h-2 rounded-full bg-emerald-500"
|
||
animate={{
|
||
scale: [1, 1.5, 1],
|
||
opacity: [1, 0.5, 1]
|
||
}}
|
||
transition={{
|
||
duration: 1,
|
||
repeat: Infinity,
|
||
repeatDelay: 0.2
|
||
}}
|
||
/>
|
||
<motion.div
|
||
className="flex items-center gap-1.5"
|
||
initial="hidden"
|
||
animate="visible"
|
||
variants={{
|
||
hidden: { opacity: 0 },
|
||
visible: { opacity: 1 }
|
||
}}
|
||
>
|
||
<motion.span
|
||
className="text-emerald-500 font-medium"
|
||
variants={{
|
||
hidden: { opacity: 0, y: 20 },
|
||
visible: { opacity: 1, y: 0 }
|
||
}}
|
||
transition={{ duration: 0.5 }}
|
||
>
|
||
{currentLoadingText}
|
||
</motion.span>
|
||
</motion.div>
|
||
<motion.div
|
||
className="w-2 h-2 rounded-full bg-emerald-500"
|
||
animate={{
|
||
scale: [1, 1.5, 1],
|
||
opacity: [1, 0.5, 1]
|
||
}}
|
||
transition={{
|
||
duration: 1,
|
||
repeat: Infinity,
|
||
repeatDelay: 0.2,
|
||
delay: 0.3
|
||
}}
|
||
/>
|
||
</motion.div>
|
||
) : (
|
||
<motion.div
|
||
className="flex items-center gap-2 justify-center"
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
<motion.div
|
||
className="w-1.5 h-1.5 rounded-full bg-blue-500"
|
||
animate={{
|
||
scale: [1, 1.5, 1],
|
||
opacity: [1, 0.5, 1]
|
||
}}
|
||
transition={{
|
||
duration: 1,
|
||
repeat: Infinity,
|
||
repeatDelay: 0.2
|
||
}}
|
||
/>
|
||
<motion.p
|
||
className="normalS400 subtitle-had8uE text-blue-500/80"
|
||
key={currentLoadingText}
|
||
initial={{ opacity: 0, x: -10 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
exit={{ opacity: 0, x: 10 }}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
{currentLoadingText}
|
||
</motion.p>
|
||
<motion.div
|
||
className="w-1.5 h-1.5 rounded-full bg-blue-500"
|
||
animate={{
|
||
scale: [1, 1.5, 1],
|
||
opacity: [1, 0.5, 1]
|
||
}}
|
||
transition={{
|
||
duration: 1,
|
||
repeat: Infinity,
|
||
repeatDelay: 0.2,
|
||
delay: 0.3
|
||
}}
|
||
/>
|
||
<motion.div
|
||
className="w-1.5 h-1.5 rounded-full bg-blue-500"
|
||
animate={{
|
||
scale: [1, 1.5, 1],
|
||
opacity: [1, 0.5, 1]
|
||
}}
|
||
transition={{
|
||
duration: 1,
|
||
repeat: Infinity,
|
||
repeatDelay: 0.2,
|
||
delay: 0.6
|
||
}}
|
||
/>
|
||
</motion.div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="media-Ocdu1O">
|
||
<div className="videoContainer-qteKNi" style={currentStep !== '6' ? { flex: 3 } : {}} ref={containerRef}>
|
||
{isLoading ? (
|
||
<Skeleton className="w-full aspect-video rounded-lg" />
|
||
) : (
|
||
<>
|
||
<div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }}>
|
||
{renderSketchContent()}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="imageGrid-ymZV9z hidden-scrollbar">
|
||
{isLoading ? (
|
||
<>
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
<Skeleton className="w-[128px] h-[128px] rounded-lg" />
|
||
</>
|
||
) : (
|
||
<div
|
||
ref={thumbnailsRef}
|
||
className="w-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hidden-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing"
|
||
onMouseDown={handleMouseDown}
|
||
onMouseMove={handleMouseMove}
|
||
onMouseUp={handleMouseUp}
|
||
onMouseLeave={() => setIsDragging(false)}
|
||
>
|
||
{Number(currentStep) === 6 ? null : (
|
||
<>
|
||
{(Number(currentStep) > 2 && Number(currentStep) < 6) ? (
|
||
<>
|
||
{taskSketch.map((sketch, index) => (
|
||
<div
|
||
key={`video-${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 && setCurrentSketchIndex(index)}
|
||
>
|
||
<ProgressiveReveal
|
||
{...presets.thumbnail}
|
||
delay={index * 0.1}
|
||
customVariants={{
|
||
hidden: {
|
||
opacity: 0,
|
||
scale: 0.95,
|
||
filter: "blur(10px)"
|
||
},
|
||
visible: {
|
||
opacity: 1,
|
||
scale: 1,
|
||
filter: "blur(0px)",
|
||
transition: {
|
||
duration: 0.8,
|
||
ease: [0.23, 1, 0.32, 1],
|
||
opacity: { duration: 0.6, ease: "easeInOut" },
|
||
scale: { duration: 1, ease: "easeOut" },
|
||
filter: { duration: 0.8, ease: "easeOut", delay: 0.2 }
|
||
}
|
||
}
|
||
}}
|
||
loadingBgConfig={{
|
||
...presets.thumbnail.loadingBgConfig,
|
||
glowOpacity: 0.4,
|
||
duration: 4
|
||
}}
|
||
>
|
||
<div className="w-full h-full transform hover:scale-105 transition-transform duration-500">
|
||
{taskVideos[index] ? (
|
||
<video
|
||
className="w-full h-full object-cover"
|
||
src={taskVideos[index].url}
|
||
muted
|
||
playsInline
|
||
loop
|
||
poster={sketch.url}
|
||
/>
|
||
) : (
|
||
<img
|
||
className="w-full h-full object-cover"
|
||
src={sketch.url}
|
||
alt={`缩略图 ${index + 1}`}
|
||
/>
|
||
)}
|
||
</div>
|
||
</ProgressiveReveal>
|
||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||
<span className="text-xs text-white/90">场景 {index + 1}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</>
|
||
) : (
|
||
<>
|
||
{renderedSketches}
|
||
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && (
|
||
<motion.div
|
||
className="relative aspect-video rounded-lg overflow-hidden"
|
||
initial={{ opacity: 0, scale: 0.8 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
{/* 动态渐变背景 */}
|
||
<motion.div
|
||
className="absolute inset-0 bg-gradient-to-r from-cyan-300 via-sky-400 to-blue-500"
|
||
animate={{
|
||
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
||
}}
|
||
transition={{
|
||
duration: 5,
|
||
repeat: Infinity,
|
||
ease: "linear"
|
||
}}
|
||
style={{
|
||
backgroundSize: "200% 200%",
|
||
}}
|
||
/>
|
||
{/* 动态光效 */}
|
||
<motion.div
|
||
className="absolute inset-0 opacity-50"
|
||
style={{
|
||
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
||
}}
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
}}
|
||
transition={{
|
||
duration: 2,
|
||
repeat: Infinity,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
<div className="absolute inset-0 flex items-center justify-center">
|
||
<div className="relative">
|
||
<motion.div
|
||
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
rotate: [0, 180, 360],
|
||
}}
|
||
transition={{
|
||
duration: 4,
|
||
repeat: Infinity,
|
||
ease: "linear"
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||
<span className="text-xs text-white/90">场景 {sketchCount + 1}</span>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* AI 建议栏 */}
|
||
<AISuggestionBar
|
||
suggestions={mockSuggestions}
|
||
onSuggestionClick={handleSuggestionClick}
|
||
onSubmit={handleSubmit}
|
||
placeholder="请输入你的想法,或点击预设词条获取 AI 建议..."
|
||
/>
|
||
|
||
<EditModal
|
||
isOpen={isEditModalOpen}
|
||
activeEditTab={activeEditTab}
|
||
onClose={() => setIsEditModalOpen(false)}
|
||
taskStatus={taskObject?.taskStatus || '1'}
|
||
taskSketch={taskSketch}
|
||
sketchVideo={taskVideos}
|
||
currentSketchIndex={currentSketchIndex}
|
||
onSketchSelect={setCurrentSketchIndex}
|
||
/>
|
||
</div>
|
||
)
|
||
} |