321 lines
12 KiB
TypeScript
321 lines
12 KiB
TypeScript
import React, { useEffect, useState, useCallback } from 'react';
|
||
import {
|
||
Brain, Clock, CheckCircle, XCircle, AlertCircle, Pause, Play, Square,
|
||
TrendingUp, BarChart3, Eye, Star, Target
|
||
} from 'lucide-react';
|
||
import { useVideoClassificationStore } from '../store/videoClassificationStore';
|
||
import type { QueueStats, TaskProgress, ClassificationStats } from '../store/videoClassificationStore';
|
||
|
||
interface VideoClassificationProgressProps {
|
||
materialId?: string;
|
||
projectId?: string;
|
||
autoRefresh?: boolean;
|
||
refreshInterval?: number;
|
||
}
|
||
|
||
/**
|
||
* AI视频分类进度显示组件
|
||
* 遵循前端开发规范的UI设计,提供优美的动画效果和用户体验
|
||
*/
|
||
export const VideoClassificationProgress: React.FC<VideoClassificationProgressProps> = ({
|
||
materialId,
|
||
projectId,
|
||
autoRefresh = true,
|
||
refreshInterval = 3000,
|
||
}) => {
|
||
const {
|
||
queueStats,
|
||
taskProgress,
|
||
refreshQueueStatus,
|
||
refreshTaskProgress,
|
||
getProjectQueueStatus,
|
||
getProjectTaskProgress,
|
||
getClassificationStats,
|
||
error,
|
||
clearError,
|
||
} = useVideoClassificationStore();
|
||
|
||
// Type assertions to satisfy TypeScript
|
||
const typedQueueStats: QueueStats | null = queueStats;
|
||
const typedTaskProgress: Record<string, TaskProgress> = taskProgress;
|
||
|
||
const [stats, setStats] = useState<ClassificationStats | null>(null);
|
||
const [isExpanded, setIsExpanded] = useState(false);
|
||
|
||
// 刷新队列状态的方法
|
||
const refreshQueueStats = useCallback(async () => {
|
||
if (projectId) {
|
||
return await getProjectQueueStatus(projectId);
|
||
} else {
|
||
return await refreshQueueStatus();
|
||
}
|
||
}, [projectId, getProjectQueueStatus, refreshQueueStatus]);
|
||
|
||
// 刷新任务进度的方法
|
||
const refreshProgress = useCallback(async () => {
|
||
if (projectId) {
|
||
await getProjectTaskProgress(projectId);
|
||
} else {
|
||
await refreshTaskProgress();
|
||
}
|
||
}, [projectId, getProjectTaskProgress, refreshTaskProgress]);
|
||
|
||
// 自动刷新
|
||
useEffect(() => {
|
||
if (!autoRefresh) return;
|
||
|
||
const interval = setInterval(async () => {
|
||
await refreshQueueStats();
|
||
await refreshProgress();
|
||
|
||
if (projectId) {
|
||
try {
|
||
const classificationStats = await getClassificationStats(projectId);
|
||
setStats(classificationStats);
|
||
} catch (error) {
|
||
console.error('获取分类统计失败:', error);
|
||
}
|
||
}
|
||
}, refreshInterval);
|
||
|
||
return () => clearInterval(interval);
|
||
}, [autoRefresh, refreshInterval, projectId, refreshQueueStats, refreshProgress, getClassificationStats]);
|
||
|
||
// 初始加载
|
||
useEffect(() => {
|
||
refreshQueueStats();
|
||
refreshProgress();
|
||
|
||
if (projectId) {
|
||
getClassificationStats(projectId).then(setStats).catch(console.error);
|
||
}
|
||
}, [projectId, refreshQueueStats, refreshProgress, getClassificationStats]);
|
||
|
||
// 获取状态颜色和图标
|
||
const getStatusInfo = (status: string) => {
|
||
switch (status) {
|
||
case 'Running':
|
||
return { color: 'text-green-600 bg-green-50', icon: Play, text: '运行中' };
|
||
case 'Paused':
|
||
return { color: 'text-yellow-600 bg-yellow-50', icon: Pause, text: '已暂停' };
|
||
case 'Stopped':
|
||
return { color: 'text-gray-600 bg-gray-50', icon: Square, text: '已停止' };
|
||
default:
|
||
return { color: 'text-gray-600 bg-gray-50', icon: Square, text: '未知' };
|
||
}
|
||
};
|
||
|
||
// 获取任务状态信息
|
||
const getTaskStatusInfo = (status: string) => {
|
||
switch (status) {
|
||
case 'Pending':
|
||
return { color: 'text-blue-600', icon: Clock, text: '等待中' };
|
||
case 'Uploading':
|
||
return { color: 'text-purple-600', icon: TrendingUp, text: '上传中' };
|
||
case 'Analyzing':
|
||
return { color: 'text-indigo-600', icon: Brain, text: '分析中' };
|
||
case 'Completed':
|
||
return { color: 'text-green-600', icon: CheckCircle, text: '已完成' };
|
||
case 'Failed':
|
||
return { color: 'text-red-600', icon: XCircle, text: '失败' };
|
||
case 'Cancelled':
|
||
return { color: 'text-gray-600', icon: AlertCircle, text: '已取消' };
|
||
default:
|
||
return { color: 'text-gray-600', icon: Clock, text: '未知' };
|
||
}
|
||
};
|
||
|
||
|
||
// 计算进度百分比
|
||
const getOverallProgress = () => {
|
||
if (!typedQueueStats || typedQueueStats.total_tasks === 0) return 0;
|
||
return Math.round(((typedQueueStats.completed_tasks + typedQueueStats.failed_tasks) / typedQueueStats.total_tasks) * 100);
|
||
};
|
||
|
||
// 过滤相关任务
|
||
const relevantTasks = materialId
|
||
? Object.values(typedTaskProgress).filter(task => task.task_id.includes(materialId))
|
||
: Object.values(typedTaskProgress);
|
||
|
||
if (!typedQueueStats && !relevantTasks.length && !stats) {
|
||
return null;
|
||
}
|
||
|
||
const statusInfo = typedQueueStats ? getStatusInfo(typedQueueStats.status) : null;
|
||
const overallProgress = getOverallProgress()
|
||
|
||
return (
|
||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||
{/* 错误提示 */}
|
||
{error && (
|
||
<div className="bg-red-50 border-l-4 border-red-400 p-3">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center">
|
||
<XCircle className="w-4 h-4 text-red-400 mr-2" />
|
||
<span className="text-sm text-red-700">{error}</span>
|
||
</div>
|
||
<button
|
||
onClick={clearError}
|
||
className="text-red-400 hover:text-red-600"
|
||
>
|
||
<XCircle className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 主要状态显示 */}
|
||
<div className="p-4">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="flex items-center space-x-3">
|
||
<div className="flex items-center space-x-2">
|
||
<Brain className="w-5 h-5 text-purple-600" />
|
||
<h3 className="text-lg font-medium text-gray-900">AI视频分类</h3>
|
||
</div>
|
||
|
||
{statusInfo && (
|
||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusInfo.color}`}>
|
||
<statusInfo.icon className="w-3 h-3 mr-1" />
|
||
{statusInfo.text}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 队列控制按钮 */}
|
||
{typedQueueStats && (
|
||
<div className="flex items-center space-x-2">
|
||
<div className="text-xs text-gray-500 ml-2">
|
||
状态: {typedQueueStats.status}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 整体进度条 */}
|
||
{typedQueueStats && typedQueueStats.total_tasks > 0 && (
|
||
<div className="mb-4">
|
||
<div className="flex items-center justify-between text-sm text-gray-600 mb-2">
|
||
<span>整体进度</span>
|
||
<span>{overallProgress}%</span>
|
||
</div>
|
||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||
<div
|
||
className="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full transition-all duration-300 ease-out"
|
||
style={{ width: `${overallProgress}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 统计信息网格 */}
|
||
{typedQueueStats && (
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||
<div className="text-center p-3 bg-blue-50 rounded-lg">
|
||
<div className="text-2xl font-bold text-blue-600">{typedQueueStats.pending_tasks}</div>
|
||
<div className="text-xs text-blue-600">等待中</div>
|
||
</div>
|
||
<div className="text-center p-3 bg-purple-50 rounded-lg">
|
||
<div className="text-2xl font-bold text-purple-600">{typedQueueStats.processing_tasks}</div>
|
||
<div className="text-xs text-purple-600">处理中</div>
|
||
</div>
|
||
<div className="text-center p-3 bg-green-50 rounded-lg">
|
||
<div className="text-2xl font-bold text-green-600">{typedQueueStats.completed_tasks}</div>
|
||
<div className="text-xs text-green-600">已完成</div>
|
||
</div>
|
||
<div className="text-center p-3 bg-red-50 rounded-lg">
|
||
<div className="text-2xl font-bold text-red-600">{typedQueueStats.failed_tasks}</div>
|
||
<div className="text-xs text-red-600">失败</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 处理速率 */}
|
||
{typedQueueStats && typedQueueStats.processing_rate > 0 && (
|
||
<div className="flex items-center justify-center text-sm text-gray-600 mb-4">
|
||
<TrendingUp className="w-4 h-4 mr-1" />
|
||
<span>处理速率: {typedQueueStats.processing_rate.toFixed(1)} 任务/分钟</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 展开/收起详细信息 */}
|
||
{relevantTasks.length > 0 && (
|
||
<button
|
||
onClick={() => setIsExpanded(!isExpanded)}
|
||
className="w-full text-sm text-blue-600 hover:text-blue-800 py-2 border-t border-gray-100"
|
||
>
|
||
{isExpanded ? '收起详细信息' : `查看详细信息 (${relevantTasks.length} 个任务)`}
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* 详细任务列表 */}
|
||
{isExpanded && relevantTasks.length > 0 && (
|
||
<div className="border-t border-gray-100 bg-gray-50">
|
||
<div className="p-4 space-y-3 max-h-64 overflow-y-auto">
|
||
{relevantTasks.map((task) => {
|
||
const taskStatusInfo = getTaskStatusInfo(task.status);
|
||
return (
|
||
<div key={task.task_id} className="bg-white rounded-lg p-3 shadow-sm">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center space-x-2">
|
||
<taskStatusInfo.icon className={`w-4 h-4 ${taskStatusInfo.color}`} />
|
||
<span className="text-sm font-medium text-gray-900">
|
||
任务 #{task.task_id.slice(-8)}
|
||
</span>
|
||
</div>
|
||
<span className={`text-xs ${taskStatusInfo.color}`}>
|
||
{taskStatusInfo.text}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="text-xs text-gray-600 mb-2">{task.current_step}</div>
|
||
|
||
{task.progress_percentage > 0 && (
|
||
<div className="w-full bg-gray-200 rounded-full h-1.5">
|
||
<div
|
||
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
|
||
style={{ width: `${task.progress_percentage}%` }}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{task.error_message && (
|
||
<div className="mt-2 text-xs text-red-600 bg-red-50 p-2 rounded">
|
||
{task.error_message}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 分类统计 */}
|
||
{stats && (
|
||
<div className="border-t border-gray-100 bg-gradient-to-r from-blue-50 to-purple-50 p-4">
|
||
<div className="flex items-center space-x-2 mb-3">
|
||
<BarChart3 className="w-4 h-4 text-blue-600" />
|
||
<span className="text-sm font-medium text-gray-900">分类统计</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 text-xs">
|
||
<div className="flex items-center space-x-2">
|
||
<Eye className="w-3 h-3 text-blue-500" />
|
||
<span className="text-gray-600">总分类: {stats.total_classifications}</span>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<Target className="w-3 h-3 text-green-500" />
|
||
<span className="text-gray-600">平均置信度: {(stats.average_confidence * 100).toFixed(1)}%</span>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<Star className="w-3 h-3 text-yellow-500" />
|
||
<span className="text-gray-600">平均质量: {(stats.average_quality_score * 100).toFixed(1)}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|