From a5a6da472dbc15c7d0602dcace78fb64fe1efc68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Tue, 1 Jul 2025 22:37:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A9=BA=E9=A1=B5=E9=9D=A2=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/common/EmptyStateAnimation.tsx | 920 ++++++++++++++++++++ components/pages/create-to-video2.tsx | 263 +++--- components/pages/style/create-to-video2.css | 10 + package-lock.json | 14 + package.json | 2 + 5 files changed, 1064 insertions(+), 145 deletions(-) create mode 100644 components/common/EmptyStateAnimation.tsx diff --git a/components/common/EmptyStateAnimation.tsx b/components/common/EmptyStateAnimation.tsx new file mode 100644 index 0000000..b86b181 --- /dev/null +++ b/components/common/EmptyStateAnimation.tsx @@ -0,0 +1,920 @@ +"use client"; + +import { useState, useEffect, useRef } from 'react'; +import { ArrowUp } from 'lucide-react'; +import Image from 'next/image'; +import gsap from 'gsap'; +import { SplitText } from 'gsap/SplitText'; + +// 注册 SplitText 插件 +if (typeof window !== 'undefined') { + gsap.registerPlugin(SplitText); +} + +const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward'; + +const AnimatedText = ({ text, onComplete, shouldStart }: { text: string; onComplete: () => void; shouldStart: boolean }) => { + const containerRef = useRef(null); + const titleRef = useRef(null); + const inputContainerRef = useRef(null); + const inputRef = useRef(null); + const cursorRef = useRef(null); + const mouseRef = useRef(null); + const buttonRef = useRef(null); + const animationRef = useRef(null); + const [displayText, setDisplayText] = useState(''); + + const demoText = "a cute capybara with an orange on its head"; + + useEffect(() => { + if (containerRef.current && typeof window !== 'undefined' && shouldStart) { + // 清理之前的动画 + if (animationRef.current) { + animationRef.current.kill(); + } + + // 重置状态 + setDisplayText(''); + if (containerRef.current) { + containerRef.current.style.opacity = '1'; + } + + // 设置初始状态 + gsap.set([titleRef.current, inputContainerRef.current], { + opacity: 1 + }); + + // 创建主时间轴 + const mainTl = gsap.timeline(); + + // 1. 显示标题和输入框 + mainTl.fromTo(titleRef.current, { + y: -30, + opacity: 0 + }, { + y: 0, + opacity: 1, + duration: 0.5, + ease: "power2.out" + }) + .fromTo(inputContainerRef.current, { + scale: 0.9, + opacity: 0 + }, { + scale: 1, + opacity: 1, + duration: 0.3, + ease: "back.out(1.7)" + }, "-=0.3"); + + // 2. 显示鼠标指针并移动到输入框 + mainTl.fromTo(mouseRef.current, + { opacity: 0, x: -100, y: -50 }, + { + opacity: 1, + x: -150, + y: 0, + duration: 0.5, + ease: "power2.out" + }, + "+=0.5" + ); + + // 3. 鼠标移动到输入框中心 + mainTl.to(mouseRef.current, { + x: 0, + y: 0, + duration: 0.3, + ease: "power2.inOut" + }); + + // 4. 输入框聚焦效果 + mainTl.to(inputRef.current, { + scale: 1.05, + rotationY: 1, + rotationX: 15, + transformOrigin: "center center", + boxShadow: ` + inset 0 3px 0 rgba(255,255,255,0.35), + inset 0 -3px 0 rgba(0,0,0,0.2), + inset 3px 0 0 rgba(255,255,255,0.15), + inset -3px 0 0 rgba(0,0,0,0.08), + 0 0 0 3px rgba(59, 130, 246, 0.4), + 0 4px 8px rgba(0,0,0,0.15), + 0 12px 24px rgba(0,0,0,0.2), + 0 20px 40px rgba(0,0,0,0.15), + 0 0 0 1px rgba(59, 130, 246, 0.6) + `, + borderColor: 'rgba(79, 70, 229, 0.8)', + duration: 0.2, + ease: "back.out(1.7)" + }, "+=0.2"); + + // 5. 显示按钮 + mainTl.fromTo(buttonRef.current, + { scale: 0, opacity: 0 }, + { scale: 1, opacity: 1, duration: 0.2, ease: "back.out(1.7)" }, + "+=0.3" + ); + + // 隐藏鼠标指针 + mainTl.to(mouseRef.current, { + opacity: 0, + duration: 0.2 + }, "+=0.2"); + + // 6. 打字动画 + const typingDuration = demoText.length * 0.03; + let currentChar = 0; + + mainTl.to({}, { + duration: typingDuration, + ease: "none", + onUpdate: function() { + const progress = this.progress(); + const targetChar = Math.floor(progress * demoText.length); + if (targetChar !== currentChar && targetChar <= demoText.length) { + currentChar = targetChar; + setDisplayText(demoText.slice(0, currentChar)); + } + } + }, "+=0.3"); + + // 7. 隐藏光标 + mainTl.to(cursorRef.current, { + opacity: 0, + duration: 0.2 + }, "+=0.2"); + + // 重新显示鼠标指针并移动到按钮 + mainTl.fromTo(mouseRef.current, + { opacity: 0, x: 0, y: 0 }, + { + opacity: 1, + duration: 0.2, + ease: "power2.out" + }, + "-=0.3" + ); + + // 鼠标移动到按钮 + mainTl.to(mouseRef.current, { + x: 20, + y: 10, + duration: 0.3, + ease: "power2.inOut" + }, "+=0.5"); + + // 8. 输入框失焦效果 + mainTl.to(inputRef.current, { + scale: 1, + rotationY: 0, + rotationX: 0, + transformOrigin: "center center", + boxShadow: ` + inset 0 2px 0 rgba(255,255,255,0.25), + inset 0 -2px 0 rgba(0,0,0,0.15), + inset 2px 0 0 rgba(255,255,255,0.1), + inset -2px 0 0 rgba(0,0,0,0.05), + 0 2px 4px rgba(0,0,0,0.1), + 0 8px 16px rgba(0,0,0,0.15), + 0 16px 32px rgba(0,0,0,0.1), + 0 0 0 1px rgba(255,255,255,0.1) + `, + duration: 0.2, + ease: "power2.out" + }, "+=0.2"); + + // 点击效果 + mainTl.to(mouseRef.current, { + scale: 0.8, + duration: 0.1, + ease: "power2.out", + yoyo: true, + repeat: 1 + }) + .to(buttonRef.current, { + scale: 0.95, + duration: 0.1, + ease: "power2.out", + yoyo: true, + repeat: 1 + }, "-=0.2") + .to(buttonRef.current, { + boxShadow: "0 0 20px rgba(79, 70, 229, 0.6)", + duration: 0.1, + ease: "power2.out" + }, "-=0.1"); + + // 停留展示时间 + mainTl.to({}, { duration: 0.1 }); + + // 退场动画 + mainTl.to(containerRef.current, { + opacity: 0, + y: -50, + duration: 0.5, + ease: "power2.in", + onComplete: () => { + onComplete(); + } + }); + + animationRef.current = mainTl; + } + + return () => { + if (animationRef.current) { + animationRef.current.kill(); + } + }; + }, [shouldStart, onComplete]); + + return ( +
+ {/* 标题 */} +
+

+ You can input script to generate video +

+
+ + {/* 输入框模拟 */} +
+
+ + {displayText} + +
+
+ + {/* Create按钮 */} +
+ + Create +
+ + {/* 鼠标指针 */} +
+ + + +
+
+
+ ); +}; + +// 阶段文字解释组件 +const StageExplanation = ({ + text, + stage, + shouldStart, + onComplete +}: { + text: string; + stage: 'images' | 'replacing' | 'merging'; + shouldStart: boolean; + onComplete: () => void; +}) => { + const textRef = useRef(null); + const splitRef = useRef(null); + const animationRef = useRef(null); + + useEffect(() => { + if (textRef.current && typeof window !== 'undefined' && shouldStart) { + // 重置显示 + textRef.current.style.opacity = '1'; + + // 清理之前的动画 + if (splitRef.current) { + splitRef.current.revert(); + } + if (animationRef.current) { + animationRef.current.kill(); + } + + // 创建分割文本 + splitRef.current = new SplitText(textRef.current, { type: "words" }); + + let tl = gsap.timeline({ + onComplete: () => { + // 延迟后开始退场动画 + setTimeout(() => { + const exitTl = gsap.timeline({ + onComplete: () => { + onComplete(); + } + }); + + // 不同阶段的退场动画 + switch (stage) { + case 'images': + exitTl.to(splitRef.current.words, { + y: -50, + opacity: 0, + duration: 0.2, + ease: "power2.in", + stagger: 0.05 + }); + break; + case 'replacing': + exitTl.to(splitRef.current.words, { + scale: 0, + rotation: 360, + opacity: 0, + duration: 0.4, + ease: "back.in(2)", + stagger: 0.08 + }); + break; + case 'merging': + exitTl.to(splitRef.current.words, { + x: "random(-200, 200)", + y: "random(-100, -200)", + opacity: 0, + duration: 0.7, + ease: "power3.in", + stagger: 0.06 + }); + break; + } + }, 1000); // 显示1秒 + } + }); + + // 不同阶段的入场动画 + switch (stage) { + case 'images': + // 从下方弹跳进入 + tl.from(splitRef.current.words, { + y: 100, + opacity: 0, + duration: 0.5, + ease: "bounce.out", + stagger: 0.1 + }); + break; + case 'replacing': + // 旋转缩放进入 + tl.from(splitRef.current.words, { + scale: 0, + rotation: -180, + opacity: 0, + duration: 0.4, + ease: "back.out(2)", + stagger: 0.08 + }); + break; + case 'merging': + // 从四周飞入 + tl.from(splitRef.current.words, { + x: "random(-300, 300)", + y: "random(-200, 200)", + opacity: 0, + duration: 0.7, + ease: "power3.out", + stagger: 0.06 + }); + break; + } + + animationRef.current = tl; + } + + return () => { + if (animationRef.current) { + animationRef.current.kill(); + } + if (splitRef.current) { + splitRef.current.revert(); + } + }; + }, [shouldStart, stage, onComplete]); + + return ( +
+

{text}

+
+ ); +}; + +const ImageQueue = ({ shouldStart, onComplete }: { shouldStart: boolean; onComplete: () => void }) => { + const imagesRef = useRef(null); + const [currentStage, setCurrentStage] = useState<'images' | 'replacing' | 'merging'>('images'); + const [replacementIndex, setReplacementIndex] = useState(0); + const [showStageText, setShowStageText] = useState(false); + const finalVideoContainerRef = useRef(null); + + const imageUrls = [ + '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 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' + ]; + + const finalVideoUrl = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; + + // 阶段文字映射 + const stageTexts = { + images: 'Then, these are the key frames split from the storyboard', + replacing: 'Then, these are the videos corresponding to the split storyboard', + merging: 'Finally, efficiently edit the perfect video' + }; + + // 重置函数 + const resetComponent = () => { + // 清理最终视频容器 + if (finalVideoContainerRef.current) { + finalVideoContainerRef.current.remove(); + finalVideoContainerRef.current = null; + } + + // 额外清理:查找并移除所有可能的最终视频容器 + const allFinalVideos = document.querySelectorAll('div[class*="fixed"][class*="top-1/2"][class*="w-[400px]"]'); + allFinalVideos.forEach(container => { + if (container.parentNode === document.body) { + container.remove(); + } + }); + + // 重置所有状态 + setCurrentStage('images'); + setReplacementIndex(0); + setShowStageText(false); + + // 重置DOM结构 + if (imagesRef.current) { + imagesRef.current.style.display = 'flex'; + Array.from(imagesRef.current.children).forEach((container, index) => { + const htmlContainer = container as HTMLElement; + + // 移除视频元素 + const videos = htmlContainer.querySelectorAll('video'); + videos.forEach(video => video.remove()); + + // 使用GSAP重置容器样式 + gsap.set(htmlContainer, { + position: 'static', + x: 0, + y: index % 2 ? 20 : -20, + opacity: 1, + scale: 1, + rotation: 0 + }); + + // 确保图片显示 + const img = htmlContainer.querySelector('img'); + if (img) { + img.style.opacity = '1'; + } + }); + } + }; + + // 当shouldStart改变时重置并开始新的动画 + useEffect(() => { + if (shouldStart) { + resetComponent(); + // 延迟显示阶段文字 + setTimeout(() => { + setShowStageText(true); + }, 500); + } + }, [shouldStart]); + + // 阶段文字完成回调 + const handleStageTextComplete = () => { + setShowStageText(false); + // 根据当前阶段决定下一步 + if (currentStage === 'images') { + // 开始图片入场动画 + startImagesAnimation(); + } else if (currentStage === 'replacing') { + // 继续图片替换视频 + // replacementIndex会自动处理 + } else if (currentStage === 'merging') { + // 开始合并动画 + startMergingAnimation(); + } + }; + + // 图片入场动画 + const startImagesAnimation = () => { + if (imagesRef.current) { + const images = imagesRef.current.children; + + gsap.set(images, { + x: window.innerWidth, + opacity: 0, + rotation: 45 + }); + + const tl = gsap.timeline({ + onComplete: () => { + // 图片动画完成后,切换到替换阶段 + setTimeout(() => { + setCurrentStage('replacing'); + setShowStageText(true); + }, 1000); + } + }); + + tl.to(images, { + x: -100, + opacity: 1, + rotation: 0, + duration: 1, + ease: "elastic.out(1, 0.5)", + stagger: { + amount: 1, + from: "random" + } + }); + } + }; + + // 开始合并动画 + const startMergingAnimation = () => { + if (imagesRef.current) { + const containers = Array.from(imagesRef.current.children) as HTMLElement[]; + const videos = containers.map(container => container.querySelector('video')).filter(Boolean) as HTMLVideoElement[]; + + if (videos.length > 0) { + // 创建最终的大视频容器 + const finalVideoContainer = document.createElement('div'); + finalVideoContainer.className = 'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] rounded-lg overflow-hidden shadow-2xl opacity-0'; + + const finalVideo = document.createElement('video'); + finalVideo.src = finalVideoUrl; + finalVideo.autoplay = true; + finalVideo.loop = true; + finalVideo.muted = true; + finalVideo.playsInline = true; + finalVideo.className = 'w-full h-full object-cover'; + + finalVideoContainer.appendChild(finalVideo); + document.body.appendChild(finalVideoContainer); + finalVideoContainerRef.current = finalVideoContainer; + + // 等待最终视频准备就绪 + const onFinalVideoReady = () => { + finalVideo.removeEventListener('canplay', onFinalVideoReady); + + finalVideo.play().then(() => { + const tl = gsap.timeline({ + onComplete: () => { + // 移除原有的小视频容器 + if (imagesRef.current) { + imagesRef.current.style.display = 'none'; + } + + // 延迟3秒后调用完成回调,开始下一轮循环 + setTimeout(() => { + onComplete(); + }, 3000); + } + }); + + // 执行合并动画 + tl.to(containers, { + rotation: "random(-720, 720)", + x: "random(-200, 200)", + y: "random(-200, 200)", + scale: 0.8, + duration: 0.7, + ease: "power2.out", + stagger: 0.1 + }) + .to(containers, { + x: 0, + y: 0, + rotation: 0, + scale: 1, + duration: 0.5, + ease: "power2.inOut", + stagger: 0.05 + }) + .to(containers, { + scale: 1.2, + duration: 0.1, + ease: "power2.inOut" + }) + .to(finalVideoContainer, { + opacity: 1, + scale: 1, + duration: 0.2, + ease: "power2.out" + }, "-=0.2") + .to(containers, { + opacity: 0, + scale: 0.8, + duration: 0.2, + ease: "power2.in" + }, "-=0.5") + .to(finalVideoContainer, { + scale: 1, + duration: 0.1, + ease: "back.out(1.7)" + }); + }).catch(() => { + console.error('最终视频播放失败'); + finalVideoContainer.remove(); + onComplete(); + }); + }; + + finalVideo.addEventListener('canplay', onFinalVideoReady); + finalVideo.addEventListener('error', () => { + console.error('最终视频加载失败'); + finalVideoContainer.remove(); + onComplete(); + }); + + finalVideo.load(); + } + } + }; + + // 图片到视频的替换动画 + useEffect(() => { + if (currentStage === 'replacing' && imagesRef.current && replacementIndex < imageUrls.length && shouldStart && !showStageText) { + const targetContainer = imagesRef.current.children[replacementIndex] as HTMLElement; + + if (targetContainer) { + // 创建视频元素 + const video = document.createElement('video'); + video.src = videoUrls[replacementIndex]; + video.autoplay = true; + video.loop = true; + video.muted = true; + video.playsInline = true; + video.className = 'absolute inset-0 w-full h-full object-cover opacity-0'; + + // 添加视频到容器中 + targetContainer.style.position = 'relative'; + targetContainer.appendChild(video); + + // 等待视频准备好播放 + const onCanPlay = () => { + video.removeEventListener('canplay', onCanPlay); + video.removeEventListener('error', onError); + + // 开始播放 + video.play().then(() => { + // 创建无缝替换动画 + const tl = gsap.timeline({ + onComplete: () => { + // 移除图片元素 + const img = targetContainer.querySelector('img'); + if (img) { + img.remove(); + } + + // 移除绝对定位,让视频正常占位 + video.className = 'w-full h-full object-cover'; + + // 检查是否是最后一个视频 + if (replacementIndex === imageUrls.length - 1) { + // 最后一个视频替换完成,切换到合并阶段 + setTimeout(() => { + setCurrentStage('merging'); + setShowStageText(true); + }, 1000); + } else { + // 延迟后替换下一个 + setTimeout(() => { + setReplacementIndex(prev => prev + 1); + }, 600); + } + } + }); + + // 同时淡入视频和淡出图片 + tl.to(video, { + opacity: 1, + duration: 0.1, + ease: "power2.inOut" + }) + .to(targetContainer.querySelector('img'), { + opacity: 0, + duration: 0.1, + ease: "power2.inOut" + }, 0) + .to(targetContainer, { + scale: 1.02, + duration: 0.1, + ease: "power2.out", + yoyo: true, + repeat: 1 + }, 0.1); + }).catch(onError); + }; + + const onError = () => { + video.removeEventListener('canplay', onCanPlay); + video.removeEventListener('error', onError); + console.error('视频加载或播放失败:', videoUrls[replacementIndex]); + + // 移除失败的视频元素 + video.remove(); + + // 跳过到下一个 + if (replacementIndex === imageUrls.length - 1) { + setTimeout(() => { + setCurrentStage('merging'); + setShowStageText(true); + }, 500); + } else { + setTimeout(() => { + setReplacementIndex(prev => prev + 1); + }, 500); + } + }; + + video.addEventListener('canplay', onCanPlay); + video.addEventListener('error', onError); + + // 加载视频 + video.load(); + } + } + }, [currentStage, replacementIndex, shouldStart, showStageText]); + + return ( + <> + {showStageText && ( + + )} +
+ {imageUrls.map((url, index) => ( +
+ {`reference-${index +
+ ))} +
+ + ); +}; + +// 主要的空状态动画组件 +export const EmptyStateAnimation = () => { + const [showText, setShowText] = useState(false); + const [showImages, setShowImages] = useState(false); + const [animationCycle, setAnimationCycle] = useState(0); + const [isClient, setIsClient] = useState(false); + + // 全局清理函数 + const globalCleanup = () => { + // 清理所有可能的最终视频容器 + const existingFinalVideos = document.querySelectorAll('div[class*="fixed"][class*="top-1/2"][class*="w-[400px]"]'); + existingFinalVideos.forEach(container => { + if (container.parentNode === document.body) { + container.remove(); + } + }); + + // 清理所有body下的视频相关元素 + const bodyChildren = Array.from(document.body.children); + bodyChildren.forEach(child => { + if (child instanceof HTMLElement && + (child.querySelector('video') || child.tagName === 'VIDEO')) { + child.remove(); + } + }); + }; + + // 循环控制 + const startNextCycle = () => { + // 先进行全局清理 + globalCleanup(); + + setAnimationCycle(prev => prev + 1); + setShowImages(false); + + // 延迟一下再显示文字,确保清理完成 + setTimeout(() => { + setShowText(true); + }, 100); + }; + + const handleTextComplete = () => { + setShowText(false); + setTimeout(() => { + setShowImages(true); + }, 100); + }; + + const handleImagesComplete = () => { + setShowImages(false); + // 延迟后开始新的循环 + setTimeout(() => { + startNextCycle(); + }, 1000); + }; + + // 开始第一轮动画 + useEffect(() => { + if (isClient && animationCycle === 0) { + startNextCycle(); + } + }, [isClient]); + + // 组件卸载时清理 + useEffect(() => { + return () => { + globalCleanup(); + }; + }, []); + + useEffect(() => { + setIsClient(true); + }, []); + + if (!isClient) { + return null; + } + + return ( +
+ {showText && ( + + )} + {showImages && ( + + )} +
+ ); +}; \ No newline at end of file diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index 37d19c6..711f71f 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -20,6 +20,7 @@ import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/api/enums"; import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode"; import { getUploadTokenWithDomain, uploadToQiniu } from "@/api/common"; import { convertScriptToScene, convertVideoToScene } from "@/api/video_flow"; +import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation'; const JoyrideNoSSR = dynamic(() => import('react-joyride'), { ssr: false, @@ -27,6 +28,11 @@ const JoyrideNoSSR = dynamic(() => import('react-joyride'), { // 导入Step类型 import type { Step } from 'react-joyride'; +// interface Step { +// target: string; +// content: string; +// placement?: 'top' | 'bottom' | 'left' | 'right'; +// } // 添加自定义滚动条样式 const scrollbarStyles = ` @@ -72,6 +78,8 @@ export function CreateToVideo2() { const [runTour, setRunTour] = useState(true); const [episodeId, setEpisodeId] = useState(0); const [isCreating, setIsCreating] = useState(false); + const [generatedVideoList, setGeneratedVideoList] = useState([]); + const [projectName, setProjectName] = useState(localStorage.getItem('projectName') || '默认名称'); const handleUploadVideo = async () => { console.log('upload video'); @@ -84,13 +92,13 @@ export function CreateToVideo2() { if (file) { try { setIsUploading(true); - + // 获取上传token const { token } = await getUploadTokenWithDomain(); - + // 上传到七牛云 const videoUrl = await uploadToQiniu(file, token); - + // 上传成功,设置视频URL setVideoUrl(videoUrl); console.log('视频上传成功:', videoUrl); @@ -113,7 +121,7 @@ export function CreateToVideo2() { status: 1, summary: script }; - + // 调用创建剧集API const episodeResponse = await createScriptEpisode(episodeData); if (episodeResponse.code !== 0) { @@ -121,13 +129,13 @@ export function CreateToVideo2() { alert(`创建剧集失败: ${episodeResponse.message}`); return; } - let episodeId = episodeResponse.data.id ; + let episodeId = episodeResponse.data.id; if (videoUrl || script) { try { setIsCreating(true); let convertResponse; - + // 根据选中的选项卡调用相应的API if (activeTab === 'script') { // 剧本模式:调用convertScriptToScene (第43-56行) @@ -150,18 +158,18 @@ export function CreateToVideo2() { } // 更新剧集 const updateEpisodeData: UpdateScriptEpisodeRequest = { - id: episodeId, - atmosphere: convertResponse.data.atmosphere, - summary: convertResponse.data.summary, - scene: convertResponse.data.scene, - characters: convertResponse.data.characters, + id: episodeId, + atmosphere: convertResponse.data.atmosphere, + summary: convertResponse.data.summary, + scene: convertResponse.data.scene, + characters: convertResponse.data.characters, }; const updateEpisodeResponse = await updateScriptEpisode(updateEpisodeData); - + // 检查转换结果 if (convertResponse.code === 0) { - // 成功创建后跳转到work-flow页面, 并设置episodeId 和 projectType - router.push(`/create/work-flow?episodeId=${episodeResponse.data.id}`); + // 成功创建后跳转到work-flow页面, 并设置episodeId 和 projectType + router.push(`/create/work-flow?episodeId=${episodeResponse.data.id}`); } else { alert(`转换失败: ${convertResponse.message}`); } @@ -279,16 +287,16 @@ export function CreateToVideo2() { // 创建范围对象 const range = document.createRange(); const selection = window.getSelection(); - + // 获取编辑器内的文本节点 const textNode = Array.from(editorRef.current.childNodes).find( node => node.nodeType === Node.TEXT_NODE ) || editorRef.current.appendChild(document.createTextNode(script)); - + // 设置范围到文本末尾 range.setStart(textNode, script.length); range.setEnd(textNode, script.length); - + // 应用选择 selection?.removeAllRanges(); selection?.addRange(range); @@ -353,10 +361,10 @@ export function CreateToVideo2() { }, []); return ( -
{isClient && ( )} -
-
-
- empty_video -
- Generated videos will appear here. - handleStartCreating()}>Start creating! -
-
-
-
- +
+ {/* 空状态 */} + {/* 工具栏 */}
-
+
{isExpanded ? (
setIsExpanded(false)}> {/* 图标 展开按钮 */} @@ -447,12 +419,12 @@ export function CreateToVideo2() {
setActiveTab('script')}> script
-
setActiveTab('clone')}> - clone +
setActiveTab('clone')}> + clone
- -
+ +
{activeTab === 'clone' && (
@@ -462,7 +434,7 @@ export function CreateToVideo2() {
- Add Video + Add Video
{videoUrl && ( @@ -476,92 +448,93 @@ export function CreateToVideo2() { )} {activeTab === 'script' && (
-
-
setIsFocus(false)} - onInput={handleEditorChange} - suppressContentEditableWarning - > - {script} -
-
- Describe the content you want to create. Get an - setInputText(ideaText)} +
+
setIsFocus(false)} + onInput={handleEditorChange} + suppressContentEditableWarning > - idea - + {script} +
+
+ Describe the content you want to create. Get an + setInputText(ideaText)} + > + idea + +
-
)} -
-
-
-
- -
- - - {selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'} - - -
-
+
+
+
+
+ +
+ + + {selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'} + + +
+
- -
-
-
+ +
+
+
+
-
-
-
- {isCreating ? ( - <> - - Creating... - - ) : ( - <> - - Create - - )} +
+
+ {isCreating ? ( + <> + + Creating... + + ) : ( + <> + + Create + + )} +
diff --git a/components/pages/style/create-to-video2.css b/components/pages/style/create-to-video2.css index 10a1175..0f2ee3b 100644 --- a/components/pages/style/create-to-video2.css +++ b/components/pages/style/create-to-video2.css @@ -136,4 +136,14 @@ .mode-dropdown.ant-dropdown .ant-dropdown-menu-item-selected { background: rgba(255, 255, 255, 0.1); +} + +.animated-text { + color: #dfdcff; + font-size: clamp(2rem, 12rem, 1vw); + line-height: 1.2; + box-sizing: border-box; + width: 100%; + text-align: center; + perspective: 500px; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c5bccd9..8728540 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", + "@types/gsap": "^1.20.2", "@types/node": "20.6.2", "@types/react": "18.2.22", "@types/react-beautiful-dnd": "^13.1.8", @@ -57,6 +58,7 @@ "eslint": "8.49.0", "eslint-config-next": "13.5.1", "framer-motion": "^12.19.1", + "gsap": "^3.13.0", "input-otp": "^1.2.4", "lodash": "^4.17.21", "lucide-react": "^0.446.0", @@ -6524,6 +6526,12 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/gsap": { + "version": "1.20.2", + "resolved": "https://registry.npmmirror.com/@types/gsap/-/gsap-1.20.2.tgz", + "integrity": "sha512-i9nUsnS32+VTgoX5IlaCYukJpCoB3c6h3bZvO67aIRdb3z8NFTWgeUDpQutLyb1ujowp6nw37qLQNvEowqq8yw==", + "license": "MIT" + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.6", "resolved": "https://registry.npmmirror.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", @@ -9061,6 +9069,12 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/gsap": { + "version": "3.13.0", + "resolved": "https://registry.npmmirror.com/gsap/-/gsap-3.13.0.tgz", + "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", diff --git a/package.json b/package.json index fb079e3..4e83f67 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", + "@types/gsap": "^1.20.2", "@types/node": "20.6.2", "@types/react": "18.2.22", "@types/react-beautiful-dnd": "^13.1.8", @@ -58,6 +59,7 @@ "eslint": "8.49.0", "eslint-config-next": "13.5.1", "framer-motion": "^12.19.1", + "gsap": "^3.13.0", "input-otp": "^1.2.4", "lodash": "^4.17.21", "lucide-react": "^0.446.0",