forked from 77media/video-flow
158 lines
5.2 KiB
TypeScript
158 lines
5.2 KiB
TypeScript
'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={`${className || ''}`}
|
||
>
|
||
<div
|
||
data-alt="h5-header-bar"
|
||
className="flex items-start gap-3"
|
||
>
|
||
{/* 左侧标题区域 */}
|
||
<div data-alt="title-area" className="flex-1 min-w-0">
|
||
<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>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default H5TaskInfo
|
||
|
||
|