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;
|
||||
}
|
||||
|
||||
// 获取剧集详情请求数据类型
|
||||
export interface detailScriptEpisodeRequest {
|
||||
id: number;
|
||||
}
|
||||
|
||||
// 创建剧集响应数据类型
|
||||
export interface ScriptEpisode {
|
||||
id: number;
|
||||
@ -65,3 +70,8 @@ export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Pro
|
||||
export const updateScriptEpisode = async (data: UpdateScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
|
||||
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