forked from 77media/video-flow
304 lines
11 KiB
TypeScript
304 lines
11 KiB
TypeScript
"use client"
|
||
import React, { useRef, useEffect } from "react";
|
||
import "./style/work-flow.css";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import { AISuggestionBar } from "@/components/ai-suggestion-bar";
|
||
import { EditModal } from "@/components/ui/edit-modal";
|
||
import { ErrorBoundary } from "@/components/ui/error-boundary";
|
||
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";
|
||
import { AlertCircle, RefreshCw } from "lucide-react";
|
||
import { motion } from "framer-motion";
|
||
|
||
export default function WorkFlow() {
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
|
||
const [activeEditTab, setActiveEditTab] = React.useState('1');
|
||
|
||
// 使用自定义 hooks 管理状态
|
||
const {
|
||
taskObject,
|
||
taskSketch,
|
||
taskVideos,
|
||
sketchCount,
|
||
isLoading,
|
||
currentStep,
|
||
currentSketchIndex,
|
||
isGeneratingSketch,
|
||
isGeneratingVideo,
|
||
currentLoadingText,
|
||
totalSketchCount,
|
||
roles,
|
||
music,
|
||
final,
|
||
dataLoadError,
|
||
setCurrentSketchIndex,
|
||
retryLoadData,
|
||
} = useWorkflowData();
|
||
|
||
const {
|
||
isPlaying,
|
||
isVideoPlaying,
|
||
showControls,
|
||
setShowControls,
|
||
setIsPlaying,
|
||
togglePlay,
|
||
toggleVideoPlay,
|
||
playTimerRef,
|
||
} = usePlaybackControls(taskSketch, taskVideos, currentStep);
|
||
|
||
// 跟踪是否已经自动开始播放过,避免重复触发
|
||
const hasAutoStartedRef = useRef(false);
|
||
|
||
// 跟踪循环播放的起始索引,用于判断是否完成一轮循环
|
||
const loopStartIndexRef = useRef<number | null>(null);
|
||
|
||
// 调试:监控关键状态变化
|
||
useEffect(() => {
|
||
console.log('工作流状态:', {
|
||
currentStep,
|
||
isGeneratingSketch,
|
||
isGeneratingVideo,
|
||
isPlaying,
|
||
taskSketchLength: taskSketch.length,
|
||
sketchCount,
|
||
totalSketchCount
|
||
});
|
||
}, [isGeneratingSketch, taskSketch.length, sketchCount, totalSketchCount, currentStep, isPlaying]);
|
||
|
||
// 专门监控isPlaying状态变化
|
||
useEffect(() => {
|
||
console.log('播放状态变化:', isPlaying ? '开始播放' : '停止播放');
|
||
}, [isPlaying]);
|
||
|
||
// 检查分镜数据
|
||
useEffect(() => {
|
||
if (taskSketch.length > 0) {
|
||
console.log('分镜数据:', `${taskSketch.length}个分镜,当前索引:${currentSketchIndex}`);
|
||
}
|
||
}, [taskSketch.length, currentSketchIndex]);
|
||
|
||
// 第一个分镜视频生成完成时停止循环播放并切换到第一个
|
||
useEffect(() => {
|
||
if (taskVideos.length === 1 && isPlaying) {
|
||
console.log('第一个分镜视频生成完成,停止循环播放并切换到第一个分镜');
|
||
setIsPlaying(false); // 停止循环播放
|
||
setCurrentSketchIndex(0); // 切换到第一个分镜
|
||
}
|
||
}, [taskVideos.length, isPlaying, setIsPlaying, setCurrentSketchIndex]);
|
||
|
||
// 分镜草图生成完毕后自动开始播放
|
||
useEffect(() => {
|
||
if (
|
||
!isGeneratingSketch && // 分镜草图生成完毕
|
||
taskSketch.length > 0 && // 有分镜草图数据
|
||
sketchCount === totalSketchCount && // 确保所有分镜草图都生成完毕
|
||
(currentStep === '1' || currentStep === '2') && // 允许在步骤1或步骤2初期触发
|
||
!hasAutoStartedRef.current && // 还没有自动开始过播放
|
||
!isPlaying // 当前没有播放
|
||
) {
|
||
console.log('所有分镜草图生成完毕,自动开始播放');
|
||
// 添加小延迟确保状态完全更新
|
||
setTimeout(() => {
|
||
hasAutoStartedRef.current = true;
|
||
setIsPlaying(true); // 自动开始播放
|
||
}, 500);
|
||
}
|
||
|
||
// 当切换到步骤3及以后时重置标记
|
||
if (Number(currentStep) >= 3) {
|
||
hasAutoStartedRef.current = false;
|
||
}
|
||
}, [isGeneratingSketch, taskSketch.length, sketchCount, totalSketchCount, currentStep, isPlaying, setIsPlaying]);
|
||
|
||
// 处理自动播放的分镜切换逻辑 仅循环一轮
|
||
useEffect(() => {
|
||
if (isPlaying && taskSketch.length > 0) {
|
||
console.log('开始自动切换分镜,总数:', taskSketch.length);
|
||
|
||
// 记录循环开始时的索引
|
||
if (loopStartIndexRef.current === null) {
|
||
loopStartIndexRef.current = currentSketchIndex;
|
||
console.log('记录循环起始索引:', currentSketchIndex);
|
||
}
|
||
|
||
const interval = setInterval(() => {
|
||
setCurrentSketchIndex((prev: number) => {
|
||
const nextIndex = (prev + 1) % taskSketch.length;
|
||
|
||
// 检查是否完成了一轮循环(回到起始索引)
|
||
if (nextIndex === loopStartIndexRef.current && prev !== loopStartIndexRef.current) {
|
||
console.log('完成一轮循环,停止自动播放');
|
||
setTimeout(() => {
|
||
setIsPlaying(false);
|
||
loopStartIndexRef.current = null; // 重置循环起始索引
|
||
}, 1000); // 延迟1秒停止,让最后一个分镜显示完整
|
||
}
|
||
|
||
return nextIndex;
|
||
});
|
||
}, 1000);
|
||
|
||
return () => {
|
||
clearInterval(interval);
|
||
};
|
||
} else {
|
||
// 当停止播放时重置循环起始索引
|
||
loopStartIndexRef.current = null;
|
||
}
|
||
}, [isPlaying, taskSketch.length, setCurrentSketchIndex, currentSketchIndex]);
|
||
|
||
// 模拟 AI 建议 英文
|
||
const mockSuggestions = [
|
||
"Refine scene transitions",
|
||
"Adjust scene composition",
|
||
"Improve character action design",
|
||
"Add environmental atmosphere",
|
||
"Adjust lens language"
|
||
];
|
||
|
||
const handleEditModalOpen = (tab: string) => {
|
||
setActiveEditTab(tab);
|
||
setIsEditModalOpen(true);
|
||
};
|
||
|
||
const handleSuggestionClick = (suggestion: string) => {
|
||
console.log('Selected suggestion:', suggestion);
|
||
};
|
||
|
||
const handleSubmit = (text: string) => {
|
||
console.log('Submitted text:', text);
|
||
};
|
||
|
||
return (
|
||
<ErrorBoundary>
|
||
<div className="w-full h-full overflow-hidden">
|
||
<div className="flex h-full flex-col p-6 justify-center items-center pt-0">
|
||
<div className="container-H2sRZG">
|
||
<div className="splashContainer-otuV_A">
|
||
<div className="content-vPGYx8">
|
||
<div className="info-UUGkPJ">
|
||
<ErrorBoundary>
|
||
<TaskInfo
|
||
isLoading={isLoading}
|
||
taskObject={taskObject}
|
||
currentLoadingText={currentLoadingText}
|
||
dataLoadError={dataLoadError}
|
||
/>
|
||
</ErrorBoundary>
|
||
</div>
|
||
</div>
|
||
<div className="media-Ocdu1O">
|
||
<div
|
||
className="videoContainer-qteKNi"
|
||
style={currentStep !== '6' ? { flex: 3 } : {}}
|
||
ref={containerRef}
|
||
>
|
||
{dataLoadError ? (
|
||
<motion.div
|
||
className="flex flex-col items-center justify-center w-full aspect-video rounded-lg bg-red-50 border-2 border-red-200"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.5 }}
|
||
>
|
||
<motion.div
|
||
className="flex items-center gap-3 mb-4"
|
||
initial={{ scale: 0.8 }}
|
||
animate={{ scale: 1 }}
|
||
transition={{ duration: 0.3, delay: 0.2 }}
|
||
>
|
||
<AlertCircle className="w-8 h-8 text-red-500" />
|
||
<h3 className="text-lg font-medium text-red-800">数据加载失败</h3>
|
||
</motion.div>
|
||
|
||
<p className="text-red-600 text-center mb-6 max-w-md px-4">
|
||
{dataLoadError}
|
||
</p>
|
||
|
||
<motion.button
|
||
className="flex items-center gap-2 px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||
onClick={() => retryLoadData?.()}
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<RefreshCw className="w-4 h-4" />
|
||
重试加载
|
||
</motion.button>
|
||
</motion.div>
|
||
) : isLoading ? (
|
||
<Skeleton className="w-full aspect-video rounded-lg" />
|
||
) : (
|
||
<div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }}>
|
||
<ErrorBoundary>
|
||
<MediaViewer
|
||
currentStep={currentStep}
|
||
currentSketchIndex={currentSketchIndex}
|
||
taskSketch={taskSketch}
|
||
taskVideos={taskVideos}
|
||
isVideoPlaying={isVideoPlaying}
|
||
isPlaying={isPlaying}
|
||
showControls={showControls}
|
||
isGeneratingSketch={isGeneratingSketch}
|
||
isGeneratingVideo={isGeneratingVideo}
|
||
onControlsChange={setShowControls}
|
||
onEditModalOpen={handleEditModalOpen}
|
||
onToggleVideoPlay={toggleVideoPlay}
|
||
onTogglePlay={togglePlay}
|
||
final={final}
|
||
/>
|
||
</ErrorBoundary>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="imageGrid-ymZV9z hide-scrollbar" style={{ display: currentStep === '6' ? 'none' : 'block' }}>
|
||
<ErrorBoundary>
|
||
<ThumbnailGrid
|
||
isLoading={isLoading}
|
||
currentStep={currentStep}
|
||
currentSketchIndex={currentSketchIndex}
|
||
taskSketch={taskSketch}
|
||
taskVideos={taskVideos}
|
||
isGeneratingSketch={isGeneratingSketch}
|
||
isGeneratingVideo={isGeneratingVideo}
|
||
sketchCount={sketchCount}
|
||
totalSketchCount={totalSketchCount}
|
||
onSketchSelect={setCurrentSketchIndex}
|
||
/>
|
||
</ErrorBoundary>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* AI 建议栏 */}
|
||
<ErrorBoundary>
|
||
<AISuggestionBar
|
||
suggestions={mockSuggestions}
|
||
onSuggestionClick={handleSuggestionClick}
|
||
onSubmit={handleSubmit}
|
||
placeholder="Please input your ideas, or click the predefined tags to receive AI advice..."
|
||
/>
|
||
</ErrorBoundary>
|
||
|
||
<ErrorBoundary>
|
||
<EditModal
|
||
isOpen={isEditModalOpen}
|
||
activeEditTab={activeEditTab}
|
||
onClose={() => setIsEditModalOpen(false)}
|
||
taskStatus={taskObject?.taskStatus || '1'}
|
||
taskSketch={taskSketch}
|
||
sketchVideo={taskVideos}
|
||
currentSketchIndex={currentSketchIndex}
|
||
onSketchSelect={setCurrentSketchIndex}
|
||
roles={roles}
|
||
music={music}
|
||
/>
|
||
</ErrorBoundary>
|
||
</div>
|
||
</ErrorBoundary>
|
||
)
|
||
} |