forked from 77media/video-flow
251 lines
7.3 KiB
TypeScript
251 lines
7.3 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Heart, Camera, Film, Scissors } from 'lucide-react';
|
|
import { motion } from 'framer-motion';
|
|
import Scriptwriter from './scriptwriter';
|
|
import StoryboardArtist from './storyboard-artist';
|
|
import VisualDirector from './visual-director';
|
|
import Editor from './editor';
|
|
import { scriptwriterData, storyboardData, productionData, editorData } from './mock-data';
|
|
|
|
interface Stage {
|
|
id: string;
|
|
title: string;
|
|
icon: React.ElementType;
|
|
color: string;
|
|
profession: string;
|
|
duration: number; // 加载持续时间(毫秒)
|
|
}
|
|
|
|
const stages: Stage[] = [
|
|
{
|
|
id: 'script',
|
|
title: '编剧工作台',
|
|
icon: Heart,
|
|
color: '#8b5cf6',
|
|
profession: '编剧',
|
|
duration: 3 * 60 * 1000 // 3分钟
|
|
},
|
|
{
|
|
id: 'storyboard',
|
|
title: '分镜设计台',
|
|
icon: Camera,
|
|
color: '#06b6d4',
|
|
profession: '分镜师',
|
|
duration: 8 * 60 * 1000 // 8分钟
|
|
},
|
|
{
|
|
id: 'production',
|
|
title: '制作渲染台',
|
|
icon: Film,
|
|
color: '#10b981',
|
|
profession: '视觉导演',
|
|
duration: 10 * 60 * 1000 // 10分钟
|
|
},
|
|
{
|
|
id: 'editing',
|
|
title: '剪辑调色台',
|
|
icon: Scissors,
|
|
color: '#f59e0b',
|
|
profession: '剪辑师',
|
|
duration: 15 * 60 * 1000 // 15分钟
|
|
}
|
|
];
|
|
|
|
// 思考指示器组件
|
|
const ThinkingDots = ({ show, text, color }: { show: boolean; text: string; color: string }) => {
|
|
if (!show) return null;
|
|
|
|
return (
|
|
<div className="flex items-center space-x-2">
|
|
<div className="flex space-x-1">
|
|
{[0, 1, 2].map((i) => (
|
|
<motion.div
|
|
key={i}
|
|
className="w-2 h-2 rounded-full"
|
|
style={{ backgroundColor: color }}
|
|
animate={{
|
|
scale: [1, 1.2, 1],
|
|
opacity: [0.5, 1, 0.5]
|
|
}}
|
|
transition={{
|
|
duration: 1.5,
|
|
repeat: Infinity,
|
|
delay: i * 0.2
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
<span className="text-white text-sm">{text}</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const WorkOffice: React.FC = () => {
|
|
const [currentStage, setCurrentStage] = useState(0);
|
|
const [isPlaying, setIsPlaying] = useState(true);
|
|
const [progress, setProgress] = useState(0);
|
|
const [currentContent, setCurrentContent] = useState<Record<string, any>>(scriptwriterData);
|
|
const [thinkingText, setThinkingText] = useState(`${stages[0].profession}正在思考...`);
|
|
const [startTime, setStartTime] = useState<number | null>(null);
|
|
|
|
// 模拟数据加载过程
|
|
useEffect(() => {
|
|
if (!isPlaying) return;
|
|
|
|
// 记录开始时间
|
|
if (startTime === null) {
|
|
setStartTime(Date.now());
|
|
}
|
|
|
|
const currentDuration = stages[currentStage].duration;
|
|
const updateInterval = 50; // 更新间隔(毫秒)
|
|
|
|
const interval = setInterval(() => {
|
|
const now = Date.now();
|
|
const elapsed = now - (startTime || now);
|
|
const newProgress = Math.min((elapsed / currentDuration) * 100, 100);
|
|
|
|
setProgress(newProgress);
|
|
|
|
if (newProgress >= 100) {
|
|
setIsPlaying(false);
|
|
setStartTime(null);
|
|
}
|
|
}, updateInterval);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [isPlaying, currentStage, startTime]);
|
|
|
|
// 根据当前阶段加载对应数据
|
|
useEffect(() => {
|
|
let data: Record<string, any> = {};
|
|
switch (currentStage) {
|
|
case 0:
|
|
data = scriptwriterData;
|
|
break;
|
|
case 1:
|
|
data = storyboardData;
|
|
break;
|
|
case 2:
|
|
data = productionData;
|
|
break;
|
|
case 3:
|
|
data = editorData;
|
|
break;
|
|
}
|
|
|
|
// 重置状态并开始新的加载
|
|
setProgress(0);
|
|
setIsPlaying(true);
|
|
setStartTime(Date.now());
|
|
setCurrentContent({});
|
|
setThinkingText(`${stages[currentStage].profession}正在思考...`);
|
|
|
|
// 模拟数据加载延迟
|
|
const loadingTimeout = setTimeout(() => {
|
|
setCurrentContent(data);
|
|
}, 1000);
|
|
|
|
return () => clearTimeout(loadingTimeout);
|
|
}, [currentStage]);
|
|
|
|
// 渲染当前工作台组件
|
|
const renderCurrentWorkstation = () => {
|
|
switch (currentStage) {
|
|
case 0:
|
|
return <Scriptwriter currentContent={currentContent} isPlaying={isPlaying} />;
|
|
case 1:
|
|
return <StoryboardArtist currentContent={currentContent} isPlaying={isPlaying} />;
|
|
case 2:
|
|
return <VisualDirector currentContent={currentContent} isPlaying={isPlaying} />;
|
|
case 3:
|
|
return <Editor currentContent={currentContent} isPlaying={isPlaying} />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// 计算剩余时间
|
|
const getRemainingTime = () => {
|
|
if (!isPlaying || startTime === null) return '0:00';
|
|
|
|
const currentDuration = stages[currentStage].duration;
|
|
const elapsed = Date.now() - startTime;
|
|
const remaining = Math.max(0, currentDuration - elapsed);
|
|
|
|
const minutes = Math.floor(remaining / 60000);
|
|
const seconds = Math.floor((remaining % 60000) / 1000);
|
|
|
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
};
|
|
|
|
return (
|
|
<div className="h-full rounded-2xl overflow-hidden shadow-2xl relative">
|
|
{/* 正在加载的部分 文字显示 */}
|
|
<div className="absolute top-[0] left-1/2 -translate-x-1/2 z-10">
|
|
<ThinkingDots
|
|
show={isPlaying}
|
|
text={thinkingText}
|
|
color={stages[currentStage].color}
|
|
/>
|
|
</div>
|
|
|
|
{/* 工作台内容区域 */}
|
|
<div className="absolute left-0 right-0 top-[2rem] w-full aspect-video overflow-y-auto" style={{height: 'calc(100% - 11rem'}}>
|
|
{renderCurrentWorkstation()}
|
|
</div>
|
|
|
|
{/* 底部控制栏 */}
|
|
<div className="p-6 absolute bottom-0 left-0 right-0">
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="text-white font-medium">
|
|
{stages[currentStage].title}
|
|
</span>
|
|
<div className="flex items-center space-x-4">
|
|
<span className="text-gray-400 text-sm">
|
|
剩余时间: {getRemainingTime()}
|
|
</span>
|
|
<span className="text-gray-400 text-sm">
|
|
{Math.round(progress)}% 完成
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="w-full bg-gray-700 rounded-full h-3">
|
|
<motion.div
|
|
className="h-3 rounded-full transition-all duration-300"
|
|
style={{
|
|
width: `${progress}%`,
|
|
backgroundColor: stages[currentStage].color
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 工作台切换按钮组 */}
|
|
<div className="flex justify-center space-x-4">
|
|
{stages.map((stage, index) => (
|
|
<button
|
|
key={stage.id}
|
|
onClick={() => {
|
|
if (!isPlaying) {
|
|
setCurrentStage(index);
|
|
}
|
|
}}
|
|
disabled={isPlaying}
|
|
className={`w-12 h-12 rounded-full flex items-center justify-center transition-all ${
|
|
currentStage === index ? 'ring-2 ring-white/50' : 'hover:bg-white/10'
|
|
} ${isPlaying ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
style={{ backgroundColor: `${stage.color}40` }}
|
|
>
|
|
<stage.icon className="w-6 h-6 text-white" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default WorkOffice;
|