空页面动画展示

This commit is contained in:
北枳 2025-07-01 22:37:55 +08:00
parent b6d608fd54
commit a5a6da472d
5 changed files with 1064 additions and 145 deletions

View File

@ -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<HTMLDivElement>(null);
const titleRef = useRef<HTMLDivElement>(null);
const inputContainerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLDivElement>(null);
const cursorRef = useRef<HTMLDivElement>(null);
const mouseRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLDivElement>(null);
const animationRef = useRef<any>(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 (
<div ref={containerRef} className="animated-text flex flex-col items-center gap-6 text-center">
{/* 标题 */}
<div ref={titleRef} className="title-text">
<h2 className="text-xl font-medium text-white/70 mb-2">
You can input script to generate video
</h2>
</div>
{/* 输入框模拟 */}
<div ref={inputContainerRef} className="input-container relative" style={{ perspective: '1000px' }}>
<div
ref={inputRef}
className="relative w-[600px] h-[60px] bg-gradient-to-br from-white/12 to-white/4 backdrop-blur-md rounded-lg px-4 py-3 flex items-center transition-all duration-300"
style={{
background: 'rgba(233,231,231,0.9)',
boxShadow: `
inset 0 2px 0 rgba(233,231,231,0.25),
inset 0 -2px 0 rgba(0,0,0,0.15),
inset 2px 0 0 rgba(233,231,231,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(233,231,231,0.1)
`
}}
>
<span className="text-[#000] text-base font-mono">
{displayText}
</span>
<div
ref={cursorRef}
className="w-0.5 h-5 bg-blue-400 ml-1 animate-pulse"
/>
</div>
{/* Create按钮 */}
<div
ref={buttonRef}
className="absolute right-2 top-1/2 -translate-y-1/2 opacity-0 bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2 rounded-md text-sm font-medium cursor-pointer transition-all duration-200 flex items-center gap-2"
>
<ArrowUp className="w-4 h-4" />
Create
</div>
{/* 鼠标指针 */}
<div
ref={mouseRef}
className="absolute right-8 top-1/2 -translate-y-1/2 opacity-0 pointer-events-none z-10"
>
<svg
width="30"
height="30"
viewBox="0 0 24 24"
fill="none"
className="text-white drop-shadow-lg"
>
<path
d="M8 2L8 22L12 18L16 22L22 16L12 6L8 2Z"
fill="currentColor"
stroke="rgba(0,0,0,0.2)"
strokeWidth="1"
/>
</svg>
</div>
</div>
</div>
);
};
// 阶段文字解释组件
const StageExplanation = ({
text,
stage,
shouldStart,
onComplete
}: {
text: string;
stage: 'images' | 'replacing' | 'merging';
shouldStart: boolean;
onComplete: () => void;
}) => {
const textRef = useRef<HTMLDivElement>(null);
const splitRef = useRef<any>(null);
const animationRef = useRef<any>(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 (
<div
ref={textRef}
className="fixed top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center z-10"
>
<p className="text-lg font-medium text-white/80 max-w-[600px]">{text}</p>
</div>
);
};
const ImageQueue = ({ shouldStart, onComplete }: { shouldStart: boolean; onComplete: () => void }) => {
const imagesRef = useRef<HTMLDivElement>(null);
const [currentStage, setCurrentStage] = useState<'images' | 'replacing' | 'merging'>('images');
const [replacementIndex, setReplacementIndex] = useState(0);
const [showStageText, setShowStageText] = useState(false);
const finalVideoContainerRef = useRef<HTMLDivElement | null>(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 && (
<StageExplanation
text={stageTexts[currentStage]}
stage={currentStage}
shouldStart={showStageText}
onComplete={handleStageTextComplete}
/>
)}
<div ref={imagesRef} className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex gap-4">
{imageUrls.map((url, index) => (
<div
key={index}
className="w-[200px] h-[150px] rounded-lg overflow-hidden shadow-lg"
style={{
transform: `translateY(${index % 2 ? '20px' : '-20px'})`
}}
>
<Image
src={url}
alt={`reference-${index + 1}`}
width={200}
height={150}
className="w-full h-full object-cover"
/>
</div>
))}
</div>
</>
);
};
// 主要的空状态动画组件
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 (
<div className='flex flex-col justify-center items-center'>
{showText && (
<AnimatedText
key={`text-${animationCycle}`}
text="Choose to input a script below or select a video to replicate. Before clicking the create button, you can choose the generation mode - automatic or manual intervention, as well as the resolution of the generated video. Once confirmed, you can click the create button."
onComplete={handleTextComplete}
shouldStart={showText}
/>
)}
{showImages && (
<ImageQueue
key={`images-${animationCycle}`}
shouldStart={showImages}
onComplete={handleImagesComplete}
/>
)}
</div>
);
};

View File

@ -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<number>(0);
const [isCreating, setIsCreating] = useState(false);
const [generatedVideoList, setGeneratedVideoList] = useState<any[]>([]);
const [projectName, setProjectName] = useState(localStorage.getItem('projectName') || '默认名称');
const handleUploadVideo = async () => {
console.log('upload video');
@ -121,7 +129,7 @@ export function CreateToVideo2() {
alert(`创建剧集失败: ${episodeResponse.message}`);
return;
}
let episodeId = episodeResponse.data.id ;
let episodeId = episodeResponse.data.id;
if (videoUrl || script) {
try {
@ -356,7 +364,7 @@ export function CreateToVideo2() {
<div
ref={containerRef}
className="container mx-auto overflow-hidden custom-scrollbar"
style={isExpanded ? {height: 'calc(100vh - 12rem)'} : {height: 'calc(100vh - 20rem)'}}
style={isExpanded ? { height: 'calc(100vh - 12rem)' } : { height: 'calc(100vh - 20rem)' }}
>
{isClient && (
<JoyrideNoSSR
@ -373,28 +381,7 @@ export function CreateToVideo2() {
overlayColor: 'rgba(0, 0, 0, 0.5)',
textColor: '#F3F4F6',
arrowColor: '#1F2937',
},
tooltip: {
borderRadius: '1rem',
},
tooltipContainer: {
textAlign: 'left',
},
buttonNext: {
fontSize: '14px',
},
buttonBack: {
fontSize: '14px',
},
buttonSkip: {
fontSize: '14px',
},
buttonClose: {
fontSize: '14px',
},
spotlight: {
backgroundColor: 'transparent',
},
}
}}
disableOverlayClose
spotlightClicks
@ -409,24 +396,9 @@ export function CreateToVideo2() {
}}
/>
)}
<div className='scroll-load-box h-full overflow-y-scroll w-[calc(100%-16px)] mx-auto'>
<div className='min-h-[100%]'>
<div className='flex flex-col items-center fixed top-1/2 left-1/2 -translate-x-1/2 translate-y-[calc(-50%-68px)]'>
<Image
src='/assets/empty_video.png'
width={160}
height={160}
alt='empty_video'
priority
/>
<div className='text-[16px] font-[400] leading-[24px]'>
<span className='opacity-60'>Generated videos will appear here. </span>
<span className='font-[700] border-0 border-solid border-white hover:border-b-[1px] cursor-pointer' onClick={() => handleStartCreating()}>Start creating!</span>
</div>
</div>
</div>
</div>
<div className='min-h-[100%] flex flex-col justify-center items-center'>
{/* 空状态 */}
<EmptyStateAnimation />
{/* 工具栏 */}
<div className='video-tool-component relative w-[1080px]'>
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-[#0C0E11] backdrop-blur-[15px]'>
@ -570,5 +542,6 @@ export function CreateToVideo2() {
</div>
</div>
</div>
</div>
);
}

View File

@ -137,3 +137,13 @@
.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;
}

14
package-lock.json generated
View File

@ -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",

View File

@ -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",