优化细节

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> </div>
{/* Create Project Button */} {/* 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 <motion.div
className="relative group" className="relative group"
whileHover={!isCreating ? { scale: 1.05 } : {}} whileHover={!isCreating ? { scale: 1.05 } : {}}

View File

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

View File

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

View File

@ -3,6 +3,16 @@
import React from 'react'; import React from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import {
Image,
Video,
CheckCircle,
Music,
Loader2,
User,
Scissors,
Tv
} from 'lucide-react';
interface TaskInfoProps { interface TaskInfoProps {
isLoading: boolean; isLoading: boolean;
@ -10,7 +20,32 @@ interface TaskInfoProps {
currentLoadingText: string; 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) { export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfoProps) {
const StageIcon = getStageIcon(currentLoadingText);
if (isLoading) { if (isLoading) {
return ( return (
<> <>
@ -46,7 +81,7 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfo
}} }}
/> />
<motion.div <motion.div
className="flex items-center gap-1.5" className="flex items-center gap-2"
initial="hidden" initial="hidden"
animate="visible" animate="visible"
variants={{ variants={{
@ -54,6 +89,16 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfo
visible: { opacity: 1 } 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 <motion.span
className="text-emerald-500 font-medium" className="text-emerald-500 font-medium"
variants={{ variants={{
@ -98,9 +143,31 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfo
repeatDelay: 0.2 repeatDelay: 0.2
}} }}
/> />
{/* 阶段图标 */}
<motion.div
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="text-blue-500"
animate={{
rotate: [0, 360],
scale: [1, 1.1, 1]
}}
transition={{
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
}}
>
<StageIcon className="w-5 h-5" />
</motion.div>
<motion.div <motion.div
className="relative" className="relative"
key={currentLoadingText}
initial={{ opacity: 0, x: -10 }} initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }} exit={{ opacity: 0, x: 10 }}
@ -185,6 +252,8 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfo
}} }}
/> />
</motion.div> </motion.div>
</motion.div>
<motion.div <motion.div
className="w-1.5 h-1.5 rounded-full bg-blue-500" className="w-1.5 h-1.5 rounded-full bg-blue-500"
animate={{ animate={{

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState, useEffect, useRef, useCallback } from 'react'; 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数据 // 当前选择的mock数据
let selectedMockData = getRandomMockData(); let selectedMockData = getRandomMockData();
@ -9,6 +9,7 @@ let selectedMockData = getRandomMockData();
export function useWorkflowData() { export function useWorkflowData() {
const [taskObject, setTaskObject] = useState<any>(null); const [taskObject, setTaskObject] = useState<any>(null);
const [taskSketch, setTaskSketch] = useState<any[]>([]); const [taskSketch, setTaskSketch] = useState<any[]>([]);
const [taskRoles, setTaskRoles] = useState<any[]>([]);
const [taskVideos, setTaskVideos] = useState<any[]>([]); const [taskVideos, setTaskVideos] = useState<any[]>([]);
const [sketchCount, setSketchCount] = useState(0); const [sketchCount, setSketchCount] = useState(0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -49,7 +50,7 @@ export function useWorkflowData() {
// 模拟分批获取分镜草图 // 模拟分批获取分镜草图
for (let i = 0; i < totalSketches; i++) { 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 = { const newSketch = {
id: `sketch-${i}`, id: `sketch-${i}`,
@ -76,17 +77,37 @@ export function useWorkflowData() {
// 模拟接口请求 每次获取一个角色 轮询获取 // 模拟接口请求 每次获取一个角色 轮询获取
const getTaskRole = async (taskId: string) => { 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) => { 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) => { 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++) { 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 = { const newVideo = {
id: `video-${i}`, id: `video-${i}`,
@ -130,6 +151,8 @@ export function useWorkflowData() {
} }
const totalSketches = selectedMockData.sketch.length; const totalSketches = selectedMockData.sketch.length;
const totalVideos = selectedMockData.video.length;
const totalCharacters = selectedMockData.roles.length;
if (currentStep === '1') { if (currentStep === '1') {
if (isGeneratingSketch) { if (isGeneratingSketch) {
@ -138,10 +161,14 @@ export function useWorkflowData() {
setCurrentLoadingText(STEP_MESSAGES.sketchComplete); setCurrentLoadingText(STEP_MESSAGES.sketchComplete);
} }
} else if (currentStep === '2') { } else if (currentStep === '2') {
setCurrentLoadingText(STEP_MESSAGES.character); // 在角色生成阶段loading文字已经在 getTaskRole 函数中直接管理
// 这里不需要额外设置,避免覆盖
if (taskRoles.length === totalCharacters) {
setCurrentLoadingText(STEP_MESSAGES.newCharacter(totalCharacters, totalCharacters));
}
} else if (currentStep === '3') { } else if (currentStep === '3') {
if (isGeneratingVideo) { if (isGeneratingVideo) {
setCurrentLoadingText(STEP_MESSAGES.video(taskVideos.length, totalSketches)); setCurrentLoadingText(STEP_MESSAGES.video(taskVideos.length, totalVideos));
} else { } else {
setCurrentLoadingText(STEP_MESSAGES.videoComplete); setCurrentLoadingText(STEP_MESSAGES.videoComplete);
} }
@ -152,7 +179,7 @@ export function useWorkflowData() {
} else { } else {
setCurrentLoadingText(STEP_MESSAGES.complete); 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(() => { useEffect(() => {
@ -194,7 +221,15 @@ export function useWorkflowData() {
// 获取分镜视频后,开始获取背景音 // 获取分镜视频后,开始获取背景音
await getTaskBackgroundAudio(taskId); 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' // 修改 taskObject 下的 taskStatus 为 '5'
setTaskObject((prev: any) => ({ setTaskObject((prev: any) => ({
@ -202,15 +237,7 @@ export function useWorkflowData() {
taskStatus: '5' taskStatus: '5'
})); }));
setCurrentStep('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 getTaskFinalProduct(taskId);
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));

View File

@ -139,7 +139,7 @@ function VideoScreenLayoutComponent({ videos }: VideoScreenLayoutProps) {
}; };
return ( 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 <div
ref={containerRef} ref={containerRef}

View File

@ -247,6 +247,7 @@ export const STEP_MESSAGES = {
sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`, sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`,
sketchComplete: 'Sketch generation complete', sketchComplete: 'Sketch generation complete',
character: 'Drawing characters...', 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}...`, video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`,
videoComplete: 'Video generation complete', videoComplete: 'Video generation complete',
audio: 'Generating background audio...', audio: 'Generating background audio...',
@ -254,3 +255,12 @@ export const STEP_MESSAGES = {
final: 'Generating final product...', final: 'Generating final product...',
complete: 'Task completed' 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 一个最终成品
}