优化细节

This commit is contained in:
北枳 2025-07-04 14:32:28 +08:00
parent e0fbbb0f1e
commit 9dc168f22e
7 changed files with 259 additions and 110 deletions

View File

@ -151,7 +151,7 @@ export function HomePage2() {
</div>
{/* Create Project Button */}
<div className="fixed bottom-[5rem] left-[50%] -translate-x-1/2 z-50">
<div className="fixed bottom-[3rem] left-[50%] -translate-x-1/2 z-50">
<motion.div
className="relative group"
whileHover={!isCreating ? { scale: 1.05 } : {}}

View File

@ -9,7 +9,7 @@
font-style: normal;
font-weight: 400;
line-height: 1.5rem;
width: 190px;
height: 128px;
width: 8rem;
height: 7rem;
color: rgba(255, 255, 255, 0.75);
}

View File

@ -49,9 +49,12 @@ export function MediaViewer({
// 最终视频控制状态
const [isFinalVideoPlaying, setIsFinalVideoPlaying] = useState(true);
const [isFullscreen, setIsFullscreen] = useState(false);
const [finalVideoReady, setFinalVideoReady] = useState(false);
const [userHasInteracted, setUserHasInteracted] = useState(false);
// 音量控制函数
const toggleMute = () => {
setUserHasInteracted(true);
setIsMuted(!isMuted);
if (mainVideoRef.current) {
mainVideoRef.current.muted = !isMuted;
@ -62,6 +65,7 @@ export function MediaViewer({
};
const handleVolumeChange = (newVolume: number) => {
setUserHasInteracted(true);
setVolume(newVolume);
if (mainVideoRef.current) {
mainVideoRef.current.volume = newVolume;
@ -79,20 +83,63 @@ export function MediaViewer({
}
};
useEffect(() => {
if (finalVideoRef.current && finalVideoReady) {
if (isFinalVideoPlaying) {
finalVideoRef.current.play().catch(error => {
console.log('最终视频自动播放被阻止:', error);
// 如果自动播放被阻止,将状态设置为暂停
setIsFinalVideoPlaying(false);
});
} else {
finalVideoRef.current.pause();
}
}
}, [isFinalVideoPlaying, finalVideoReady]);
// 最终视频播放控制
const toggleFinalVideoPlay = () => {
setUserHasInteracted(true);
setIsFinalVideoPlaying(!isFinalVideoPlaying);
};
// 处理最终视频加载完成
const handleFinalVideoLoaded = () => {
if (finalVideoRef.current) {
setFinalVideoReady(true);
applyVolumeSettings(finalVideoRef.current);
// 如果当前状态是应该播放的,尝试播放
if (isFinalVideoPlaying) {
finalVideoRef.current.pause();
} else {
finalVideoRef.current.play();
finalVideoRef.current.play().catch(error => {
console.log('最终视频自动播放被阻止:', error);
setIsFinalVideoPlaying(false);
});
}
setIsFinalVideoPlaying(!isFinalVideoPlaying);
}
};
// 处理视频点击 - 首次交互时尝试播放
const handleVideoClick = () => {
if (!userHasInteracted && finalVideoRef.current && finalVideoReady) {
setUserHasInteracted(true);
if (isFinalVideoPlaying) {
finalVideoRef.current.play().catch(error => {
console.log('视频播放失败:', error);
});
}
}
};
// 包装编辑按钮点击事件
const handleEditClick = (tab: string) => {
setUserHasInteracted(true);
onEditModalOpen(tab);
};
// 全屏控制
const toggleFullscreen = () => {
setUserHasInteracted(true);
if (!document.fullscreenElement) {
// 进入全屏
if (finalVideoRef.current) {
@ -164,6 +211,17 @@ export function MediaViewer({
};
}, []);
// 组件卸载时清理视频状态
useEffect(() => {
return () => {
// 清理最终视频状态
setFinalVideoReady(false);
if (finalVideoRef.current) {
finalVideoRef.current.pause();
}
};
}, []);
// 渲染音量控制组件
const renderVolumeControls = () => (
<div className="flex items-center gap-2">
@ -228,7 +286,6 @@ export function MediaViewer({
<video
className="w-full h-full rounded-lg object-cover object-center"
src={taskVideos[currentSketchIndex]?.url}
autoPlay
loop
playsInline
muted
@ -249,13 +306,13 @@ export function MediaViewer({
ref={finalVideoRef}
className="w-full h-full object-cover rounded-lg"
src={finalVideo.url}
poster={taskSketch[currentSketchIndex]?.url}
autoPlay={isFinalVideoPlaying}
loop
playsInline
onLoadedData={() => applyVolumeSettings(finalVideoRef.current!)}
onLoadedData={handleFinalVideoLoaded}
onPlay={() => setIsFinalVideoPlaying(true)}
onPause={() => setIsFinalVideoPlaying(false)}
onClick={handleVideoClick}
/>
</motion.div>
@ -272,7 +329,7 @@ export function MediaViewer({
<GlassIconButton
icon={Edit3}
tooltip="Edit sketch"
onClick={() => onEditModalOpen('4')}
onClick={() => handleEditClick('4')}
/>
</motion.div>
)}
@ -506,7 +563,7 @@ export function MediaViewer({
<GlassIconButton
icon={Edit3}
tooltip="Edit sketch"
onClick={() => onEditModalOpen('3')}
onClick={() => handleEditClick('3')}
/>
</motion.div>
)}
@ -692,7 +749,7 @@ export function MediaViewer({
<GlassIconButton
icon={Edit3}
tooltip="Edit sketch"
onClick={() => onEditModalOpen('1')}
onClick={() => handleEditClick('1')}
/>
</motion.div>
)}
@ -737,20 +794,6 @@ export function MediaViewer({
</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>
);
};

View File

@ -3,6 +3,16 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Skeleton } from '@/components/ui/skeleton';
import {
Image,
Video,
CheckCircle,
Music,
Loader2,
User,
Scissors,
Tv
} from 'lucide-react';
interface TaskInfoProps {
isLoading: boolean;
@ -10,7 +20,32 @@ interface TaskInfoProps {
currentLoadingText: string;
}
// 根据加载文本返回对应的图标
const getStageIcon = (loadingText: string) => {
const text = loadingText.toLowerCase();
if (text.includes('sketch')) {
return Image;
} else if (text.includes('video')) {
return Video;
} else if (text.includes('character')) {
return User;
} else if (text.includes('audio')) {
return Music;
} else if (text.includes('post')) {
return Scissors;
} else if (text.includes('final')) {
return Tv;
} else if (text.includes('complete')) {
return CheckCircle;
} else {
return Loader2;
}
};
export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfoProps) {
const StageIcon = getStageIcon(currentLoadingText);
if (isLoading) {
return (
<>
@ -46,7 +81,7 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfo
}}
/>
<motion.div
className="flex items-center gap-1.5"
className="flex items-center gap-2"
initial="hidden"
animate="visible"
variants={{
@ -54,6 +89,16 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfo
visible: { opacity: 1 }
}}
>
<motion.div
className="text-emerald-500"
variants={{
hidden: { opacity: 0, scale: 0.8 },
visible: { opacity: 1, scale: 1 }
}}
transition={{ duration: 0.5 }}
>
<CheckCircle className="w-5 h-5" />
</motion.div>
<motion.span
className="text-emerald-500 font-medium"
variants={{
@ -98,93 +143,117 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfo
repeatDelay: 0.2
}}
/>
{/* 阶段图标 */}
<motion.div
className="relative"
className="flex items-center gap-2"
key={currentLoadingText}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.3 }}
>
{/* 背景发光效果 */}
<motion.div
className="absolute inset-0 text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-400 to-purple-400 blur-sm"
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
className="text-blue-500"
animate={{
rotate: [0, 360],
scale: [1, 1.1, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "linear"
}}
style={{
backgroundSize: "200% 200%",
transition={{
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
}}
>
<span className="normalS400 subtitle-had8uE">{currentLoadingText}</span>
<StageIcon className="w-5 h-5" />
</motion.div>
{/* 主文字 - 颜色填充动画 */}
<motion.div
className="relative z-10"
animate={{
scale: [1, 1.02, 1],
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut"
}}
className="relative"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.3 }}
>
<motion.span
className="normalS400 subtitle-had8uE text-transparent bg-clip-text bg-gradient-to-r from-blue-600 via-cyan-500 to-purple-600"
{/* 背景发光效果 */}
<motion.div
className="absolute inset-0 text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-400 to-purple-400 blur-sm"
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 3,
duration: 2,
repeat: Infinity,
ease: "linear"
}}
style={{
backgroundSize: "300% 300%",
backgroundSize: "200% 200%",
}}
>
{currentLoadingText}
</motion.span>
<span className="normalS400 subtitle-had8uE">{currentLoadingText}</span>
</motion.div>
{/* 主文字 - 颜色填充动画 */}
<motion.div
className="relative z-10"
animate={{
scale: [1, 1.02, 1],
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut"
}}
>
<motion.span
className="normalS400 subtitle-had8uE text-transparent bg-clip-text bg-gradient-to-r from-blue-600 via-cyan-500 to-purple-600"
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear"
}}
style={{
backgroundSize: "300% 300%",
}}
>
{currentLoadingText}
</motion.span>
</motion.div>
{/* 动态光点效果 */}
<motion.div
className="absolute left-0 top-1/2 transform -translate-y-1/2 w-2 h-2 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full blur-sm"
animate={{
x: [0, 200, 0],
opacity: [0, 1, 0],
scale: [0.5, 1, 0.5],
}}
transition={{
duration: 2.5,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* 文字底部装饰线 */}
<motion.div
className="absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-blue-500 via-cyan-400 to-purple-500"
animate={{
width: ["0%", "100%", "0%"],
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
width: { duration: 2, repeat: Infinity, ease: "easeInOut" },
backgroundPosition: { duration: 1.5, repeat: Infinity, ease: "linear" }
}}
style={{
backgroundSize: "200% 200%",
}}
/>
</motion.div>
{/* 动态光点效果 */}
<motion.div
className="absolute left-0 top-1/2 transform -translate-y-1/2 w-2 h-2 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full blur-sm"
animate={{
x: [0, 200, 0],
opacity: [0, 1, 0],
scale: [0.5, 1, 0.5],
}}
transition={{
duration: 2.5,
repeat: Infinity,
ease: "easeInOut",
}}
/>
{/* 文字底部装饰线 */}
<motion.div
className="absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-blue-500 via-cyan-400 to-purple-500"
animate={{
width: ["0%", "100%", "0%"],
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
width: { duration: 2, repeat: Infinity, ease: "easeInOut" },
backgroundPosition: { duration: 1.5, repeat: Infinity, ease: "linear" }
}}
style={{
backgroundSize: "200% 200%",
}}
/>
</motion.div>
<motion.div
className="w-1.5 h-1.5 rounded-full bg-blue-500"
animate={{

View File

@ -1,7 +1,7 @@
'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { getRandomMockData, STEP_MESSAGES } from '@/components/work-flow/constants';
import { getRandomMockData, STEP_MESSAGES, MOCK_DELAY_TIME } from '@/components/work-flow/constants';
// 当前选择的mock数据
let selectedMockData = getRandomMockData();
@ -9,6 +9,7 @@ let selectedMockData = getRandomMockData();
export function useWorkflowData() {
const [taskObject, setTaskObject] = useState<any>(null);
const [taskSketch, setTaskSketch] = useState<any[]>([]);
const [taskRoles, setTaskRoles] = useState<any[]>([]);
const [taskVideos, setTaskVideos] = useState<any[]>([]);
const [sketchCount, setSketchCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
@ -49,7 +50,7 @@ export function useWorkflowData() {
// 模拟分批获取分镜草图
for (let i = 0; i < totalSketches; i++) {
await new Promise(resolve => setTimeout(resolve, 5000)); // 10s
await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.sketch)); // 10s
const newSketch = {
id: `sketch-${i}`,
@ -76,17 +77,37 @@ export function useWorkflowData() {
// 模拟接口请求 每次获取一个角色 轮询获取
const getTaskRole = async (taskId: string) => {
await new Promise(resolve => setTimeout(resolve, 2000 * selectedMockData.roles.length)); // 延长到30秒
setTaskRoles([]);
const roleData = selectedMockData.roles;
const totalRoles = roleData.length;
for (let i = 0; i < totalRoles; i++) {
// 先更新loading文字显示当前正在生成的角色
setCurrentLoadingText(STEP_MESSAGES.newCharacter(i, totalRoles));
await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.character)); // 2s 一个角色
// 添加角色到列表
setTaskRoles(prev => [...prev, roleData[i]]);
// 更新loading文字显示已完成的角色数量
setCurrentLoadingText(STEP_MESSAGES.newCharacter(i + 1, totalRoles));
// 如果不是最后一个角色,稍微延迟一下让用户看到更新
if (i < totalRoles - 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
};
// 模拟接口请求 获取背景音
const getTaskBackgroundAudio = async (taskId: string) => {
await new Promise(resolve => setTimeout(resolve, 2000)); // 10s
await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.audio)); // 10s
};
// 模拟接口请求 获取最终成品
const getTaskFinalProduct = async (taskId: string) => {
await new Promise(resolve => setTimeout(resolve, 10000)); // 50s
await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.final)); // 50s
};
// 模拟接口请求 每次获取一个分镜视频 轮询获取
@ -99,7 +120,7 @@ export function useWorkflowData() {
// 模拟分批获取分镜视频
for (let i = 0; i < totalVideos; i++) {
await new Promise(resolve => setTimeout(resolve, 6000)); // 60s
await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.video)); // 60s
const newVideo = {
id: `video-${i}`,
@ -130,6 +151,8 @@ export function useWorkflowData() {
}
const totalSketches = selectedMockData.sketch.length;
const totalVideos = selectedMockData.video.length;
const totalCharacters = selectedMockData.roles.length;
if (currentStep === '1') {
if (isGeneratingSketch) {
@ -138,10 +161,14 @@ export function useWorkflowData() {
setCurrentLoadingText(STEP_MESSAGES.sketchComplete);
}
} else if (currentStep === '2') {
setCurrentLoadingText(STEP_MESSAGES.character);
// 在角色生成阶段loading文字已经在 getTaskRole 函数中直接管理
// 这里不需要额外设置,避免覆盖
if (taskRoles.length === totalCharacters) {
setCurrentLoadingText(STEP_MESSAGES.newCharacter(totalCharacters, totalCharacters));
}
} else if (currentStep === '3') {
if (isGeneratingVideo) {
setCurrentLoadingText(STEP_MESSAGES.video(taskVideos.length, totalSketches));
setCurrentLoadingText(STEP_MESSAGES.video(taskVideos.length, totalVideos));
} else {
setCurrentLoadingText(STEP_MESSAGES.videoComplete);
}
@ -152,7 +179,7 @@ export function useWorkflowData() {
} else {
setCurrentLoadingText(STEP_MESSAGES.complete);
}
}, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length]);
}, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length, taskRoles.length]);
// 初始化数据
useEffect(() => {
@ -194,7 +221,15 @@ export function useWorkflowData() {
// 获取分镜视频后,开始获取背景音
await getTaskBackgroundAudio(taskId);
await new Promise(resolve => setTimeout(resolve, 2000));
// 后期制作:抽卡中 对口型中 配音中 一致性处理中
setCurrentLoadingText(STEP_MESSAGES.postProduction('Selecting optimal frames'));
await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.postProduction));
setCurrentLoadingText(STEP_MESSAGES.postProduction('Aligning lip sync'));
await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.postProduction));
setCurrentLoadingText(STEP_MESSAGES.postProduction('Adding background audio'));
await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.postProduction));
setCurrentLoadingText(STEP_MESSAGES.postProduction('Consistency processing'));
await new Promise(resolve => setTimeout(resolve, MOCK_DELAY_TIME.postProduction));
// 修改 taskObject 下的 taskStatus 为 '5'
setTaskObject((prev: any) => ({
@ -202,15 +237,7 @@ export function useWorkflowData() {
taskStatus: '5'
}));
setCurrentStep('5');
// 后期制作:抽卡中 对口型中 配音中 一致性处理中
setCurrentLoadingText(STEP_MESSAGES.postProduction('Selecting optimal frames'));
await new Promise(resolve => setTimeout(resolve, 10000));
setCurrentLoadingText(STEP_MESSAGES.postProduction('Aligning lip sync'));
await new Promise(resolve => setTimeout(resolve, 10000));
setCurrentLoadingText(STEP_MESSAGES.postProduction('Adding background audio'));
await new Promise(resolve => setTimeout(resolve, 10000));
setCurrentLoadingText(STEP_MESSAGES.postProduction('Consistency processing'));
await new Promise(resolve => setTimeout(resolve, 10000));
// 获取背景音后,开始获取最终成品
await getTaskFinalProduct(taskId);
await new Promise(resolve => setTimeout(resolve, 2000));

View File

@ -139,7 +139,7 @@ function VideoScreenLayoutComponent({ videos }: VideoScreenLayoutProps) {
};
return (
<div className="relative w-full h-[600px] overflow-hidden bg-[var(--background)]">
<div className="relative w-full h-[360px] mt-[3rem] overflow-hidden bg-[var(--background)]">
{/* 视频面板容器 */}
<div
ref={containerRef}

View File

@ -247,10 +247,20 @@ export const STEP_MESSAGES = {
sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`,
sketchComplete: 'Sketch generation complete',
character: 'Drawing characters...',
newCharacter: (count: number, total: number) => `Drawing character ${count + 1 > total ? total : count + 1}/${total}...`,
video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`,
videoComplete: 'Video generation complete',
audio: 'Generating background audio...',
postProduction: (step: string) => `Post-production: ${step}...`,
final: 'Generating final product...',
complete: 'Task completed'
};
};
export const MOCK_DELAY_TIME = {
sketch: 5000, // 5s 一个草图
character: 2000, // 2s 一个角色
video: 6000, // 6s 一个分镜视频
audio: 2000, // 2s 一个音频
postProduction: 2000, // 2s 一个后期制作
final: 10000, // 10s 一个最终成品
}