2025-09-25 16:26:23 +08:00

209 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import React, { useEffect, useMemo, useState } from 'react'
import { TaskObject } from '@/api/DTO/movieEdit'
import { motion, AnimatePresence } from 'framer-motion'
import { Heart, Camera, Film, Scissors, type LucideIcon } from 'lucide-react'
interface H5TaskInfoProps {
/** 标题文案 */
title: string
/** 当前分镜序号从1开始 */
current: number
/** 任务对象(用于读取总数等信息) */
taskObject: TaskObject
className?: string
currentLoadingText: string
/** 选中视图final 或 video */
selectedView?: 'final' | 'video' | null
}
const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
title,
current,
taskObject,
className,
currentLoadingText,
selectedView
}) => {
type StageIndex = 0 | 1 | 2 | 3
const [currentStage, setCurrentStage] = useState<StageIndex>(0)
const stageIconMap: Record<StageIndex, { icon: LucideIcon; color: string }> = {
0: { icon: Heart, color: '#8b5cf6' },
1: { icon: Camera, color: '#06b6d4' },
2: { icon: Film, color: '#10b981' },
3: { icon: Scissors, color: '#f59e0b' }
}
const computeStage = (text: string): StageIndex => {
if (text.includes('initializing...') || text.includes('script') || text.includes('character')) return 0
if (text.includes('sketch') && !text.includes('shot sketch')) return 1
if (!text.includes('Post-production') && (text.includes('shot sketch') || text.includes('video'))) return 2
if (text.includes('Post-production')) return 3
return 0
}
useEffect(() => {
setCurrentStage(computeStage(currentLoadingText))
}, [currentLoadingText])
const stageColor = useMemo(() => stageIconMap[currentStage].color, [currentStage])
const total = useMemo(() => {
if (taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') {
return taskObject.videos?.total_count || taskObject.videos?.data?.length || 0
}
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
const rolesTotal = taskObject.roles?.total_count || taskObject.roles?.data?.length || 0
const scenesTotal = taskObject.scenes?.total_count || taskObject.scenes?.data?.length || 0
return rolesTotal + scenesTotal
}
return 0
}, [taskObject])
const shouldShowCount = taskObject.currentStage !== 'script'
const displayCurrent = useMemo(() => {
if (taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') {
return Math.max(current, 1)
}
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
const bounded = Math.min(Math.max(current, 1), Math.max(total, 1))
return bounded
}
return 0
}, [taskObject, current, total])
// 构造副标题文本:优先根据 selectedView 覆盖
const subtitle = useMemo(() => {
if (selectedView === 'final' && taskObject.final?.url) {
return 'Final 1/1'
}
if (selectedView === 'video' && !['scene', 'character'].includes(taskObject.currentStage)) {
const videosTotal = taskObject.videos?.total_count || taskObject.videos?.data?.length || 0
return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(videosTotal, 1)}`
}
// 回退到原有逻辑
if (taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') {
return `Shots ${Math.max(displayCurrent, 1)}/${Math.max(total, 1)}`
}
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
return `Roles & Scenes ${Math.max(displayCurrent, 1)}/${Math.max(total, 1)}`
}
return null
}, [selectedView, taskObject, displayCurrent, total])
/** 阶段图标H5 精简版) */
const StageIcon = useMemo(() => {
const Icon = stageIconMap[currentStage].icon
return (
<motion.div
data-alt="stage-icon"
className="relative"
initial={{ opacity: 0, x: -8, scale: 0.9 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
transition={{ duration: 0.25 }}
>
<motion.div
className="rounded-full p-1"
animate={{
rotate: [0, 360],
scale: [1, 1.1, 1],
transition: {
rotate: { duration: 3, repeat: Infinity, ease: 'linear' },
scale: { duration: 1.6, repeat: Infinity, ease: 'easeInOut' }
}
}}
>
<Icon className="w-4 h-4" style={{ color: stageColor }} />
</motion.div>
</motion.div>
)
}, [currentStage, stageColor])
return (
<div
data-alt="h5-header"
className={`absolute top-0 -left-4 right-0 z-[50] pr-1 ${className || ''}`}
>
<div
data-alt="h5-header-bar"
className="flex items-start gap-3"
>
{/* 左侧标题区域 */}
<div data-alt="title-area" className="flex-1 min-w-0 bg-gradient-to-b from-slate-900/80 via-slate-900/40 to-transparent backdrop-blur-sm rounded-lg py-4 px-4">
<h1
data-alt="title"
className="text-white text-lg font-bold"
title={title}
>
{title || '...'}
</h1>
{shouldShowCount && subtitle && (
<span data-alt="shot-count" className="flex items-center gap-4 text-sm text-slate-300">
{subtitle}
</span>
)}
</div>
{/* 右侧状态区域 */}
<div data-alt="status-area" className="flex-shrink-0 bg-gradient-to-b from-slate-900/80 via-slate-900/40 to-transparent backdrop-blur-sm rounded-lg py-4 px-4 max-w-[200px]">
<AnimatePresence mode="popLayout">
{currentLoadingText && currentLoadingText !== 'Task completed' && (
<motion.div
key={currentLoadingText}
data-alt="status-line"
className="flex flex-col gap-2"
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.25 }}
>
<div className="flex items-center justify-center">
{StageIcon}
</div>
<div className="relative text-center">
{/* 背景流光 */}
<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-[1px]"
animate={{
backgroundPosition: ['0% 50%', '100% 50%', '0% 50%'],
transition: { duration: 2, repeat: Infinity, ease: 'linear' }
}}
style={{ backgroundSize: '200% 200%' }}
>
<span className="text-xs leading-tight break-words">{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.div>
{/* 底部装饰线 */}
<motion.div
className="absolute -bottom-0.5 left-1/2 transform -translate-x-1/2 h-0.5 w-8"
style={{
background: `linear-gradient(to right, ${stageColor}, rgb(34 211 238), rgb(168 85 247))`
}}
animate={{ width: ['0%', '100%', '0%'] }}
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
)
}
export default H5TaskInfo