video-flow-b/components/pages/work-flow.tsx
2025-06-29 01:34:20 +08:00

1214 lines
46 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, { 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
taskCreatedAt: new Date().toISOString(),
taskUpdatedAt: new Date().toISOString(),
};
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>
)
}