diff --git a/api/script_episode.ts b/api/script_episode.ts index 343750f..6edae57 100644 --- a/api/script_episode.ts +++ b/api/script_episode.ts @@ -43,6 +43,11 @@ export interface UpdateScriptEpisodeRequest { video_url?: string; } +// 获取剧集详情请求数据类型 +export interface detailScriptEpisodeRequest { + id: number; +} + // 创建剧集响应数据类型 export interface ScriptEpisode { id: number; @@ -64,4 +69,9 @@ export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Pro // 更新剧集 export const updateScriptEpisode = async (data: UpdateScriptEpisodeRequest): Promise> => { return post>('/script_episode/update', data); +}; + +// 获取剧集详情 +export const detailScriptEpisode = async (data: detailScriptEpisodeRequest): Promise> => { + return post>('/script_episode/detail', data); }; \ No newline at end of file diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index b544833..ddbcf0c 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -1,69 +1,55 @@ "use client" -import React, { useEffect, useState, useRef, useMemo, useCallback } from "react"; -import { Play, ChevronUp, Loader2, Edit3, FileText, Pause } from "lucide-react"; +import React, { useRef, useEffect } from "react"; import "./style/work-flow.css"; -import LiquidGlass from '@/plugins/liquid-glass'; import { Skeleton } from "@/components/ui/skeleton"; import { AISuggestionBar } from "@/components/ai-suggestion-bar"; -import { motion, AnimatePresence } from "framer-motion"; -import { debounce } from "lodash"; -import { GlassIconButton } from "@/components/ui/glass-icon-button"; import { EditModal } from "@/components/ui/edit-modal"; -import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal'; - -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; - -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', -}; +import { TaskInfo } from "./work-flow/task-info"; +import { MediaViewer } from "./work-flow/media-viewer"; +import { ThumbnailGrid } from "./work-flow/thumbnail-grid"; +import { useWorkflowData } from "./work-flow/use-workflow-data"; +import { usePlaybackControls } from "./work-flow/use-playback-controls"; export default function WorkFlow() { - const [taskObject, setTaskObject] = useState(null); - const [projectObject, setProjectObject] = useState(null); - const [taskSketch, setTaskSketch] = useState([]); - const [sketchCount, setSketchCount] = useState(0); const containerRef = useRef(null); - const [isLoading, setIsLoading] = useState(true); - const [isAIBarVisible, setIsAIBarVisible] = useState(true); - const [currentStep, setCurrentStep] = useState('0'); - const [currentSketchIndex, setCurrentSketchIndex] = useState(0); - const [isGeneratingSketch, setIsGeneratingSketch] = useState(false); - const thumbnailsRef = useRef(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'); - const [taskVideos, setTaskVideos] = useState([]); - const [isGeneratingVideo, setIsGeneratingVideo] = useState(false); - const mainVideoRef = useRef(null); - const [isPlaying, setIsPlaying] = useState(false); - const playTimerRef = useRef(null); - const [isVideoPlaying, setIsVideoPlaying] = useState(true); - const videoPlayTimerRef = useRef(null); - const [currentLoadingText, setCurrentLoadingText] = useState('加载中...'); + const [isEditModalOpen, setIsEditModalOpen] = React.useState(false); + const [activeEditTab, setActiveEditTab] = React.useState('1'); + + // 使用自定义 hooks 管理状态 + const { + taskObject, + taskSketch, + taskVideos, + sketchCount, + isLoading, + currentStep, + currentSketchIndex, + isGeneratingSketch, + isGeneratingVideo, + currentLoadingText, + setCurrentSketchIndex, + } = useWorkflowData(); + + const { + isPlaying, + isVideoPlaying, + showControls, + setShowControls, + togglePlay, + toggleVideoPlay, + playTimerRef, + } = usePlaybackControls(taskSketch, taskVideos, currentStep); + + // 处理自动播放的分镜切换逻辑 + useEffect(() => { + if (isPlaying && taskSketch.length > 0 && playTimerRef.current) { + const interval = setInterval(() => { + setCurrentSketchIndex((prev: number) => (prev + 1) % taskSketch.length); + }, 2000); + + return () => clearInterval(interval); + } + }, [isPlaying, taskSketch.length, setCurrentSketchIndex]); // 模拟 AI 建议 const mockSuggestions = [ @@ -74,212 +60,9 @@ export default function WorkFlow() { "调整镜头语言" ]; - 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'); - // 获取最终成品后,任务完成 - }); - }, []); - - // 监听当前选中索引变化,自动滚动到对应位置 - 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 handleEditModalOpen = (tab: string) => { - // 停止循环播放 - setIsPlaying(false); - // 停止分镜视频播放 - setIsVideoPlaying(false); setActiveEditTab(tab); setIsEditModalOpen(true); - } - - // 处理鼠标/触摸拖动事件 - const handleMouseDown = (e: React.MouseEvent) => { - setIsDragging(true); - setStartX(e.pageX - thumbnailsRef.current!.offsetLeft); - setScrollLeft(thumbnailsRef.current!.scrollLeft); - }; - - const handleMouseMove = (e: React.MouseEvent) => { - 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) => { - 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; // 如果拖动距离太小,保持原有的点击选中逻辑 - } - - setCurrentSketchIndex(Math.min(Math.max(0, nearestIndex), taskSketch.length - 1)); - }; - - // 模拟接口请求 获取任务详情 - const getTaskDetail = async (taskId: string) => { - // const response = await fetch(`/api/task/${taskId}`); - // const data = await response.json(); - // mock data - const data = { - 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; - } - - // 模拟接口请求 每次获取一个分镜草图 轮询获取 - 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)); // 模拟2秒延迟 - - 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 => { - // 避免重复添加相同id的sketch - 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)); // 模拟2秒延迟 - } - - // 模拟接口请求 获取背景音 - const getTaskBackgroundAudio = async (taskId: string) => { - await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟 - } - - // 模拟接口请求 获取最终成品 - const getTaskFinalProduct = async (taskId: string) => { - await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟 - } - - // 模拟接口请求 每次获取一个分镜视频 轮询获取 - const getTaskVideo = async (taskId: string) => { - setIsGeneratingVideo(true); - setTaskVideos([]); - - // 模拟分批获取分镜视频 - for (let i = 0; i < MOCK_SKETCH_COUNT; i++) { - await new Promise(resolve => setTimeout(resolve, 5000)); // 模拟5秒延迟 - - 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 => { - // 避免重复添加相同id的video - if (prev.find(video => video.id === newVideo.id)) { - return prev; - } - return [...prev, newVideo]; - }); - setCurrentSketchIndex(i); - } - - setIsGeneratingVideo(false); }; const handleSuggestionClick = (suggestion: string) => { @@ -290,609 +73,6 @@ export default function WorkFlow() { console.log('Submitted text:', text); }; - // 缓存渲染的缩略图列表 - const renderedSketches = useMemo(() => - taskSketch.map((sketch, index) => ( -
!isDragging && setCurrentSketchIndex(index)} - > - -
- {`缩略图 -
-
-
- 场景 {index + 1} -
-
- )), [taskSketch, currentSketchIndex, isDragging] - ); - - // 缓存渲染的视频缩略图列表 - const renderedVideos = useMemo(() => - taskVideos.map((video, index) => ( -
!isDragging && setCurrentSketchIndex(index)} - > - -
-
-
-
- 场景 {index + 1} -
-
- )), [taskVideos, currentSketchIndex, isDragging, taskSketch] - ); - - // 处理播放/暂停 - const togglePlay = useCallback(() => { - setIsPlaying(prev => !prev); - }, []); - - // 自动播放逻辑 - useEffect(() => { - if (isPlaying && taskSketch.length > 0) { - playTimerRef.current = setInterval(() => { - setCurrentSketchIndex(prev => { - const nextIndex = (prev + 1) % taskSketch.length; - return nextIndex; - }); - }, 2000); // 每2秒切换一次 - } else if (playTimerRef.current) { - clearInterval(playTimerRef.current); - } - - return () => { - if (playTimerRef.current) { - clearInterval(playTimerRef.current); - } - }; - }, [isPlaying, taskSketch.length]); - - // 当切换到视频模式时,停止播放 - useEffect(() => { - if (currentStep === '3') { - setIsPlaying(false); - } - }, [currentStep]); - - // 处理视频播放/暂停 - const toggleVideoPlay = useCallback(() => { - if (mainVideoRef.current) { - if (isVideoPlaying) { - mainVideoRef.current.pause(); - } else { - // 从暂停位置继续播放 - mainVideoRef.current.play(); - } - } - setIsVideoPlaying(prev => !prev); - }, []); - - // 视频自动播放逻辑 - 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 (currentStep !== '3') { - setIsVideoPlaying(false); - } - }, [currentStep]); - - // 更新加载文字 - 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]); - - const renderSketchContent = () => { - - // 展示最终成片 - if (Number(currentStep) === 6) { - return ( -
setShowControls(true)} onMouseLeave={() => setShowControls(false)}> -
- {/* 背景模糊的视频 */} - - - - {/* 最终成片视频 */} - - - - {/* 操作按钮组 */} - - {showControls && ( - <> - {/* 顶部按钮组 */} - - handleEditModalOpen('4')} - /> - - - )} - - - {/* 视频信息浮层 */} - -
-
- - 最终成片 -
-
-
- - {/* 完成标记 */} - - 制作完成 - -
-
- ); - } - - // 展示分镜视频 - if (Number(currentStep) > 2 && Number(currentStep) < 6) { - return ( -
setShowControls(true)} - onMouseLeave={() => setShowControls(false)} - > - {taskVideos[currentSketchIndex] ? ( - -
- {/* 背景模糊的图片 */} -
- background -
- - {/* 视频 */} - - -
-
- ) : ( -
- {/* 保持显示当前分镜草图 */} - {`分镜草图 - -
- )} - - {/* 操作按钮组 */} - - {showControls && ( - <> - {/* 顶部按钮组 */} - - handleEditModalOpen('3')} - /> - - - )} - - - {/* 底部播放按钮 */} - - - - - - - -
- ); - } - - // 展示分镜草图 - return ( -
setShowControls(true)} - onMouseLeave={() => setShowControls(false)} - > - {taskSketch[currentSketchIndex] ? ( - - {`分镜草图 - - ) : ( -
- {/* 动态渐变背景 */} - - {/* 动态光效 */} - - -
- -
-
-
- )} - - {/* 操作按钮组 */} - - {showControls && ( - <> - {/* 顶部按钮组 */} - - handleEditModalOpen('1')} - /> - - - )} - - - {/* 底部播放按钮 */} - - - - - - - - - {/* 播放进度指示器 */} - - {isPlaying && ( - - )} - -
- ); - }; - return (
@@ -900,289 +80,50 @@ export default function WorkFlow() {
- {isLoading ? ( - <> - - - - ) : ( - <> -
{taskObject?.projectName}:{taskObject?.taskName}
- {/* 实时反馈当前 currentLoadingText */} - {currentLoadingText === '任务完成' ? ( - - - - - {currentLoadingText} - - - - - ) : ( - - - - {currentLoadingText} - - - - - )} - - )} +
-
+
{isLoading ? ( ) : ( - <> -
- {renderSketchContent()} -
- +
+ +
)}
- {isLoading ? ( - <> - - - - - - ) : ( -
setIsDragging(false)} - > - {Number(currentStep) === 6 ? null : ( - <> - {(Number(currentStep) > 2 && Number(currentStep) < 6) ? ( - <> - {taskSketch.map((sketch, index) => ( -
!isDragging && setCurrentSketchIndex(index)} - > - -
- {taskVideos[index] ? ( -
-
-
- 场景 {index + 1} -
-
- ))} - - ) : ( - <> - {renderedSketches} - {isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && ( - - {/* 动态渐变背景 */} - - {/* 动态光效 */} - -
-
- -
-
-
- 场景 {sketchCount + 1} -
-
- )} - - )} - - )} -
- )} +
diff --git a/components/pages/work-flow/index.ts b/components/pages/work-flow/index.ts new file mode 100644 index 0000000..c308ef1 --- /dev/null +++ b/components/pages/work-flow/index.ts @@ -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'; \ No newline at end of file diff --git a/components/pages/work-flow/media-viewer.tsx b/components/pages/work-flow/media-viewer.tsx new file mode 100644 index 0000000..f0cf42c --- /dev/null +++ b/components/pages/work-flow/media-viewer.tsx @@ -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(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 = () => ( +
onControlsChange(true)} + onMouseLeave={() => onControlsChange(false)} + > +
+ {/* 背景模糊的视频 */} + + + + {/* 最终成片视频 */} + + + + {/* 操作按钮组 */} + + {showControls && ( + + onEditModalOpen('4')} + /> + + )} + + + {/* 视频信息浮层 */} + +
+
+ + 最终成片 +
+
+
+ + {/* 完成标记 */} + + 制作完成 + +
+
+ ); + + // 渲染视频内容 + const renderVideoContent = () => ( +
onControlsChange(true)} + onMouseLeave={() => onControlsChange(false)} + > + {taskVideos[currentSketchIndex] ? ( + +
+ {/* 背景模糊的图片 */} +
+ background +
+ + {/* 视频 */} + + +
+
+ ) : ( +
+ {`分镜草图 +
+ )} + + {/* 操作按钮组 */} + + {showControls && ( + + onEditModalOpen('3')} + /> + + )} + + + {/* 底部播放按钮 */} + + + + + + + +
+ ); + + // 渲染分镜草图 + const renderSketchContent = () => ( +
onControlsChange(true)} + onMouseLeave={() => onControlsChange(false)} + > + {taskSketch[currentSketchIndex] ? ( + + {`分镜草图 + + ) : ( +
+ {/* 动态渐变背景 */} + + {/* 动态光效 */} + + +
+ +
+
+
+ )} + + {/* 操作按钮组 */} + + {showControls && ( + + onEditModalOpen('1')} + /> + + )} + + + {/* 底部播放按钮 */} + + + + + + + + + {/* 播放进度指示器 */} + + {isPlaying && ( + + )} + +
+ ); + + // 根据当前步骤渲染对应内容 + if (Number(currentStep) === 6) { + return renderFinalVideo(); + } + + if (Number(currentStep) > 2 && Number(currentStep) < 6) { + return renderVideoContent(); + } + + return renderSketchContent(); +} \ No newline at end of file diff --git a/components/pages/work-flow/task-info.tsx b/components/pages/work-flow/task-info.tsx new file mode 100644 index 0000000..6f035b4 --- /dev/null +++ b/components/pages/work-flow/task-info.tsx @@ -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 ( + <> + + + + ); + } + + return ( + <> +
+ {taskObject?.projectName}:{taskObject?.taskName} +
+ + {currentLoadingText === '任务完成' ? ( + + + + + {currentLoadingText} + + + + + ) : ( + + + + {currentLoadingText} + + + + + )} + + ); +} \ No newline at end of file diff --git a/components/pages/work-flow/thumbnail-grid.tsx b/components/pages/work-flow/thumbnail-grid.tsx new file mode 100644 index 0000000..d159624 --- /dev/null +++ b/components/pages/work-flow/thumbnail-grid.tsx @@ -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(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) => { + setIsDragging(true); + setStartX(e.pageX - thumbnailsRef.current!.offsetLeft); + setScrollLeft(thumbnailsRef.current!.scrollLeft); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + 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) => { + 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 ( + <> + + + + + + ); + } + + // 最终成片阶段不显示缩略图 + if (Number(currentStep) === 6) { + return null; + } + + // 渲染生成中的缩略图 + const renderGeneratingThumbnail = () => ( + + {/* 动态渐变背景 */} + + {/* 动态光效 */} + +
+
+ +
+
+
+ 场景 {sketchCount + 1} +
+
+ ); + + // 渲染视频阶段的缩略图 + const renderVideoThumbnails = () => ( + taskSketch.map((sketch, index) => ( +
!isDragging && onSketchSelect(index)} + > + +
+ {taskVideos[index] ? ( +
+
+
+ 场景 {index + 1} +
+
+ )) + ); + + // 渲染分镜草图阶段的缩略图 + const renderSketchThumbnails = () => ( + <> + {taskSketch.map((sketch, index) => ( +
!isDragging && onSketchSelect(index)} + > + +
+ {`缩略图 +
+
+
+ 场景 {index + 1} +
+
+ ))} + {isGeneratingSketch && sketchCount < MOCK_SKETCH_COUNT && renderGeneratingThumbnail()} + + ); + + return ( +
setIsDragging(false)} + > + {Number(currentStep) > 2 && Number(currentStep) < 6 + ? renderVideoThumbnails() + : renderSketchThumbnails() + } +
+ ); +} \ No newline at end of file diff --git a/components/pages/work-flow/use-playback-controls.tsx b/components/pages/work-flow/use-playback-controls.tsx new file mode 100644 index 0000000..c937f1e --- /dev/null +++ b/components/pages/work-flow/use-playback-controls.tsx @@ -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(null); + const videoPlayTimerRef = useRef(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, // 暴露给父组件使用 + }; +} \ No newline at end of file diff --git a/components/pages/work-flow/use-workflow-data.tsx b/components/pages/work-flow/use-workflow-data.tsx new file mode 100644 index 0000000..88e789c --- /dev/null +++ b/components/pages/work-flow/use-workflow-data.tsx @@ -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(null); + const [taskSketch, setTaskSketch] = useState([]); + const [taskVideos, setTaskVideos] = useState([]); + 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, + }; +} \ No newline at end of file diff --git a/components/work-flow/api.ts b/components/work-flow/api.ts new file mode 100644 index 0000000..e974fe6 --- /dev/null +++ b/components/work-flow/api.ts @@ -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 => { + // 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 => { + // 模拟分批获取分镜草图 + 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 => { + await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟 +}; + +// 模拟接口请求 获取背景音 +export const getTaskBackgroundAudio = async (taskId: string): Promise => { + await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟 +}; + +// 模拟接口请求 获取最终成品 +export const getTaskFinalProduct = async (taskId: string): Promise => { + await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟2秒延迟 +}; + +// 模拟接口请求 每次获取一个分镜视频 轮询获取 +export const getTaskVideo = async ( + taskId: string, + sketchCount: number, + onProgress: (video: VideoItem, index: number) => void +): Promise => { + // 模拟分批获取分镜视频 + 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); + } +}; \ No newline at end of file diff --git a/components/work-flow/constants.ts b/components/work-flow/constants.ts new file mode 100644 index 0000000..4b316f7 --- /dev/null +++ b/components/work-flow/constants.ts @@ -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: '任务完成' +}; \ No newline at end of file diff --git a/hooks/useWorkFlow.ts b/hooks/useWorkFlow.ts new file mode 100644 index 0000000..9c20e2f --- /dev/null +++ b/hooks/useWorkFlow.ts @@ -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(null); + const [taskSketch, setTaskSketch] = useState([]); + const [taskVideos, setTaskVideos] = useState([]); + 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(null); + const videoPlayTimerRef = useRef(null); + const mainVideoRef = useRef(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, + }; +}; \ No newline at end of file