665 lines
27 KiB
TypeScript
665 lines
27 KiB
TypeScript
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<MaterialImportDialogProps> = ({
|
||
isOpen,
|
||
projectId,
|
||
onClose,
|
||
onImportComplete
|
||
}) => {
|
||
const {
|
||
isImporting,
|
||
importProgress,
|
||
error,
|
||
importMaterialsAsync,
|
||
updateImportProgress,
|
||
selectMaterialFiles,
|
||
selectMaterialFolders,
|
||
scanFolderMaterials,
|
||
validateMaterialFiles,
|
||
checkFFmpegAvailable,
|
||
getSupportedExtensions,
|
||
clearError
|
||
} = useMaterialStore();
|
||
|
||
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||
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<string[]>([]);
|
||
const [recursiveScan, setRecursiveScan] = useState(true);
|
||
const [selectedFileTypes, setSelectedFileTypes] = useState<string[]>([]);
|
||
|
||
// 模特绑定相关状态
|
||
const [selectedModelId, setSelectedModelId] = useState<string>('');
|
||
const [availableModels, setAvailableModels] = useState<Model[]>([]);
|
||
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 (
|
||
<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 max-w-2xl w-full mx-4 max-h-[90vh] overflow-hidden">
|
||
{/* 头部 */}
|
||
<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="text-gray-400 hover:text-gray-600 transition-colors"
|
||
>
|
||
<X className="w-6 h-6" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* 内容 */}
|
||
<div className="p-6 overflow-y-auto max-h-[calc(90vh-140px)]">
|
||
{/* FFmpeg 状态检查 */}
|
||
{!ffmpegAvailable && (
|
||
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||
<div className="flex items-center">
|
||
<AlertCircle className="w-5 h-5 text-yellow-600 mr-2" />
|
||
<span className="text-yellow-800">
|
||
FFmpeg 不可用,视频处理功能将受限。请确保已安装 FFmpeg。
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 错误信息 */}
|
||
{error && (
|
||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||
<div className="flex items-center">
|
||
<AlertCircle className="w-5 h-5 text-red-600 mr-2" />
|
||
<span className="text-red-800">{error}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 步骤 1: 选择导入方式 */}
|
||
{step === 'select' && (
|
||
<div className="space-y-6">
|
||
<div className="text-center">
|
||
<Upload className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">选择导入方式</h3>
|
||
<p className="text-gray-600 mb-6">
|
||
支持视频、音频、图片等多种格式
|
||
</p>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<button
|
||
onClick={handleSelectFiles}
|
||
className="p-6 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors group"
|
||
>
|
||
<FileText className="w-12 h-12 text-gray-400 group-hover:text-blue-500 mx-auto mb-3" />
|
||
<h4 className="text-lg font-medium text-gray-900 mb-2">选择文件</h4>
|
||
<p className="text-sm text-gray-600">
|
||
手动选择单个或多个文件
|
||
</p>
|
||
</button>
|
||
|
||
<button
|
||
onClick={handleSelectFolders}
|
||
className="p-6 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors group"
|
||
>
|
||
<Upload className="w-12 h-12 text-gray-400 group-hover:text-blue-500 mx-auto mb-3" />
|
||
<h4 className="text-lg font-medium text-gray-900 mb-2">选择文件夹</h4>
|
||
<p className="text-sm text-gray-600">
|
||
批量导入整个文件夹
|
||
</p>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 步骤 2: 批量导入配置 */}
|
||
{step === 'batch' && (
|
||
<div className="space-y-6">
|
||
<div className="text-center">
|
||
<Upload className="w-16 h-16 text-blue-600 mx-auto mb-4" />
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">配置批量导入</h3>
|
||
<p className="text-gray-600 mb-6">
|
||
已选择 {selectedFolders.length} 个文件夹
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div className="bg-gray-50 p-4 rounded-lg">
|
||
<h4 className="text-sm font-medium text-gray-900 mb-2">选择的文件夹:</h4>
|
||
<div className="space-y-2">
|
||
{selectedFolders.map((folder, index) => (
|
||
<div key={index} className="text-sm text-gray-600 bg-white p-2 rounded border">
|
||
{folder}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
id="recursive"
|
||
checked={recursiveScan}
|
||
onChange={(e) => setRecursiveScan(e.target.checked)}
|
||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||
/>
|
||
<label htmlFor="recursive" className="ml-2 text-sm text-gray-700">
|
||
递归扫描子文件夹
|
||
</label>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
文件类型过滤 (留空表示所有支持的类型)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
placeholder="例如: mp4,avi,mov (用逗号分隔)"
|
||
value={selectedFileTypes.join(',')}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 步骤 3: 配置导入选项 */}
|
||
{step === 'configure' && (
|
||
<div className="space-y-6">
|
||
<div>
|
||
<h3 className="text-lg font-medium text-gray-900 mb-4">已选择的文件</h3>
|
||
<div className="max-h-40 overflow-y-auto border border-gray-200 rounded-lg">
|
||
{selectedFiles.map((file, index) => (
|
||
<div key={index} className="flex items-center p-3 border-b border-gray-100 last:border-b-0">
|
||
<FileText className="w-5 h-5 text-gray-400 mr-3" />
|
||
<span className="text-sm text-gray-900 truncate">{getFileName(file)}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<h3 className="text-lg font-medium text-gray-900">导入配置</h3>
|
||
|
||
<div className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
id="autoProcess"
|
||
checked={autoProcess}
|
||
onChange={(e) => setAutoProcess(e.target.checked)}
|
||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||
/>
|
||
<label htmlFor="autoProcess" className="ml-2 text-sm text-gray-900">
|
||
自动处理素材(提取元数据、场景检测等)
|
||
</label>
|
||
</div>
|
||
|
||
{autoProcess && (
|
||
<div>
|
||
<label htmlFor="maxDuration" className="block text-sm font-medium text-gray-700 mb-2">
|
||
最大片段时长(秒)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
id="maxDuration"
|
||
value={maxSegmentDuration}
|
||
onChange={(e) => 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"
|
||
/>
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
超过此时长的视频将被自动切分
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 模特绑定选择 */}
|
||
<div>
|
||
<label htmlFor="modelSelect" className="block text-sm font-medium text-gray-700 mb-2">
|
||
绑定模特(可选)
|
||
</label>
|
||
<CustomSelect
|
||
value={selectedModelId}
|
||
onChange={setSelectedModelId}
|
||
options={[
|
||
{ value: '', label: '不绑定模特' },
|
||
...availableModels.map(model => ({
|
||
value: model.id,
|
||
label: model.stage_name || model.name,
|
||
})),
|
||
]}
|
||
placeholder={loadingModels ? '加载中...' : '选择模特'}
|
||
disabled={loadingModels}
|
||
className="w-full"
|
||
/>
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
选择模特后,导入的素材将自动绑定到该模特
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 步骤 3: 导入进度 */}
|
||
{step === 'importing' && (
|
||
<div className="space-y-6">
|
||
<div className="text-center">
|
||
{/* 主要加载动画 */}
|
||
<div className="relative">
|
||
<div className="w-20 h-20 mx-auto mb-6">
|
||
<div className="absolute inset-0 border-4 border-blue-200 rounded-full"></div>
|
||
<div className="absolute inset-2 flex items-center justify-center">
|
||
<Upload className="w-8 h-8 text-blue-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 className="text-xl font-semibold text-gray-900 mb-2">正在导入素材</h3>
|
||
|
||
{importProgress ? (
|
||
<div className="space-y-6">
|
||
{/* 状态描述 */}
|
||
<div className="flex items-center justify-center space-x-2">
|
||
<div className="w-2 h-2 bg-blue-600 rounded-full animate-pulse"></div>
|
||
<div className="text-sm text-gray-600 font-medium">
|
||
{importProgress.current_status}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 进度条 */}
|
||
<div className="space-y-3">
|
||
<div className="flex justify-between items-center text-sm">
|
||
<span className="text-gray-600">进度</span>
|
||
<span className="font-semibold text-blue-600">
|
||
{Math.round((importProgress.processed_count / importProgress.total_count) * 100)}%
|
||
</span>
|
||
</div>
|
||
|
||
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
||
<div
|
||
className="bg-gradient-to-r from-blue-500 to-blue-600 h-3 rounded-full transition-all duration-500 ease-out relative"
|
||
style={{
|
||
width: `${(importProgress.processed_count / importProgress.total_count) * 100}%`
|
||
}}
|
||
>
|
||
<div className="absolute inset-0 bg-white opacity-30 animate-pulse"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-between text-xs text-gray-500">
|
||
<span>{importProgress.processed_count} / {importProgress.total_count} 文件</span>
|
||
<span>剩余 {importProgress.total_count - importProgress.processed_count} 个</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 当前处理文件 */}
|
||
{importProgress.current_file && (
|
||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||
<div className="flex items-center space-x-3">
|
||
<div className="flex-shrink-0">
|
||
<FileText className="w-5 h-5 text-blue-600" />
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-blue-900">正在处理</p>
|
||
<p className="text-sm text-blue-700 truncate" title={importProgress.current_file}>
|
||
{getFileName(importProgress.current_file)}
|
||
</p>
|
||
</div>
|
||
<div className="flex-shrink-0">
|
||
<Loader2 className="w-4 h-4 text-blue-600 animate-spin" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 处理阶段指示器 */}
|
||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||
<div className="flex flex-col items-center space-y-1">
|
||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||
importProgress.processed_count > 0 ? 'bg-green-100 text-green-600' : 'bg-gray-100 text-gray-400'
|
||
}`}>
|
||
<CheckCircle className="w-4 h-4" />
|
||
</div>
|
||
<span className="text-gray-600">文件扫描</span>
|
||
</div>
|
||
<div className="flex flex-col items-center space-y-1">
|
||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||
importProgress.processed_count > 0 ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-400'
|
||
}`}>
|
||
<Loader2 className={`w-4 h-4 ${importProgress.processed_count > 0 ? 'animate-spin' : ''}`} />
|
||
</div>
|
||
<span className="text-gray-600">数据处理</span>
|
||
</div>
|
||
<div className="flex flex-col items-center space-y-1">
|
||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||
importProgress.processed_count === importProgress.total_count ? 'bg-green-100 text-green-600' : 'bg-gray-100 text-gray-400'
|
||
}`}>
|
||
<Upload className="w-4 h-4" />
|
||
</div>
|
||
<span className="text-gray-600">导入完成</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 错误信息 */}
|
||
{importProgress.errors.length > 0 && (
|
||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||
<div className="flex items-start space-x-3">
|
||
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||
<div className="flex-1">
|
||
<h4 className="text-sm font-medium text-red-800 mb-2">
|
||
处理错误 ({importProgress.errors.length})
|
||
</h4>
|
||
<div className="max-h-24 overflow-y-auto text-xs text-red-700 space-y-1">
|
||
{importProgress.errors.map((error, index) => (
|
||
<div key={index} className="bg-red-100 p-2 rounded border-l-2 border-red-400">
|
||
{error}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
/* 初始化状态 */
|
||
<div className="space-y-4">
|
||
<div className="text-sm text-gray-600">正在初始化导入流程...</div>
|
||
<div className="flex justify-center space-x-1">
|
||
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce"></div>
|
||
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{animationDelay: '0.1s'}}></div>
|
||
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 步骤 4: 完成 */}
|
||
{step === 'complete' && (
|
||
<div className="space-y-6">
|
||
<div className="text-center">
|
||
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">导入完成</h3>
|
||
<p className="text-gray-600">
|
||
素材已成功导入到项目中
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 底部按钮 */}
|
||
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200">
|
||
{step === 'batch' && (
|
||
<>
|
||
<button
|
||
onClick={() => setStep('select')}
|
||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||
>
|
||
返回
|
||
</button>
|
||
<button
|
||
onClick={handleScanFolders}
|
||
disabled={selectedFolders.length === 0 || isImporting}
|
||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{isImporting ? (
|
||
<div className="flex items-center space-x-2">
|
||
<Loader2 className="w-4 h-4 animate-spin" />
|
||
<span>处理中...</span>
|
||
</div>
|
||
) : (
|
||
'扫描文件夹'
|
||
)}
|
||
</button>
|
||
</>
|
||
)}
|
||
|
||
{step === 'configure' && (
|
||
<>
|
||
<button
|
||
onClick={() => setStep(importMode === 'folders' ? 'batch' : 'select')}
|
||
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||
>
|
||
返回
|
||
</button>
|
||
<button
|
||
onClick={handleStartImport}
|
||
disabled={selectedFiles.length === 0 || isImporting}
|
||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{isImporting ? (
|
||
<div className="flex items-center space-x-2">
|
||
<Loader2 className="w-4 h-4 animate-spin" />
|
||
<span>导入中...</span>
|
||
</div>
|
||
) : (
|
||
'开始导入'
|
||
)}
|
||
</button>
|
||
</>
|
||
)}
|
||
|
||
{(step === 'select' || step === 'complete') && (
|
||
<button
|
||
onClick={onClose}
|
||
className="px-6 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||
>
|
||
{step === 'complete' ? '完成' : '取消'}
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|