forked from 77media/video-flow
H5进度条
This commit is contained in:
parent
d92a8f285a
commit
444e8bc58e
@ -5,6 +5,7 @@ import "./style/work-flow.css";
|
|||||||
import { EditModal } from "@/components/ui/edit-modal";
|
import { EditModal } from "@/components/ui/edit-modal";
|
||||||
import { TaskInfo } from "./work-flow/task-info";
|
import { TaskInfo } from "./work-flow/task-info";
|
||||||
import H5TaskInfo from "./work-flow/H5TaskInfo";
|
import H5TaskInfo from "./work-flow/H5TaskInfo";
|
||||||
|
import H5ProgressBar from "./work-flow/H5ProgressBar";
|
||||||
import H5MediaViewer from "./work-flow/H5MediaViewer";
|
import H5MediaViewer from "./work-flow/H5MediaViewer";
|
||||||
import { MediaViewer } from "./work-flow/media-viewer";
|
import { MediaViewer } from "./work-flow/media-viewer";
|
||||||
import { ThumbnailGrid } from "./work-flow/thumbnail-grid";
|
import { ThumbnailGrid } from "./work-flow/thumbnail-grid";
|
||||||
@ -16,12 +17,8 @@ import { SaveEditUseCase } from "@/app/service/usecase/SaveEditUseCase";
|
|||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import SmartChatBox from "@/components/SmartChatBox/SmartChatBox";
|
import SmartChatBox from "@/components/SmartChatBox/SmartChatBox";
|
||||||
import { Drawer, Tooltip, notification } from 'antd';
|
import { Drawer, Tooltip, notification } from 'antd';
|
||||||
import { showEditingNotification } from "@/components/pages/work-flow/editing-notification";
|
|
||||||
// import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
|
|
||||||
import { exportVideoWithRetry } from '@/utils/export-service';
|
import { exportVideoWithRetry } from '@/utils/export-service';
|
||||||
import { getFirstFrame } from '@/utils/tools';
|
|
||||||
import { EditPoint as EditPointType } from './work-flow/video-edit/types';
|
import { EditPoint as EditPointType } from './work-flow/video-edit/types';
|
||||||
import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
|
|
||||||
import { useDeviceType } from '@/hooks/useDeviceType';
|
import { useDeviceType } from '@/hooks/useDeviceType';
|
||||||
import { H5ProgressToastProvider, useH5ProgressToast } from '@/components/ui/h5-progress-toast';
|
import { H5ProgressToastProvider, useH5ProgressToast } from '@/components/ui/h5-progress-toast';
|
||||||
|
|
||||||
@ -141,16 +138,16 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
const title = isMobile ? 'editing...' : 'Performing intelligent editing...';
|
const title = isMobile ? 'editing...' : 'Performing intelligent editing...';
|
||||||
|
|
||||||
// 显示进度提示并启动超时定时器
|
// 显示进度提示并启动超时定时器
|
||||||
emitToastShow({ title: title, progress: 0 });
|
// emitToastShow({ title: title, progress: 0 });
|
||||||
// 启动自动推进到 90% 的进度(8分钟)
|
// 启动自动推进到 90% 的进度(8分钟)
|
||||||
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
||||||
editingProgressStartRef.current = Date.now();
|
editingProgressStartRef.current = Date.now();
|
||||||
const totalMs = 8 * 60 * 1000;
|
const totalMs = 8 * 60 * 1000;
|
||||||
editingProgressIntervalRef.current = setInterval(() => {
|
// editingProgressIntervalRef.current = setInterval(() => {
|
||||||
const elapsed = Date.now() - editingProgressStartRef.current;
|
// const elapsed = Date.now() - editingProgressStartRef.current;
|
||||||
const pct = Math.min(90, Math.max(0, Math.floor((elapsed / totalMs) * 90)));
|
// const pct = Math.min(90, Math.max(0, Math.floor((elapsed / totalMs) * 90)));
|
||||||
emitToastUpdate({ progress: pct });
|
// emitToastUpdate({ progress: pct });
|
||||||
}, 250);
|
// }, 250);
|
||||||
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
|
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
|
||||||
editingTimeoutRef.current = setTimeout(() => {
|
editingTimeoutRef.current = setTimeout(() => {
|
||||||
console.log('❌ Editing timeout - retrying...');
|
console.log('❌ Editing timeout - retrying...');
|
||||||
@ -159,9 +156,9 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
clearInterval(editingProgressIntervalRef.current);
|
clearInterval(editingProgressIntervalRef.current);
|
||||||
editingProgressIntervalRef.current = null;
|
editingProgressIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
emitToastHide();
|
// emitToastHide();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emitToastShow({ title: 'Retry intelligent editing...', progress: 0 });
|
// emitToastShow({ title: 'Retry intelligent editing...', progress: 0 });
|
||||||
// 重试阶段自动推进(5分钟到 90%)
|
// 重试阶段自动推进(5分钟到 90%)
|
||||||
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
||||||
editingProgressStartRef.current = Date.now();
|
editingProgressStartRef.current = Date.now();
|
||||||
@ -169,7 +166,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
editingProgressIntervalRef.current = setInterval(() => {
|
editingProgressIntervalRef.current = setInterval(() => {
|
||||||
const elapsed = Date.now() - editingProgressStartRef.current;
|
const elapsed = Date.now() - editingProgressStartRef.current;
|
||||||
const pct = Math.min(90, Math.max(0, Math.floor((elapsed / retryTotalMs) * 90)));
|
const pct = Math.min(90, Math.max(0, Math.floor((elapsed / retryTotalMs) * 90)));
|
||||||
emitToastUpdate({ progress: pct });
|
// emitToastUpdate({ progress: pct });
|
||||||
}, 250);
|
}, 250);
|
||||||
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
|
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
|
||||||
editingTimeoutRef.current = setTimeout(() => {
|
editingTimeoutRef.current = setTimeout(() => {
|
||||||
@ -183,7 +180,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
clearInterval(editingProgressIntervalRef.current);
|
clearInterval(editingProgressIntervalRef.current);
|
||||||
editingProgressIntervalRef.current = null;
|
editingProgressIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
emitToastHide();
|
// emitToastHide();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}, 5 * 60 * 1000);
|
}, 5 * 60 * 1000);
|
||||||
}, 200);
|
}, 200);
|
||||||
@ -204,7 +201,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
clearInterval(editingProgressIntervalRef.current);
|
clearInterval(editingProgressIntervalRef.current);
|
||||||
editingProgressIntervalRef.current = null;
|
editingProgressIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
emitToastHide();
|
// emitToastHide();
|
||||||
}, [emitToastHide]);
|
}, [emitToastHide]);
|
||||||
|
|
||||||
// 使用自定义 hooks 管理状态
|
// 使用自定义 hooks 管理状态
|
||||||
@ -272,15 +269,15 @@ const WorkFlow = React.memo(function WorkFlow() {
|
|||||||
clearInterval(editingProgressIntervalRef.current);
|
clearInterval(editingProgressIntervalRef.current);
|
||||||
editingProgressIntervalRef.current = null;
|
editingProgressIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
emitToastUpdate({ title: 'Editing successful', progress: 100 });
|
// emitToastUpdate({ title: 'Editing successful', progress: 100 });
|
||||||
console.log('Editing successful');
|
console.log('Editing successful');
|
||||||
localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true');
|
localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true');
|
||||||
setEditingStatus('success');
|
setEditingStatus('success');
|
||||||
setIsEditingInProgress(false);
|
setIsEditingInProgress(false);
|
||||||
isEditingInProgressRef.current = false;
|
isEditingInProgressRef.current = false;
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
emitToastHide();
|
// emitToastHide();
|
||||||
}, 3000);
|
// }, 3000);
|
||||||
}
|
}
|
||||||
}, [taskObject.final, isHandleEdit, episodeId, emitToastHide, emitToastUpdate]);
|
}, [taskObject.final, isHandleEdit, episodeId, emitToastHide, emitToastUpdate]);
|
||||||
|
|
||||||
@ -422,13 +419,22 @@ Please process this video editing request.`;
|
|||||||
<div className="content-vPGYx8">
|
<div className="content-vPGYx8">
|
||||||
<div className="info-UUGkPJ">
|
<div className="info-UUGkPJ">
|
||||||
{isMobile || isTablet ? (
|
{isMobile || isTablet ? (
|
||||||
<H5TaskInfo
|
<>
|
||||||
title={taskObject.title}
|
<H5TaskInfo
|
||||||
current={currentSketchIndex + 1}
|
title={taskObject.title}
|
||||||
taskObject={taskObject}
|
current={currentSketchIndex + 1}
|
||||||
selectedView={selectedView}
|
taskObject={taskObject}
|
||||||
currentLoadingText={currentLoadingText}
|
selectedView={selectedView}
|
||||||
/>
|
currentLoadingText={currentLoadingText}
|
||||||
|
/>
|
||||||
|
{taskObject.currentStage !== 'init' && (
|
||||||
|
<H5ProgressBar
|
||||||
|
taskObject={taskObject}
|
||||||
|
scriptData={scriptData}
|
||||||
|
currentLoadingText={currentLoadingText}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<TaskInfo
|
<TaskInfo
|
||||||
taskObject={taskObject}
|
taskObject={taskObject}
|
||||||
|
|||||||
@ -102,14 +102,14 @@ export function H5MediaViewer({
|
|||||||
/** 视频轮播容器高度:按 aspectRatio 计算(基于视口宽度) */
|
/** 视频轮播容器高度:按 aspectRatio 计算(基于视口宽度) */
|
||||||
const videoWrapperHeight = useMemo(() => {
|
const videoWrapperHeight = useMemo(() => {
|
||||||
const { w, h } = parseAspect(aspectRatio);
|
const { w, h } = parseAspect(aspectRatio);
|
||||||
return `min(calc(100vw * ${h} / ${w}), calc(100vh - 8.5rem))`;
|
return `min(calc(100vw * ${h} / ${w}), calc(100vh - 10.5rem))`;
|
||||||
}, [aspectRatio]);
|
}, [aspectRatio]);
|
||||||
|
|
||||||
/** 图片轮播容器高度:默认 16:9 */
|
/** 图片轮播容器高度:默认 16:9 */
|
||||||
const imageWrapperHeight = useMemo(() => {
|
const imageWrapperHeight = useMemo(() => {
|
||||||
// return 'calc(100vw * 9 / 16)';
|
// return 'calc(100vw * 9 / 16)';
|
||||||
const { w, h } = parseAspect(aspectRatio);
|
const { w, h } = parseAspect(aspectRatio);
|
||||||
return `min(calc(100vw * ${h} / ${w}), calc(100vh - 8.5rem))`;
|
return `min(calc(100vw * ${h} / ${w}), calc(100vh - 10.5rem))`;
|
||||||
}, [aspectRatio]);
|
}, [aspectRatio]);
|
||||||
|
|
||||||
// 计算当前阶段类型
|
// 计算当前阶段类型
|
||||||
@ -354,7 +354,7 @@ export function H5MediaViewer({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-alt="open-catalog-button"
|
data-alt="open-catalog-button"
|
||||||
className="fixed bottom-[6rem] right-4 z-[60] w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 text-white shadow-lg flex items-center justify-center active:scale-95"
|
className="fixed bottom-[5rem] right-4 z-[60] w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 text-white shadow-lg flex items-center justify-center active:scale-95"
|
||||||
aria-label="open-catalog"
|
aria-label="open-catalog"
|
||||||
onClick={() => setIsCatalogOpen(true)}
|
onClick={() => setIsCatalogOpen(true)}
|
||||||
>
|
>
|
||||||
|
|||||||
293
components/pages/work-flow/H5ProgressBar.tsx
Normal file
293
components/pages/work-flow/H5ProgressBar.tsx
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
'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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
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
|
||||||
|
|
||||||
@ -147,57 +147,6 @@ const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -29,19 +29,19 @@ interface TaskInfoProps {
|
|||||||
const stageIconMap = {
|
const stageIconMap = {
|
||||||
0: {
|
0: {
|
||||||
icon: Heart,
|
icon: Heart,
|
||||||
color: '#8b5cf6'
|
color: '#6bf5f9'
|
||||||
},
|
},
|
||||||
1: {
|
1: {
|
||||||
icon: Camera,
|
icon: Camera,
|
||||||
color: '#06b6d4'
|
color: '#88bafb'
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
icon: Film,
|
icon: Film,
|
||||||
color: '#10b981'
|
color: '#a285fd'
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
icon: Scissors,
|
icon: Scissors,
|
||||||
color: '#f59e0b'
|
color: '#c73dff'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
return () => {
|
return () => {
|
||||||
console.log("unmount-useWorkflowData");
|
console.log("unmount-useWorkflowData");
|
||||||
// 组件卸载时隐藏H5进度提示
|
// 组件卸载时隐藏H5进度提示
|
||||||
emitToastHide();
|
// emitToastHide();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
// 查看缓存中 是否已经 加载过 这个项目的 剪辑计划
|
// 查看缓存中 是否已经 加载过 这个项目的 剪辑计划
|
||||||
@ -161,7 +161,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 显示生成剪辑计划进度提示
|
// 显示生成剪辑计划进度提示
|
||||||
emitToastShow({ title: isMobile ? 'Preparing for editing...' : `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, progress: 0 });
|
// !emitToastShow({ title: isMobile ? 'Preparing for editing...' : `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, progress: 0 });
|
||||||
// 平滑推进到 80%,后续阶段接管
|
// 平滑推进到 80%,后续阶段接管
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const duration = 3 * 60 * 1000; // 3分钟推进到 80%
|
const duration = 3 * 60 * 1000; // 3分钟推进到 80%
|
||||||
@ -170,7 +170,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
const elapsed = Date.now() - start;
|
const elapsed = Date.now() - start;
|
||||||
const pct = Math.min(80, Math.floor((elapsed / duration) * 80));
|
const pct = Math.min(80, Math.floor((elapsed / duration) * 80));
|
||||||
emitToastUpdate({ progress: pct });
|
// emitToastUpdate({ progress: pct });
|
||||||
if (pct >= 80) stop();
|
if (pct >= 80) stop();
|
||||||
}, 300);
|
}, 300);
|
||||||
// 先停止轮询
|
// 先停止轮询
|
||||||
@ -200,10 +200,8 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
setNeedStreamData(true);
|
setNeedStreamData(true);
|
||||||
setIsGenerateEditPlan(false);
|
setIsGenerateEditPlan(false);
|
||||||
|
|
||||||
// 显示失败提示,并在稍后隐藏
|
|
||||||
// emitToastShow({ title: isMobile ? 'Editing plan generation failed. Retrying later.' : 'Editing plan generation failed. Retrying later.', progress: 0 });
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
emitToastHide();
|
// emitToastHide();
|
||||||
setIsLoadingGenerateEditPlan(false);
|
setIsLoadingGenerateEditPlan(false);
|
||||||
}, 8000);
|
}, 8000);
|
||||||
stop();
|
stop();
|
||||||
@ -234,14 +232,14 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
setCurrentLoadingText(LOADING_TEXT_MAP.toManyFailed);
|
setCurrentLoadingText(LOADING_TEXT_MAP.toManyFailed);
|
||||||
// 停止轮询
|
// 停止轮询
|
||||||
setNeedStreamData(false);
|
setNeedStreamData(false);
|
||||||
emitToastHide();
|
// emitToastHide();
|
||||||
}
|
}
|
||||||
if (editingStatus === 'error') {
|
if (editingStatus === 'error') {
|
||||||
window.msg.error('Editing failed, Please click the scissors button to go to the intelligent editing platform.', 8000);
|
window.msg.error('Editing failed, Please click the scissors button to go to the intelligent editing platform.', 8000);
|
||||||
setCurrentLoadingText(LOADING_TEXT_MAP.editingError);
|
setCurrentLoadingText(LOADING_TEXT_MAP.editingError);
|
||||||
// 停止轮询
|
// 停止轮询
|
||||||
setNeedStreamData(false);
|
setNeedStreamData(false);
|
||||||
emitToastHide();
|
// emitToastHide();
|
||||||
}
|
}
|
||||||
}, [isShowError, editingStatus]);
|
}, [isShowError, editingStatus]);
|
||||||
|
|
||||||
@ -422,7 +420,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
if (analyze_video_total_count > 0 && !isAnalyzing && analyze_video_completed_count !== analyze_video_total_count) {
|
if (analyze_video_total_count > 0 && !isAnalyzing && analyze_video_completed_count !== analyze_video_total_count) {
|
||||||
setIsAnalyzing(true);
|
setIsAnalyzing(true);
|
||||||
// 显示准备剪辑计划的提示
|
// 显示准备剪辑计划的提示
|
||||||
emitToastShow({ title: isMobile ? 'Preparing for editing...' : 'Preparing intelligent editing plan...', progress: 0 });
|
// emitToastShow({ title: isMobile ? 'Preparing for editing...' : 'Preparing intelligent editing plan...', progress: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) {
|
if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) {
|
||||||
@ -435,7 +433,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
|||||||
} else {
|
} else {
|
||||||
setIsShowError(true);
|
setIsShowError(true);
|
||||||
setIsAnalyzing(false);
|
setIsAnalyzing(false);
|
||||||
emitToastHide();
|
// emitToastHide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user