拆分work-flow

This commit is contained in:
北枳 2025-07-02 19:52:15 +08:00
parent 34348881b3
commit 2ee662dc18
11 changed files with 1742 additions and 1138 deletions

View File

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

View 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';

View 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();
}

View 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>
)}
</>
);
}

View 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>
);
}

View 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, // 暴露给父组件使用
};
}

View 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,
};
}

View 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);
}
};

View 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
View 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,
};
};