forked from 77media/video-flow
444 lines
14 KiB
TypeScript
444 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { ScriptModal } from '@/components/ui/script-modal';
|
|
import {
|
|
CheckCircle,
|
|
Heart,
|
|
Camera,
|
|
Film,
|
|
Scissors
|
|
} from 'lucide-react';
|
|
|
|
interface TaskInfoProps {
|
|
isLoading: boolean;
|
|
taskObject: any;
|
|
currentLoadingText: string;
|
|
dataLoadError?: string | null;
|
|
roles: any[];
|
|
}
|
|
|
|
const stageIconMap = {
|
|
0: {
|
|
icon: Heart,
|
|
color: '#8b5cf6'
|
|
},
|
|
1: {
|
|
icon: Camera,
|
|
color: '#06b6d4'
|
|
},
|
|
2: {
|
|
icon: Film,
|
|
color: '#10b981'
|
|
},
|
|
3: {
|
|
icon: Scissors,
|
|
color: '#f59e0b'
|
|
}
|
|
}
|
|
|
|
const TAG_COLORS = ['#FF5733', '#126821', '#8d3913', '#FF33A1', '#A133FF', '#FF3333', '#3333FF', '#A1A1A1', '#a1115e', '#30527f'];
|
|
|
|
// 阶段图标组件
|
|
const StageIcons = ({ currentStage, isExpanded }: { currentStage: number, isExpanded: boolean }) => {
|
|
// 根据当前阶段重新排序图标
|
|
const orderedStages = useMemo(() => {
|
|
const stages = Object.entries(stageIconMap).map(([stage, data]) => ({
|
|
stage: parseInt(stage),
|
|
...data
|
|
}));
|
|
|
|
return stages;
|
|
}, [currentStage]);
|
|
|
|
return (
|
|
<motion.div
|
|
className="relative flex items-center"
|
|
>
|
|
<AnimatePresence mode="popLayout">
|
|
{orderedStages.map((stage, index) => {
|
|
const isCurrentStage = stage.stage === currentStage;
|
|
const Icon = stage.icon;
|
|
|
|
// 只显示当前阶段或展开状态
|
|
if (!isExpanded && !isCurrentStage) return null;
|
|
|
|
return (
|
|
<motion.div
|
|
key={stage.stage}
|
|
className="relative"
|
|
initial={isExpanded ? {
|
|
opacity: 0,
|
|
x: -20,
|
|
scale: 0.5
|
|
} : {}}
|
|
animate={{
|
|
opacity: 1,
|
|
x: 0,
|
|
scale: 1,
|
|
transition: {
|
|
type: "spring",
|
|
stiffness: 300,
|
|
damping: 25,
|
|
delay: index * 0.1
|
|
}
|
|
}}
|
|
exit={{
|
|
opacity: 0,
|
|
x: 20,
|
|
scale: 0.5,
|
|
transition: { duration: 0.2 }
|
|
}}
|
|
style={{
|
|
marginLeft: index > 0 ? '8px' : '0px',
|
|
zIndex: isCurrentStage ? 2 : 1
|
|
}}
|
|
>
|
|
<motion.div
|
|
className={`relative rounded-full p-1 ${isCurrentStage ? 'bg-opacity-20' : 'bg-opacity-10'}`}
|
|
animate={isCurrentStage ? {
|
|
rotate: [0, 360],
|
|
scale: [1, 1.2, 1],
|
|
transition: {
|
|
rotate: { duration: 3, repeat: Infinity, ease: "linear" },
|
|
scale: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
|
|
}
|
|
} : {}}
|
|
>
|
|
<Icon
|
|
className="w-5 h-5"
|
|
style={{ color: stage.color }}
|
|
/>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</AnimatePresence>
|
|
</motion.div>
|
|
);
|
|
};
|
|
|
|
export function TaskInfo({
|
|
isLoading,
|
|
taskObject,
|
|
currentLoadingText,
|
|
dataLoadError,
|
|
roles
|
|
}: TaskInfoProps) {
|
|
const [isScriptModalOpen, setIsScriptModalOpen] = useState(false);
|
|
const [currentStage, setCurrentStage] = useState(0);
|
|
const [isShowScriptIcon, setIsShowScriptIcon] = useState(true);
|
|
const [isStageIconsExpanded, setIsStageIconsExpanded] = useState(false);
|
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const stageColor = useMemo(() => {
|
|
return stageIconMap[currentStage as keyof typeof stageIconMap].color;
|
|
}, [currentStage]);
|
|
|
|
// 监听 currentLoadingText
|
|
useEffect(() => {
|
|
// 清理之前的定时器
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
|
|
if (currentLoadingText.includes('Task completed')) {
|
|
console.log('Closing modal at completion');
|
|
setIsScriptModalOpen(false);
|
|
setIsShowScriptIcon(false);
|
|
}
|
|
if (currentLoadingText.includes('Post-production')) {
|
|
if (isScriptModalOpen) {
|
|
setIsScriptModalOpen(false);
|
|
}
|
|
setCurrentStage(3);
|
|
console.log('isScriptModalOpen-Post-production', currentLoadingText, isScriptModalOpen);
|
|
timerRef.current = setTimeout(() => {
|
|
setIsScriptModalOpen(true);
|
|
}, 8000);
|
|
}
|
|
if (currentLoadingText.includes('video') && !currentLoadingText.includes('Post-production')) {
|
|
console.log('isScriptModalOpen-video', currentLoadingText, isScriptModalOpen);
|
|
if (isScriptModalOpen) {
|
|
setIsScriptModalOpen(false);
|
|
setCurrentStage(2);
|
|
|
|
// 延迟8s 再次打开
|
|
timerRef.current = setTimeout(() => {
|
|
setIsScriptModalOpen(true);
|
|
}, 8000);
|
|
} else {
|
|
setIsScriptModalOpen(true);
|
|
setCurrentStage(2);
|
|
}
|
|
}
|
|
if (currentLoadingText.includes('video status')) {
|
|
if (isScriptModalOpen) {
|
|
setIsScriptModalOpen(false);
|
|
}
|
|
setIsScriptModalOpen(true);
|
|
setCurrentStage(2);
|
|
}
|
|
if (currentLoadingText.includes('sketch')) {
|
|
console.log('isScriptModalOpen-sketch', currentLoadingText, isScriptModalOpen);
|
|
if (isScriptModalOpen) {
|
|
setIsScriptModalOpen(false);
|
|
setCurrentStage(1);
|
|
|
|
// 延迟8s 再次打开
|
|
timerRef.current = setTimeout(() => {
|
|
setIsScriptModalOpen(true);
|
|
}, 8000);
|
|
} else {
|
|
setIsScriptModalOpen(true);
|
|
setCurrentStage(1);
|
|
}
|
|
}
|
|
if (currentLoadingText.includes('sketch status')) {
|
|
if (isScriptModalOpen) {
|
|
setIsScriptModalOpen(false);
|
|
}
|
|
setIsScriptModalOpen(true);
|
|
setCurrentStage(1);
|
|
}
|
|
// if (currentLoadingText.includes('character')) {
|
|
// console.log('isScriptModalOpen-character', currentLoadingText, isScriptModalOpen);
|
|
// if (isScriptModalOpen) {
|
|
// setIsScriptModalOpen(false);
|
|
|
|
// // 延迟8s 再次打开
|
|
// timerRef.current = setTimeout(() => {
|
|
// setIsScriptModalOpen(true);
|
|
// setCurrentStage(0);
|
|
// }, 8000);
|
|
// } else {
|
|
// setIsScriptModalOpen(true);
|
|
// setCurrentStage(0);
|
|
// }
|
|
// }
|
|
if (currentLoadingText.includes('initializing')) {
|
|
console.log('isScriptModalOpen-initializing', currentLoadingText, isScriptModalOpen);
|
|
setIsScriptModalOpen(true);
|
|
setCurrentStage(0);
|
|
}
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
}
|
|
}, [currentLoadingText]);
|
|
|
|
// 使用 useMemo 缓存标签颜色映射
|
|
const tagColors = useMemo(() => {
|
|
if (!taskObject?.tags) return {};
|
|
return taskObject.tags.reduce((acc: Record<string, string>, tag: string) => {
|
|
acc[tag] = TAG_COLORS[Math.floor(Math.random() * TAG_COLORS.length)];
|
|
return acc;
|
|
}, {});
|
|
}, [taskObject?.tags]); // 只在 tags 改变时重新计算
|
|
|
|
return (
|
|
<>
|
|
<div className="title-JtMejk flex items-center justify-center gap-2">
|
|
{taskObject?.title ? (
|
|
<>
|
|
<span>
|
|
{taskObject?.title || 'loading project info...'}
|
|
</span>
|
|
</>
|
|
) : 'loading project info...'}
|
|
</div>
|
|
|
|
{/* 主题 彩色标签tags */}
|
|
<div className="flex items-center justify-center gap-2">
|
|
{taskObject?.tags?.map((tag: string) => (
|
|
<div
|
|
key={tag}
|
|
className="text-sm text-white rounded-full px-2 py-1"
|
|
style={{ backgroundColor: tagColors[tag] }}
|
|
>
|
|
{tag}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<ScriptModal
|
|
isOpen={isScriptModalOpen}
|
|
onClose={() => {
|
|
console.log('Modal manually closed');
|
|
setIsScriptModalOpen(false);
|
|
}}
|
|
currentStage={currentStage}
|
|
roles={roles}
|
|
currentLoadingText={currentLoadingText}
|
|
/>
|
|
|
|
{currentLoadingText === 'Task completed' ? (
|
|
<motion.div
|
|
className="flex items-center gap-3 justify-center"
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<CheckCircle className="w-5 h-5 text-emerald-500" />
|
|
<span className="text-emerald-500 font-medium">{currentLoadingText}</span>
|
|
</div>
|
|
</motion.div>
|
|
) : (
|
|
<motion.div
|
|
className="flex items-center gap-2 justify-center cursor-pointer"
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3 }}
|
|
onClick={() => setIsScriptModalOpen(true)}
|
|
>
|
|
<motion.div
|
|
className="w-1.5 h-1.5 rounded-full"
|
|
style={{ backgroundColor: stageColor }}
|
|
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-2"
|
|
key={currentLoadingText}
|
|
initial={{ opacity: 0, x: -10 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0.5, x: 10 }}
|
|
transition={{ duration: 0.3 }}
|
|
onMouseEnter={() => setIsStageIconsExpanded(true)}
|
|
onMouseLeave={() => setIsStageIconsExpanded(false)}
|
|
>
|
|
<StageIcons currentStage={currentStage} isExpanded={isStageIconsExpanded} />
|
|
|
|
<motion.div
|
|
className="relative"
|
|
initial={{ opacity: 0, x: -10 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: 10 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
{/* 背景发光效果 */}
|
|
<motion.div
|
|
className="absolute inset-0 text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-cyan-400 to-purple-400 blur-sm"
|
|
animate={{
|
|
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
|
}}
|
|
transition={{
|
|
duration: 2,
|
|
repeat: Infinity,
|
|
ease: "linear"
|
|
}}
|
|
style={{
|
|
backgroundSize: "200% 200%",
|
|
}}
|
|
>
|
|
<span className="normalS400 subtitle-had8uE">{currentLoadingText}</span>
|
|
</motion.div>
|
|
|
|
{/* 主文字 - 颜色填充动画 */}
|
|
<motion.div
|
|
className="relative z-10"
|
|
animate={{
|
|
scale: [1, 1.02, 1],
|
|
}}
|
|
transition={{
|
|
duration: 1.5,
|
|
repeat: Infinity,
|
|
ease: "easeInOut"
|
|
}}
|
|
>
|
|
<motion.span
|
|
className="normalS400 subtitle-had8uE text-transparent bg-clip-text bg-gradient-to-r from-blue-600 via-cyan-500 to-purple-600"
|
|
animate={{
|
|
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
|
}}
|
|
transition={{
|
|
duration: 3,
|
|
repeat: Infinity,
|
|
ease: "linear"
|
|
}}
|
|
style={{
|
|
backgroundSize: "300% 300%",
|
|
}}
|
|
>
|
|
{currentLoadingText}
|
|
</motion.span>
|
|
</motion.div>
|
|
|
|
{/* 动态光点效果 */}
|
|
<motion.div
|
|
className="absolute left-0 top-1/2 transform -translate-y-1/2 w-2 h-2 bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full blur-sm"
|
|
animate={{
|
|
x: [0, 200, 0],
|
|
opacity: [0, 1, 0],
|
|
scale: [0.5, 1, 0.5],
|
|
}}
|
|
transition={{
|
|
duration: 2.5,
|
|
repeat: Infinity,
|
|
ease: "easeInOut",
|
|
}}
|
|
/>
|
|
|
|
{/* 文字底部装饰线 */}
|
|
<motion.div
|
|
className="absolute bottom-0 left-0 h-0.5"
|
|
style={{
|
|
background: `linear-gradient(to right, ${stageColor}, rgb(34 211 238), rgb(168 85 247))`,
|
|
}}
|
|
animate={{
|
|
width: ["0%", "100%", "0%"]
|
|
}}
|
|
transition={{
|
|
width: { duration: 2, repeat: Infinity, ease: "easeInOut" }
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="w-1.5 h-1.5 rounded-full"
|
|
style={{ backgroundColor: stageColor }}
|
|
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"
|
|
style={{ backgroundColor: stageColor }}
|
|
animate={{
|
|
scale: [1, 1.5, 1],
|
|
opacity: [1, 0.5, 1]
|
|
}}
|
|
transition={{
|
|
duration: 1,
|
|
repeat: Infinity,
|
|
repeatDelay: 0.2,
|
|
delay: 0.6
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|