Files
mixvideo-v2/apps/desktop/src/components/template/BatchImportProgressModal.tsx
imeepos 939efd70d4 feat: 完善模板导入功能
新增功能:
- 添加详细的模板导入日志系统
- 实现全局进度存储机制
- 完善模板状态管理

 修复问题:
- 修复进度监控无限轮询问题
- 修复模板列表状态显示不正确问题
- 修复所有unwrap()导致的panic错误
- 修复外键约束失败问题

 改进:
- 优化素材上传逻辑,只上传视频/音频/图片
- 上传失败时自动跳过而不是中断导入
- 缺失文件时继续导入而不是失败
- 改进错误处理机制
2025-07-14 21:34:07 +08:00

283 lines
10 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.
import React, { useEffect, useState } from 'react';
import { X, CheckCircle, Clock, AlertCircle, Pause, Square } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
interface BatchImportProgressModalProps {
onClose: () => void;
onComplete?: () => void;
}
interface BatchProgress {
total_items: number;
completed_items: number;
failed_items: number;
current_item?: string;
overall_progress: number;
is_running: boolean;
}
export const BatchImportProgressModal: React.FC<BatchImportProgressModalProps> = ({
onClose,
onComplete,
}) => {
const [progress, setProgress] = useState<BatchProgress | null>(null);
const [isCompleted, setIsCompleted] = useState(false);
const [queueStatus, setQueueStatus] = useState<[number, number, number, number] | null>(null);
useEffect(() => {
const fetchProgress = async () => {
try {
const progressData = await invoke<BatchProgress>('get_batch_import_progress');
setProgress(progressData);
const status = await invoke<[number, number, number, number]>('get_queue_status');
setQueueStatus(status);
if (progressData && !progressData.is_running &&
progressData.completed_items + progressData.failed_items >= progressData.total_items) {
setIsCompleted(true);
if (onComplete) {
setTimeout(() => {
onComplete();
}, 3000); // 3秒后自动关闭
}
}
} catch (error) {
console.error('获取批量导入进度失败:', error);
}
};
// 立即获取一次进度
fetchProgress();
// 定期轮询进度
const interval = setInterval(() => {
if (!isCompleted) {
fetchProgress();
}
}, 1000);
return () => clearInterval(interval);
}, [isCompleted, onComplete]);
const handleStopImport = async () => {
try {
await invoke('stop_batch_import');
setProgress(prev => prev ? { ...prev, is_running: false } : null);
} catch (error) {
console.error('停止批量导入失败:', error);
}
};
const handleClearQueue = async () => {
try {
await invoke('clear_import_queue');
setProgress(null);
setQueueStatus(null);
onClose();
} catch (error) {
console.error('清空队列失败:', error);
}
};
if (!progress && !queueStatus) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4 p-6">
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">...</span>
</div>
</div>
</div>
);
}
const totalItems = progress?.total_items || 0;
const completedItems = progress?.completed_items || 0;
const failedItems = progress?.failed_items || 0;
const overallProgress = progress?.overall_progress || 0;
const isRunning = progress?.is_running || false;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4">
{/* 模态框头部 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900"></h2>
<button
onClick={onClose}
className="p-1 rounded-full hover:bg-gray-100 transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
{/* 模态框内容 */}
<div className="p-6">
{/* 状态指示器 */}
<div className="flex items-center mb-6">
{isRunning ? (
<Clock className="w-6 h-6 text-blue-500 animate-spin" />
) : isCompleted ? (
<CheckCircle className="w-6 h-6 text-green-500" />
) : (
<Pause className="w-6 h-6 text-yellow-500" />
)}
<div className="ml-3">
<div className={`text-lg font-medium ${
isRunning ? 'text-blue-600' :
isCompleted ? 'text-green-600' :
'text-yellow-600'
}`}>
{isRunning ? '正在批量导入' :
isCompleted ? '批量导入完成' :
'批量导入已暂停'}
</div>
{progress?.current_item && (
<div className="text-sm text-gray-600">
: {progress.current_item.split(/[/\\]/).pop()}
</div>
)}
</div>
</div>
{/* 进度条 */}
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700"></span>
<span className="text-sm text-gray-600">
{Math.round(overallProgress)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all duration-300 ${
isCompleted ? 'bg-green-500' : 'bg-blue-500'
}`}
style={{ width: `${Math.min(100, Math.max(0, overallProgress))}%` }}
/>
</div>
</div>
{/* 统计信息 */}
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="text-center">
<div className="text-lg font-semibold text-gray-900">
{totalItems}
</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center">
<div className="text-lg font-semibold text-blue-600">
{totalItems - completedItems - failedItems}
</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center">
<div className="text-lg font-semibold text-green-600">
{completedItems}
</div>
<div className="text-xs text-gray-500"></div>
</div>
<div className="text-center">
<div className="text-lg font-semibold text-red-600">
{failedItems}
</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
{/* 队列状态 */}
{queueStatus && (
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<div className="text-sm font-medium text-gray-700 mb-2"></div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">:</span>
<span className="font-medium">{queueStatus[0]}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">:</span>
<span className="font-medium text-yellow-600">{queueStatus[1]}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">:</span>
<span className="font-medium text-blue-600">{queueStatus[2]}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">:</span>
<span className="font-medium text-green-600">{queueStatus[3]}</span>
</div>
</div>
</div>
)}
{/* 完成信息 */}
{isCompleted && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center">
<CheckCircle className="w-5 h-5 text-green-500 mr-2" />
<div className="text-sm text-green-800">
<div className="font-medium"></div>
<div>
{completedItems}
{failedItems > 0 && `${failedItems} 个失败`}
</div>
</div>
</div>
</div>
)}
{/* 操作提示 */}
{isRunning && (
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start">
<AlertCircle className="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
<div className="text-sm text-blue-800">
<div className="font-medium mb-1"></div>
<div>
使
</div>
</div>
</div>
</div>
)}
</div>
{/* 模态框底部 */}
<div className="flex items-center justify-between p-6 border-t border-gray-200">
<div className="flex items-center space-x-2">
{isRunning && (
<button
onClick={handleStopImport}
className="flex items-center px-3 py-2 text-sm font-medium text-red-600 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors"
>
<Square className="w-4 h-4 mr-1" />
</button>
)}
{!isRunning && totalItems > 0 && (
<button
onClick={handleClearQueue}
className="flex items-center px-3 py-2 text-sm font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors"
>
</button>
)}
</div>
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
>
{isRunning ? '后台运行' : '关闭'}
</button>
</div>
</div>
</div>
);
};