import React, { useState, useEffect } from 'react'; import { X, Upload, FileText, AlertCircle, CheckCircle, Loader2 } from 'lucide-react'; import { listen } from '@tauri-apps/api/event'; import { useMaterialStore } from '../store/materialStore'; import { CreateMaterialRequest, MaterialImportResult } from '../types/material'; import { Model } from '../types/model'; import { MaterialModelBindingService } from '../services/materialModelBindingService'; import { CustomSelect } from './CustomSelect'; interface MaterialImportDialogProps { isOpen: boolean; projectId: string; onClose: () => void; onImportComplete: (result: MaterialImportResult) => void; } /** * 素材导入对话框组件 * 遵循 Tauri 开发规范的组件设计模式 */ export const MaterialImportDialog: React.FC = ({ isOpen, projectId, onClose, onImportComplete }) => { const { isImporting, importProgress, error, importMaterialsAsync, updateImportProgress, selectMaterialFiles, selectMaterialFolders, scanFolderMaterials, validateMaterialFiles, checkFFmpegAvailable, getSupportedExtensions, clearError } = useMaterialStore(); const [selectedFiles, setSelectedFiles] = useState([]); const [autoProcess, setAutoProcess] = useState(true); const [maxSegmentDuration, setMaxSegmentDuration] = useState(300); // 5分钟 const [ffmpegAvailable, setFFmpegAvailable] = useState(false); const [step, setStep] = useState<'select' | 'batch' | 'configure' | 'importing' | 'complete'>('select'); const [importMode, setImportMode] = useState<'files' | 'folders'>('files'); const [selectedFolders, setSelectedFolders] = useState([]); const [recursiveScan, setRecursiveScan] = useState(true); const [selectedFileTypes, setSelectedFileTypes] = useState([]); // 模特绑定相关状态 const [selectedModelId, setSelectedModelId] = useState(''); const [availableModels, setAvailableModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); // 检查 FFmpeg 可用性 useEffect(() => { if (isOpen) { checkFFmpegAvailable().then(setFFmpegAvailable); } }, [isOpen, checkFFmpegAvailable]); // 重置状态 useEffect(() => { if (isOpen) { setSelectedFiles([]); setStep('select'); setSelectedModelId(''); clearError(); } }, [isOpen, clearError]); // 加载可用模特 useEffect(() => { if (isOpen) { loadAvailableModels(); } }, [isOpen]); const loadAvailableModels = async () => { setLoadingModels(true); try { const models = await MaterialModelBindingService.getAllModels(); setAvailableModels(models.filter(model => model.is_active)); } catch (error) { console.error('加载模特列表失败:', error); } finally { setLoadingModels(false); } }; // 设置事件监听器用于接收导入进度更新 useEffect(() => { if (!isOpen) return; let unlistenProgress: (() => void) | null = null; let unlistenCompleted: (() => void) | null = null; let unlistenFailed: (() => void) | null = null; const setupEventListeners = async () => { try { // 监听导入进度事件 unlistenProgress = await listen('material_import_progress', (event: any) => { console.log('收到进度事件:', event.payload); const progressData = event.payload; updateImportProgress({ current_file: progressData.current_file, processed_count: progressData.processed_count, total_count: progressData.total_count, current_status: progressData.current_status, progress_percentage: progressData.progress_percentage, }); }); // 监听导入完成事件 unlistenCompleted = await listen('material_import_completed', (event: any) => { console.log('收到完成事件:', event.payload); const result = event.payload; setStep('complete'); onImportComplete(result); }); // 监听导入失败事件 unlistenFailed = await listen('material_import_failed', (event: any) => { console.log('收到失败事件:', event.payload); const errorMessage = event.payload; console.error('导入失败:', errorMessage); setStep('configure'); // 返回配置步骤 }); } catch (error) { console.error('设置事件监听器失败:', error); } }; setupEventListeners(); // 清理函数 return () => { if (unlistenProgress) unlistenProgress(); if (unlistenCompleted) unlistenCompleted(); if (unlistenFailed) unlistenFailed(); }; }, [isOpen, updateImportProgress, onImportComplete]); // 选择文件 const handleSelectFiles = async () => { try { const filePaths = await selectMaterialFiles(); if (filePaths.length > 0) { const validFiles = await validateMaterialFiles(filePaths); setSelectedFiles(validFiles); if (validFiles.length > 0) { setImportMode('files'); setStep('configure'); } } } catch (error) { console.error('选择文件失败:', error); } }; // 选择文件夹 const handleSelectFolders = async () => { try { const folderPaths = await selectMaterialFolders(); if (folderPaths.length > 0) { setSelectedFolders(folderPaths); setImportMode('folders'); setStep('batch'); } } catch (error) { console.error('选择文件夹失败:', error); } }; // 扫描文件夹并进入配置步骤 const handleScanFolders = async () => { try { const supportedTypes = await getSupportedExtensions(); const fileTypes = selectedFileTypes.length > 0 ? selectedFileTypes : supportedTypes; const scannedFiles = await scanFolderMaterials(selectedFolders, recursiveScan, fileTypes); if (scannedFiles.length > 0) { const validFiles = await validateMaterialFiles(scannedFiles); setSelectedFiles(validFiles); if (validFiles.length > 0) { setStep('configure'); } } else { console.warn('未找到任何支持的文件'); } } catch (error) { console.error('扫描文件夹失败:', error); } }; // 开始导入(使用异步版本) const handleStartImport = async () => { if (selectedFiles.length === 0) return; setStep('importing'); try { const request: CreateMaterialRequest = { project_id: projectId, file_paths: selectedFiles, auto_process: autoProcess, max_segment_duration: maxSegmentDuration, model_id: selectedModelId || undefined, }; console.log('开始异步导入:', request); // 使用异步导入,进度更新和完成状态完全通过事件监听器处理 await importMaterialsAsync(request); // 注意:不在这里设置完成状态,完全依赖事件监听器 console.log('异步导入命令执行完成,等待事件通知'); } catch (error) { console.error('导入失败:', error); setStep('configure'); // 返回配置步骤 } }; // 格式化文件大小 // const formatFileSize = (bytes: number) => { // const sizes = ['B', 'KB', 'MB', 'GB']; // if (bytes === 0) return '0 B'; // const i = Math.floor(Math.log(bytes) / Math.log(1024)); // return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; // }; // 获取文件名 const getFileName = (path: string) => { return path.split(/[/\\]/).pop() || path; }; if (!isOpen) return null; return (
{/* 头部 */}

导入素材

{/* 内容 */}
{/* FFmpeg 状态检查 */} {!ffmpegAvailable && (
FFmpeg 不可用,视频处理功能将受限。请确保已安装 FFmpeg。
)} {/* 错误信息 */} {error && (
{error}
)} {/* 步骤 1: 选择导入方式 */} {step === 'select' && (

选择导入方式

支持视频、音频、图片等多种格式

)} {/* 步骤 2: 批量导入配置 */} {step === 'batch' && (

配置批量导入

已选择 {selectedFolders.length} 个文件夹

选择的文件夹:

{selectedFolders.map((folder, index) => (
{folder}
))}
setRecursiveScan(e.target.checked)} className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" />
setSelectedFileTypes( e.target.value.split(',').map(t => t.trim()).filter(t => t) )} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
)} {/* 步骤 3: 配置导入选项 */} {step === 'configure' && (

已选择的文件

{selectedFiles.map((file, index) => (
{getFileName(file)}
))}

导入配置

setAutoProcess(e.target.checked)} className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" />
{autoProcess && (
setMaxSegmentDuration(Number(e.target.value))} min="60" max="3600" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" />

超过此时长的视频将被自动切分

)} {/* 模特绑定选择 */}
({ value: model.id, label: model.stage_name || model.name, })), ]} placeholder={loadingModels ? '加载中...' : '选择模特'} disabled={loadingModels} className="w-full" />

选择模特后,导入的素材将自动绑定到该模特

)} {/* 步骤 3: 导入进度 */} {step === 'importing' && (
{/* 主要加载动画 */}

正在导入素材

{importProgress ? (
{/* 状态描述 */}
{importProgress.current_status}
{/* 进度条 */}
进度 {Math.round((importProgress.processed_count / importProgress.total_count) * 100)}%
{importProgress.processed_count} / {importProgress.total_count} 文件 剩余 {importProgress.total_count - importProgress.processed_count} 个
{/* 当前处理文件 */} {importProgress.current_file && (

正在处理

{getFileName(importProgress.current_file)}

)} {/* 处理阶段指示器 */}
0 ? 'bg-green-100 text-green-600' : 'bg-gray-100 text-gray-400' }`}>
文件扫描
0 ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-400' }`}> 0 ? 'animate-spin' : ''}`} />
数据处理
导入完成
{/* 错误信息 */} {importProgress.errors.length > 0 && (

处理错误 ({importProgress.errors.length})

{importProgress.errors.map((error, index) => (
{error}
))}
)}
) : ( /* 初始化状态 */
正在初始化导入流程...
)}
)} {/* 步骤 4: 完成 */} {step === 'complete' && (

导入完成

素材已成功导入到项目中

)}
{/* 底部按钮 */}
{step === 'batch' && ( <> )} {step === 'configure' && ( <> )} {(step === 'select' || step === 'complete') && ( )}
); };