2025-10-10 17:35:18 +08:00

293 lines
10 KiB
TypeScript

'use client'
import React, { useMemo, useEffect, useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Heart, Camera, Film, Scissors, type LucideIcon } from 'lucide-react'
import { TaskObject } from '@/api/DTO/movieEdit'
interface H5ProgressBarProps {
taskObject: TaskObject
scriptData: any
/** Loading text for stage detection */
currentLoadingText: string
className?: string
}
/** Stage configuration map */
const stageIconMap: Record<number, { icon: LucideIcon; color: string; label: string }> = {
0: { icon: Heart, color: '#6bf5f9', label: 'Script' },
1: { icon: Camera, color: '#88bafb', label: 'Roles & Scenes' },
2: { icon: Film, color: '#a285fd', label: 'Shots' },
3: { icon: Scissors, color: '#c73dff', label: 'Final' }
}
const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
taskObject,
scriptData,
currentLoadingText,
className
}) => {
/** Calculate current stage based on taskObject state */
const currentStage = useMemo(() => {
/** Check if roles & scenes are completed */
const rolesCompleted =
taskObject.roles?.total_count > 0 &&
taskObject.roles?.data?.filter(v => v.status !== 0).length === taskObject.roles?.total_count
const scenesCompleted =
taskObject.scenes?.total_count > 0 &&
taskObject.scenes?.data?.filter(v => v.status !== 0).length === taskObject.scenes?.total_count
const rolesAndScenesCompleted = rolesCompleted && scenesCompleted
/** Check if videos are completed or nearly completed */
const videosCount = taskObject.videos?.data?.filter(v => v.video_status !== 0).length || 0
const videosTotal = taskObject.videos?.total_count || 0
const videosCompleted = videosTotal > 0 && videosCount === videosTotal
const videosNearlyComplete = videosTotal > 0 && videosCount >= videosTotal * 0.9 /** 90% complete */
/** Check if final video exists */
const finalVideoExists = !!taskObject.final?.url
/** Determine stage based on conditions */
if (finalVideoExists) {
return 4 /** Completed - all done */
}
/** Enter final video stage when videos are completed or nearly complete, or stage is explicitly set */
if (videosCompleted || videosNearlyComplete || taskObject.currentStage === 'final_video') {
return 3 /** Final video stage */
}
if (rolesAndScenesCompleted || taskObject.currentStage === 'video') {
return 2 /** Shots/video stage */
}
if (scriptData || taskObject.currentStage === 'character' || taskObject.currentStage === 'scene') {
return 1 /** Roles & scenes stage */
}
if (taskObject.currentStage === 'script') {
return 0 /** Script stage */
}
return 0 /** Default to script stage */
}, [
taskObject.currentStage,
taskObject.roles?.data,
taskObject.roles?.total_count,
taskObject.scenes?.data,
taskObject.scenes?.total_count,
taskObject.videos?.data,
taskObject.videos?.total_count,
taskObject.final?.url,
scriptData
])
/** Generate progress segments */
const segments = useMemo(() => {
/** Calculate progress for each stage */
const calculateStageProgress = (stage: number): number => {
const isCompleted = stage < currentStage
const isCurrent = stage === currentStage
const isNext = stage === currentStage + 1
/** Completed stages are always 100% */
if (isCompleted) {
return 100
}
/** Non-current and non-next stages are 0% */
if (!isCurrent && !isNext) {
return 0
}
/** Calculate current stage progress */
switch (stage) {
case 0: /** Script stage */
/** If scriptData exists or moved to next stage, show 100% */
if (scriptData || currentStage > 0) {
return 100
}
return 40
case 1: /** Roles & Scenes stage */
const rolesCount = taskObject.roles?.data?.filter(v => v.status !== 0).length || 0
const rolesTotal = taskObject.roles?.total_count || 0
const scenesCount = taskObject.scenes?.data?.filter(v => v.status !== 0).length || 0
const scenesTotal = taskObject.scenes?.total_count || 0
const totalItems = rolesTotal + scenesTotal
if (totalItems === 0) {
return 40
}
const completedItems = rolesCount + scenesCount
return Math.min(Math.round((completedItems / totalItems) * 100), 95)
case 2: /** Shots/Video stage */
const videosCount = taskObject.videos?.data?.filter(v => v.video_status !== 0).length || 0
const videosTotal = taskObject.videos?.total_count || 0
if (videosTotal === 0) {
return 40
}
return Math.min(Math.round((videosCount / videosTotal) * 100), 95)
case 3: /** Final video stage */
/** If final.url exists, show 100% */
if (taskObject.final?.url) {
return 100
}
/** If this is the next stage (not current), show initial progress */
if (isNext) {
return 0
}
/** Current stage in progress */
return 60
default:
return 0
}
}
return [0, 1, 2, 3].map((stage) => {
const config = stageIconMap[stage]
const isCompleted = stage < currentStage
const isCurrent = stage === currentStage
const segmentProgress = calculateStageProgress(stage)
return {
stage,
config,
isCompleted,
isCurrent,
segmentProgress
}
})
}, [
currentStage,
scriptData,
taskObject.roles?.data,
taskObject.roles?.total_count,
taskObject.scenes?.data,
taskObject.scenes?.total_count,
taskObject.videos?.data,
taskObject.videos?.total_count,
taskObject.final?.url
])
return (
<div
data-alt="h5-progress-bar-container"
className={`w-full py-2 ${className || ''}`}
>
<div data-alt="progress-segments" className="flex items-center gap-1 relative">
{segments.map(({ stage, config, isCompleted, isCurrent, segmentProgress }) => {
const Icon = config.icon
return (
<div
key={stage}
data-alt={`progress-segment-${stage}`}
className="flex-1 relative h-[0.35rem] bg-slate-700/50 rounded-full overflow-visible"
>
{/* Progress fill */}
<motion.div
data-alt="progress-fill"
className="absolute inset-0 rounded-full z-0 backdrop-blur-md"
style={{
background: `${config.color}80`
}}
initial={{ width: '0%' }}
animate={{ width: `${segmentProgress}%` }}
transition={{
duration: 0.6,
ease: 'easeInOut'
}}
/>
{/* Animated icon for current stage only */}
<AnimatePresence>
{isCurrent && !currentLoadingText.includes('Task completed') && segmentProgress < 100 && (
<motion.div
data-alt="stage-icon-moving"
className="absolute -top-1/2 -translate-y-1/2 z-20"
initial={{ left: '0%', x: '-50%', opacity: 0, scale: 0.5 }}
animate={{
left: `${segmentProgress}%`,
x: '-50%',
opacity: 1,
scale: 1,
rotate: [0, 360],
transition: {
left: { duration: 0.6, ease: 'easeInOut' },
x: { duration: 0 },
opacity: { duration: 0.3 },
scale: { duration: 0.3 },
rotate: { duration: 2, repeat: Infinity, ease: 'linear' }
}
}}
exit={{
opacity: 0,
scale: 0.5,
transition: { duration: 0.3 }
}}
>
<div
data-alt="icon-wrapper"
className="w-3 h-3 rounded-full bg-white/90 backdrop-blur-sm flex items-center justify-center shadow-lg"
style={{
boxShadow: `0 0 8px ${config.color}80`,
background: `${config.color}`
}}
>
{/* <Icon className="w-2 h-2" style={{ color: config.color }} /> */}
<Icon className="w-2 h-2" style={{ color: '#fff', fontWeight: 'bold' }} />
</div>
</motion.div>
)}
</AnimatePresence>
{/* Glow effect for current stage */}
{isCurrent && segmentProgress < 100 && (
<motion.div
data-alt="glow-effect"
className="absolute inset-0 rounded-full z-10"
style={{
background: `linear-gradient(to right, transparent, ${config.color}40, transparent)`
}}
animate={{
x: ['-100%', '100%'],
transition: {
duration: 1.5,
repeat: Infinity,
ease: 'linear'
}
}}
/>
)}
</div>
)
})}
</div>
{/* Stage labels (optional) */}
{/* <div data-alt="stage-labels" className="flex items-center justify-between mt-1 px-1">
{segments.map(({ stage, config, isCurrent }) => (
<span
key={stage}
data-alt={`stage-label-${stage}`}
className={`text-[10px] ${isCurrent ? 'text-white font-medium' : 'text-slate-400'}`}
style={isCurrent ? { color: config.color } : {}}
>
{config.label}
</span>
))}
</div> */}
</div>
)
}
export default H5ProgressBar