forked from 77media/video-flow
拆分work-flow
This commit is contained in:
parent
34348881b3
commit
2ee662dc18
@ -43,6 +43,11 @@ export interface UpdateScriptEpisodeRequest {
|
|||||||
video_url?: string;
|
video_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取剧集详情请求数据类型
|
||||||
|
export interface detailScriptEpisodeRequest {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
// 创建剧集响应数据类型
|
// 创建剧集响应数据类型
|
||||||
export interface ScriptEpisode {
|
export interface ScriptEpisode {
|
||||||
id: number;
|
id: number;
|
||||||
@ -64,4 +69,9 @@ export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Pro
|
|||||||
// 更新剧集
|
// 更新剧集
|
||||||
export const updateScriptEpisode = async (data: UpdateScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
|
export const updateScriptEpisode = async (data: UpdateScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
|
||||||
return post<ApiResponse<ScriptEpisode>>('/script_episode/update', data);
|
return post<ApiResponse<ScriptEpisode>>('/script_episode/update', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取剧集详情
|
||||||
|
export const detailScriptEpisode = async (data: detailScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
|
||||||
|
return post<ApiResponse<ScriptEpisode>>('/script_episode/detail', data);
|
||||||
};
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
5
components/pages/work-flow/index.ts
Normal file
5
components/pages/work-flow/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { TaskInfo } from './task-info';
|
||||||
|
export { MediaViewer } from './media-viewer';
|
||||||
|
export { ThumbnailGrid } from './thumbnail-grid';
|
||||||
|
export { useWorkflowData } from './use-workflow-data';
|
||||||
|
export { usePlaybackControls } from './use-playback-controls';
|
||||||
437
components/pages/work-flow/media-viewer.tsx
Normal file
437
components/pages/work-flow/media-viewer.tsx
Normal file
@ -0,0 +1,437 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { Edit3, Play, Pause } from 'lucide-react';
|
||||||
|
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||||||
|
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||||
|
|
||||||
|
interface MediaViewerProps {
|
||||||
|
currentStep: string;
|
||||||
|
currentSketchIndex: number;
|
||||||
|
taskSketch: any[];
|
||||||
|
taskVideos: any[];
|
||||||
|
isVideoPlaying: boolean;
|
||||||
|
isPlaying: boolean;
|
||||||
|
showControls: boolean;
|
||||||
|
onControlsChange: (show: boolean) => void;
|
||||||
|
onEditModalOpen: (tab: string) => void;
|
||||||
|
onToggleVideoPlay: () => void;
|
||||||
|
onTogglePlay: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 function MediaViewer({
|
||||||
|
currentStep,
|
||||||
|
currentSketchIndex,
|
||||||
|
taskSketch,
|
||||||
|
taskVideos,
|
||||||
|
isVideoPlaying,
|
||||||
|
isPlaying,
|
||||||
|
showControls,
|
||||||
|
onControlsChange,
|
||||||
|
onEditModalOpen,
|
||||||
|
onToggleVideoPlay,
|
||||||
|
onTogglePlay
|
||||||
|
}: MediaViewerProps) {
|
||||||
|
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
// 视频播放控制
|
||||||
|
useEffect(() => {
|
||||||
|
if (mainVideoRef.current) {
|
||||||
|
if (isVideoPlaying) {
|
||||||
|
mainVideoRef.current.play().catch(error => {
|
||||||
|
console.log('视频播放失败:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mainVideoRef.current.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isVideoPlaying]);
|
||||||
|
|
||||||
|
// 当切换视频时重置视频播放
|
||||||
|
useEffect(() => {
|
||||||
|
if (mainVideoRef.current) {
|
||||||
|
mainVideoRef.current.currentTime = 0;
|
||||||
|
if (isVideoPlaying) {
|
||||||
|
mainVideoRef.current.play().catch(error => {
|
||||||
|
console.log('视频播放失败:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentSketchIndex]);
|
||||||
|
|
||||||
|
// 渲染最终成片
|
||||||
|
const renderFinalVideo = () => (
|
||||||
|
<div
|
||||||
|
className="relative w-full h-full rounded-lg overflow-hidden"
|
||||||
|
onMouseEnter={() => onControlsChange(true)}
|
||||||
|
onMouseLeave={() => onControlsChange(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={() => onEditModalOpen('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>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 渲染视频内容
|
||||||
|
const renderVideoContent = () => (
|
||||||
|
<div
|
||||||
|
className="relative w-full h-full rounded-lg"
|
||||||
|
onMouseEnter={() => onControlsChange(true)}
|
||||||
|
onMouseLeave={() => onControlsChange(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) {
|
||||||
|
// 自动切换到下一个视频的逻辑在父组件处理
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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={() => onEditModalOpen('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={onToggleVideoPlay}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 渲染分镜草图
|
||||||
|
const renderSketchContent = () => (
|
||||||
|
<div
|
||||||
|
className="relative w-full h-full rounded-lg"
|
||||||
|
onMouseEnter={() => onControlsChange(true)}
|
||||||
|
onMouseLeave={() => onControlsChange(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={() => onEditModalOpen('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={onTogglePlay}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 根据当前步骤渲染对应内容
|
||||||
|
if (Number(currentStep) === 6) {
|
||||||
|
return renderFinalVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(currentStep) > 2 && Number(currentStep) < 6) {
|
||||||
|
return renderVideoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderSketchContent();
|
||||||
|
}
|
||||||
141
components/pages/work-flow/task-info.tsx
Normal file
141
components/pages/work-flow/task-info.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
|
interface TaskInfoProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
taskObject: any;
|
||||||
|
currentLoadingText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskInfo({ isLoading, taskObject, currentLoadingText }: TaskInfoProps) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Skeleton className="h-8 w-64 mb-2" />
|
||||||
|
<Skeleton className="h-4 w-96" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="title-JtMejk">
|
||||||
|
{taskObject?.projectName}:{taskObject?.taskName}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
294
components/pages/work-flow/thumbnail-grid.tsx
Normal file
294
components/pages/work-flow/thumbnail-grid.tsx
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||||||
|
|
||||||
|
interface ThumbnailGridProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
currentStep: string;
|
||||||
|
currentSketchIndex: number;
|
||||||
|
taskSketch: any[];
|
||||||
|
taskVideos: any[];
|
||||||
|
isGeneratingSketch: boolean;
|
||||||
|
sketchCount: number;
|
||||||
|
onSketchSelect: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MOCK_SKETCH_COUNT = 8;
|
||||||
|
|
||||||
|
export function ThumbnailGrid({
|
||||||
|
isLoading,
|
||||||
|
currentStep,
|
||||||
|
currentSketchIndex,
|
||||||
|
taskSketch,
|
||||||
|
taskVideos,
|
||||||
|
isGeneratingSketch,
|
||||||
|
sketchCount,
|
||||||
|
onSketchSelect
|
||||||
|
}: ThumbnailGridProps) {
|
||||||
|
const thumbnailsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [startX, setStartX] = useState(0);
|
||||||
|
const [scrollLeft, setScrollLeft] = useState(0);
|
||||||
|
|
||||||
|
// 监听当前选中索引变化,自动滚动到对应位置
|
||||||
|
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 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; // 如果拖动距离太小,保持原有的点击选中逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
onSketchSelect(Math.min(Math.max(0, nearestIndex), taskSketch.length - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染加载状态
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最终成片阶段不显示缩略图
|
||||||
|
if (Number(currentStep) === 6) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染生成中的缩略图
|
||||||
|
const renderGeneratingThumbnail = () => (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 渲染视频阶段的缩略图
|
||||||
|
const renderVideoThumbnails = () => (
|
||||||
|
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 && onSketchSelect(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>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 渲染分镜草图阶段的缩略图
|
||||||
|
const renderSketchThumbnails = () => (
|
||||||
|
<>
|
||||||
|
{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 && onSketchSelect(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>
|
||||||
|
))}
|
||||||
|
{isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && renderGeneratingThumbnail()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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) > 2 && Number(currentStep) < 6
|
||||||
|
? renderVideoThumbnails()
|
||||||
|
: renderSketchThumbnails()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
components/pages/work-flow/use-playback-controls.tsx
Normal file
81
components/pages/work-flow/use-playback-controls.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
export function usePlaybackControls(taskSketch: any[], taskVideos: any[], currentStep: string) {
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [isVideoPlaying, setIsVideoPlaying] = useState(true);
|
||||||
|
const [showControls, setShowControls] = useState(false);
|
||||||
|
const playTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const videoPlayTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// 处理播放/暂停
|
||||||
|
const togglePlay = useCallback(() => {
|
||||||
|
setIsPlaying(prev => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理视频播放/暂停
|
||||||
|
const toggleVideoPlay = useCallback(() => {
|
||||||
|
setIsVideoPlaying(prev => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 自动播放逻辑 - 分镜草图
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPlaying && taskSketch.length > 0) {
|
||||||
|
playTimerRef.current = setInterval(() => {
|
||||||
|
// 这里的切换逻辑需要在父组件中处理
|
||||||
|
// 因为需要访问 setCurrentSketchIndex
|
||||||
|
}, 2000);
|
||||||
|
} else if (playTimerRef.current) {
|
||||||
|
clearInterval(playTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (playTimerRef.current) {
|
||||||
|
clearInterval(playTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isPlaying, taskSketch.length]);
|
||||||
|
|
||||||
|
// 视频自动播放逻辑
|
||||||
|
useEffect(() => {
|
||||||
|
if (isVideoPlaying && taskVideos.length > 0) {
|
||||||
|
// 具体的视频播放控制在 MediaViewer 组件中处理
|
||||||
|
} else {
|
||||||
|
// 清除定时器
|
||||||
|
if (videoPlayTimerRef.current) {
|
||||||
|
clearInterval(videoPlayTimerRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (videoPlayTimerRef.current) {
|
||||||
|
clearInterval(videoPlayTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isVideoPlaying, taskVideos.length]);
|
||||||
|
|
||||||
|
// 当切换到视频模式时,停止播放
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStep === '3') {
|
||||||
|
setIsPlaying(false);
|
||||||
|
}
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
// 当切换到分镜草图模式时,停止视频播放
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStep !== '3') {
|
||||||
|
setIsVideoPlaying(false);
|
||||||
|
}
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPlaying,
|
||||||
|
isVideoPlaying,
|
||||||
|
showControls,
|
||||||
|
setShowControls,
|
||||||
|
togglePlay,
|
||||||
|
toggleVideoPlay,
|
||||||
|
playTimerRef, // 暴露给父组件使用
|
||||||
|
};
|
||||||
|
}
|
||||||
240
components/pages/work-flow/use-workflow-data.tsx
Normal file
240
components/pages/work-flow/use-workflow-data.tsx
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
export function useWorkflowData() {
|
||||||
|
const [taskObject, setTaskObject] = useState<any>(null);
|
||||||
|
const [taskSketch, setTaskSketch] = useState<any[]>([]);
|
||||||
|
const [taskVideos, setTaskVideos] = useState<any[]>([]);
|
||||||
|
const [sketchCount, setSketchCount] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [currentStep, setCurrentStep] = useState('0');
|
||||||
|
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
|
||||||
|
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
|
||||||
|
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
||||||
|
const [currentLoadingText, setCurrentLoadingText] = useState('加载中...');
|
||||||
|
|
||||||
|
// 模拟接口请求 获取任务详情
|
||||||
|
const getTaskDetail = async (taskId: string) => {
|
||||||
|
const data = {
|
||||||
|
projectId: 'projectId-123',
|
||||||
|
projectName: "Project 1",
|
||||||
|
taskId: taskId,
|
||||||
|
taskName: "Task 1",
|
||||||
|
taskDescription: "Task 1 Description",
|
||||||
|
taskStatus: "1",
|
||||||
|
taskProgress: 0,
|
||||||
|
mode: 'auto',
|
||||||
|
resolution: '1080p',
|
||||||
|
};
|
||||||
|
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));
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
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));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟接口请求 获取背景音
|
||||||
|
const getTaskBackgroundAudio = async (taskId: string) => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟接口请求 获取最终成品
|
||||||
|
const getTaskFinalProduct = async (taskId: string) => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟接口请求 每次获取一个分镜视频 轮询获取
|
||||||
|
const getTaskVideo = async (taskId: string) => {
|
||||||
|
setIsGeneratingVideo(true);
|
||||||
|
setTaskVideos([]);
|
||||||
|
|
||||||
|
// 模拟分批获取分镜视频
|
||||||
|
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
if (prev.find(video => video.id === newVideo.id)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return [...prev, newVideo];
|
||||||
|
});
|
||||||
|
setCurrentSketchIndex(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsGeneratingVideo(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新加载文字
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态数据
|
||||||
|
taskObject,
|
||||||
|
taskSketch,
|
||||||
|
taskVideos,
|
||||||
|
sketchCount,
|
||||||
|
isLoading,
|
||||||
|
currentStep,
|
||||||
|
currentSketchIndex,
|
||||||
|
isGeneratingSketch,
|
||||||
|
isGeneratingVideo,
|
||||||
|
currentLoadingText,
|
||||||
|
// 操作方法
|
||||||
|
setCurrentSketchIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
84
components/work-flow/api.ts
Normal file
84
components/work-flow/api.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import {
|
||||||
|
TaskObject,
|
||||||
|
SketchItem,
|
||||||
|
VideoItem,
|
||||||
|
MOCK_SKETCH_URLS,
|
||||||
|
MOCK_SKETCH_SCRIPT,
|
||||||
|
MOCK_VIDEO_URLS,
|
||||||
|
MOCK_SKETCH_COUNT
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
// 模拟接口请求 获取任务详情
|
||||||
|
export const getTaskDetail = async (taskId: string): Promise<TaskObject> => {
|
||||||
|
// const response = await fetch(`/api/task/${taskId}`);
|
||||||
|
// const data = await response.json();
|
||||||
|
// mock data
|
||||||
|
const data: TaskObject = {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟接口请求 每次获取一个分镜草图 轮询获取
|
||||||
|
export const getTaskSketch = async (
|
||||||
|
taskId: string,
|
||||||
|
onProgress: (sketch: SketchItem, index: number) => void
|
||||||
|
): Promise<void> => {
|
||||||
|
// 模拟分批获取分镜草图
|
||||||
|
for (let i = 0; i < MOCK_SKETCH_COUNT; i++) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||||||
|
|
||||||
|
const newSketch: SketchItem = {
|
||||||
|
id: `sketch-${i}`,
|
||||||
|
url: MOCK_SKETCH_URLS[i % MOCK_SKETCH_URLS.length],
|
||||||
|
script: MOCK_SKETCH_SCRIPT[i % MOCK_SKETCH_SCRIPT.length],
|
||||||
|
status: 'done'
|
||||||
|
};
|
||||||
|
|
||||||
|
onProgress(newSketch, i);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟接口请求 每次获取一个角色 轮询获取
|
||||||
|
export const getTaskRole = async (taskId: string): Promise<void> => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟接口请求 获取背景音
|
||||||
|
export const getTaskBackgroundAudio = async (taskId: string): Promise<void> => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟接口请求 获取最终成品
|
||||||
|
export const getTaskFinalProduct = async (taskId: string): Promise<void> => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟接口请求 每次获取一个分镜视频 轮询获取
|
||||||
|
export const getTaskVideo = async (
|
||||||
|
taskId: string,
|
||||||
|
sketchCount: number,
|
||||||
|
onProgress: (video: VideoItem, index: number) => void
|
||||||
|
): Promise<void> => {
|
||||||
|
// 模拟分批获取分镜视频
|
||||||
|
for (let i = 0; i < sketchCount; i++) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000)); // 模拟5秒延迟
|
||||||
|
|
||||||
|
const newVideo: VideoItem = {
|
||||||
|
id: `video-${i}`,
|
||||||
|
url: MOCK_VIDEO_URLS[i % MOCK_VIDEO_URLS.length],
|
||||||
|
script: MOCK_SKETCH_SCRIPT[i % MOCK_SKETCH_SCRIPT.length],
|
||||||
|
status: 'done'
|
||||||
|
};
|
||||||
|
|
||||||
|
onProgress(newVideo, i);
|
||||||
|
}
|
||||||
|
};
|
||||||
66
components/work-flow/constants.ts
Normal file
66
components/work-flow/constants.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
export 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',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MOCK_SKETCH_SCRIPT = [
|
||||||
|
'script-123',
|
||||||
|
'script-123',
|
||||||
|
'script-123',
|
||||||
|
'script-123',
|
||||||
|
];
|
||||||
|
|
||||||
|
export 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',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MOCK_SKETCH_COUNT = 8;
|
||||||
|
|
||||||
|
export 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 interface TaskObject {
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
taskId: string;
|
||||||
|
taskName: string;
|
||||||
|
taskDescription: string;
|
||||||
|
taskStatus: string;
|
||||||
|
taskProgress: number;
|
||||||
|
mode: string;
|
||||||
|
resolution: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SketchItem {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
script: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoItem {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
script: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const STEP_MESSAGES = {
|
||||||
|
loading: '正在加载任务信息...',
|
||||||
|
sketch: (count: number, total: number) => `正在生成分镜草图 ${count + 1}/${total}...`,
|
||||||
|
sketchComplete: '分镜草图生成完成',
|
||||||
|
character: '正在绘制角色...',
|
||||||
|
video: (count: number, total: number) => `正在生成分镜视频 ${count + 1}/${total}...`,
|
||||||
|
videoComplete: '分镜视频生成完成',
|
||||||
|
audio: '正在生成背景音...',
|
||||||
|
final: '正在生成最终成品...',
|
||||||
|
complete: '任务完成'
|
||||||
|
};
|
||||||
305
hooks/useWorkFlow.ts
Normal file
305
hooks/useWorkFlow.ts
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { TaskObject, SketchItem, VideoItem, STEP_MESSAGES, MOCK_SKETCH_COUNT } from '@/components/work-flow/constants';
|
||||||
|
import {
|
||||||
|
getTaskDetail,
|
||||||
|
getTaskSketch,
|
||||||
|
getTaskRole,
|
||||||
|
getTaskBackgroundAudio,
|
||||||
|
getTaskFinalProduct,
|
||||||
|
getTaskVideo
|
||||||
|
} from '@/components/work-flow/api';
|
||||||
|
|
||||||
|
export const useWorkFlow = () => {
|
||||||
|
// 基础状态
|
||||||
|
const [taskObject, setTaskObject] = useState<TaskObject | null>(null);
|
||||||
|
const [taskSketch, setTaskSketch] = useState<SketchItem[]>([]);
|
||||||
|
const [taskVideos, setTaskVideos] = useState<VideoItem[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [currentStep, setCurrentStep] = useState('0');
|
||||||
|
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
|
||||||
|
const [currentLoadingText, setCurrentLoadingText] = useState('加载中...');
|
||||||
|
|
||||||
|
// 生成状态
|
||||||
|
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
|
||||||
|
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
||||||
|
const [sketchCount, setSketchCount] = useState(0);
|
||||||
|
|
||||||
|
// 播放状态
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [isVideoPlaying, setIsVideoPlaying] = useState(true);
|
||||||
|
const playTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const videoPlayTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const mainVideoRef = useRef<HTMLVideoElement>(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');
|
||||||
|
|
||||||
|
// 初始化工作流
|
||||||
|
useEffect(() => {
|
||||||
|
const initWorkFlow = async () => {
|
||||||
|
const taskId = localStorage.getItem("taskId") || "taskId-123";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取任务详情
|
||||||
|
const data = await getTaskDetail(taskId);
|
||||||
|
setTaskObject(data);
|
||||||
|
setIsLoading(false);
|
||||||
|
setCurrentStep('1');
|
||||||
|
|
||||||
|
// 获取分镜草图
|
||||||
|
await handleGetTaskSketch(taskId);
|
||||||
|
await delay(2000);
|
||||||
|
updateTaskStatus('2');
|
||||||
|
setCurrentStep('2');
|
||||||
|
|
||||||
|
// 绘制角色
|
||||||
|
await getTaskRole(taskId);
|
||||||
|
await delay(2000);
|
||||||
|
updateTaskStatus('3');
|
||||||
|
setCurrentStep('3');
|
||||||
|
|
||||||
|
// 获取分镜视频
|
||||||
|
await handleGetTaskVideo(taskId);
|
||||||
|
await delay(2000);
|
||||||
|
updateTaskStatus('4');
|
||||||
|
setCurrentStep('4');
|
||||||
|
|
||||||
|
// 获取背景音
|
||||||
|
await getTaskBackgroundAudio(taskId);
|
||||||
|
await delay(2000);
|
||||||
|
updateTaskStatus('5');
|
||||||
|
setCurrentStep('5');
|
||||||
|
|
||||||
|
// 获取最终成品
|
||||||
|
await getTaskFinalProduct(taskId);
|
||||||
|
await delay(2000);
|
||||||
|
updateTaskStatus('6');
|
||||||
|
setCurrentStep('6');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('工作流初始化失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initWorkFlow();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
const updateTaskStatus = (status: string) => {
|
||||||
|
setTaskObject(prev => prev ? { ...prev, taskStatus: status } : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGetTaskSketch = async (taskId: string) => {
|
||||||
|
if (isGeneratingSketch || taskSketch.length > 0) return;
|
||||||
|
|
||||||
|
setIsGeneratingSketch(true);
|
||||||
|
setTaskSketch([]);
|
||||||
|
|
||||||
|
await getTaskSketch(taskId, (newSketch, index) => {
|
||||||
|
setTaskSketch(prev => {
|
||||||
|
if (prev.find(sketch => sketch.id === newSketch.id)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return [...prev, newSketch];
|
||||||
|
});
|
||||||
|
setCurrentSketchIndex(index);
|
||||||
|
setSketchCount(index + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsGeneratingSketch(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGetTaskVideo = async (taskId: string) => {
|
||||||
|
setIsGeneratingVideo(true);
|
||||||
|
setTaskVideos([]);
|
||||||
|
|
||||||
|
await getTaskVideo(taskId, taskSketch.length, (newVideo, index) => {
|
||||||
|
setTaskVideos(prev => {
|
||||||
|
if (prev.find(video => video.id === newVideo.id)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return [...prev, newVideo];
|
||||||
|
});
|
||||||
|
setCurrentSketchIndex(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsGeneratingVideo(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自动播放逻辑
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPlaying && taskSketch.length > 0) {
|
||||||
|
playTimerRef.current = setInterval(() => {
|
||||||
|
setCurrentSketchIndex(prev => (prev + 1) % taskSketch.length);
|
||||||
|
}, 2000);
|
||||||
|
} else if (playTimerRef.current) {
|
||||||
|
clearInterval(playTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (playTimerRef.current) {
|
||||||
|
clearInterval(playTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isPlaying, taskSketch.length]);
|
||||||
|
|
||||||
|
// 视频播放逻辑
|
||||||
|
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 (isLoading) {
|
||||||
|
setCurrentLoadingText(STEP_MESSAGES.loading);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (currentStep) {
|
||||||
|
case '1':
|
||||||
|
setCurrentLoadingText(
|
||||||
|
isGeneratingSketch
|
||||||
|
? STEP_MESSAGES.sketch(sketchCount, MOCK_SKETCH_COUNT)
|
||||||
|
: STEP_MESSAGES.sketchComplete
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case '2':
|
||||||
|
setCurrentLoadingText(STEP_MESSAGES.character);
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
setCurrentLoadingText(
|
||||||
|
isGeneratingVideo
|
||||||
|
? STEP_MESSAGES.video(taskVideos.length, taskSketch.length)
|
||||||
|
: STEP_MESSAGES.videoComplete
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case '4':
|
||||||
|
setCurrentLoadingText(STEP_MESSAGES.audio);
|
||||||
|
break;
|
||||||
|
case '5':
|
||||||
|
setCurrentLoadingText(STEP_MESSAGES.final);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setCurrentLoadingText(STEP_MESSAGES.complete);
|
||||||
|
}
|
||||||
|
}, [isLoading, currentStep, isGeneratingSketch, sketchCount, isGeneratingVideo, taskVideos.length, taskSketch.length]);
|
||||||
|
|
||||||
|
// 控制函数
|
||||||
|
const togglePlay = useCallback(() => {
|
||||||
|
setIsPlaying(prev => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleVideoPlay = useCallback(() => {
|
||||||
|
if (mainVideoRef.current) {
|
||||||
|
if (isVideoPlaying) {
|
||||||
|
mainVideoRef.current.pause();
|
||||||
|
} else {
|
||||||
|
mainVideoRef.current.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsVideoPlaying(prev => !prev);
|
||||||
|
}, [isVideoPlaying]);
|
||||||
|
|
||||||
|
const handleEditModalOpen = (tab: string) => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
setIsVideoPlaying(false);
|
||||||
|
setActiveEditTab(tab);
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当切换到视频模式时,停止播放
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStep === '3') {
|
||||||
|
setIsPlaying(false);
|
||||||
|
}
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
// 当切换到分镜草图模式时,停止视频播放
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStep !== '3') {
|
||||||
|
setIsVideoPlaying(false);
|
||||||
|
}
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 数据状态
|
||||||
|
taskObject,
|
||||||
|
taskSketch,
|
||||||
|
taskVideos,
|
||||||
|
isLoading,
|
||||||
|
currentStep,
|
||||||
|
currentSketchIndex,
|
||||||
|
setCurrentSketchIndex,
|
||||||
|
currentLoadingText,
|
||||||
|
|
||||||
|
// 生成状态
|
||||||
|
isGeneratingSketch,
|
||||||
|
isGeneratingVideo,
|
||||||
|
sketchCount,
|
||||||
|
|
||||||
|
// 播放状态
|
||||||
|
isPlaying,
|
||||||
|
isVideoPlaying,
|
||||||
|
mainVideoRef,
|
||||||
|
|
||||||
|
// 拖拽状态
|
||||||
|
isDragging,
|
||||||
|
setIsDragging,
|
||||||
|
startX,
|
||||||
|
setStartX,
|
||||||
|
scrollLeft,
|
||||||
|
setScrollLeft,
|
||||||
|
|
||||||
|
// 界面状态
|
||||||
|
showControls,
|
||||||
|
setShowControls,
|
||||||
|
isEditModalOpen,
|
||||||
|
setIsEditModalOpen,
|
||||||
|
activeEditTab,
|
||||||
|
|
||||||
|
// 控制函数
|
||||||
|
togglePlay,
|
||||||
|
toggleVideoPlay,
|
||||||
|
handleEditModalOpen,
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user