feat: 完善模板导入功能
新增功能: - 添加详细的模板导入日志系统 - 实现全局进度存储机制 - 完善模板状态管理 修复问题: - 修复进度监控无限轮询问题 - 修复模板列表状态显示不正确问题 - 修复所有unwrap()导致的panic错误 - 修复外键约束失败问题 改进: - 优化素材上传逻辑,只上传视频/音频/图片 - 上传失败时自动跳过而不是中断导入 - 缺失文件时继续导入而不是失败 - 改进错误处理机制
This commit is contained in:
273
apps/desktop/src/components/template/BatchImportModal.tsx
Normal file
273
apps/desktop/src/components/template/BatchImportModal.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, FolderOpen, AlertCircle, CheckCircle, Settings } from 'lucide-react';
|
||||
import { BatchImportRequest } from '../../types/template';
|
||||
import { CustomSelect } from '../CustomSelect';
|
||||
import { useProjectStore } from '../../store/projectStore';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
interface BatchImportModalProps {
|
||||
onClose: () => void;
|
||||
onImport: (request: BatchImportRequest) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const BatchImportModal: React.FC<BatchImportModalProps> = ({
|
||||
onClose,
|
||||
onImport,
|
||||
isLoading,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<Partial<BatchImportRequest>>({
|
||||
folder_path: '',
|
||||
project_id: '',
|
||||
auto_upload: true,
|
||||
max_concurrent: 3,
|
||||
});
|
||||
const [selectedFolder, setSelectedFolder] = useState<string>('');
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
const { projects } = useProjectStore();
|
||||
|
||||
// 处理文件夹选择
|
||||
const handleFolderSelect = async () => {
|
||||
try {
|
||||
// 使用 Tauri 的文件夹选择对话框
|
||||
const selected = await invoke<string | null>('select_directory');
|
||||
if (selected) {
|
||||
setSelectedFolder(selected);
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
folder_path: selected,
|
||||
}));
|
||||
setErrors(prev => ({ ...prev, folder_path: '' }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('文件夹选择失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 表单验证
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.folder_path) {
|
||||
newErrors.folder_path = '请选择包含剪映草稿的文件夹';
|
||||
}
|
||||
|
||||
if (formData.max_concurrent && (formData.max_concurrent < 1 || formData.max_concurrent > 10)) {
|
||||
newErrors.max_concurrent = '并发数量应在 1-10 之间';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
// 处理提交
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await onImport(formData as BatchImportRequest);
|
||||
} catch (error) {
|
||||
console.error('批量导入失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const projectOptions = [
|
||||
{ value: '', label: '不关联项目' },
|
||||
...projects.map(project => ({
|
||||
value: project.id,
|
||||
label: project.name,
|
||||
})),
|
||||
];
|
||||
|
||||
const concurrentOptions = [
|
||||
{ value: `1`, label: '1 个(慢速)' },
|
||||
{ value: `2`, label: '2 个' },
|
||||
{ value: `3`, label: '3 个(推荐)' },
|
||||
{ value: `5`, label: '5 个' },
|
||||
{ value: `10`, label: '10 个(快速)' },
|
||||
];
|
||||
|
||||
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>
|
||||
|
||||
{/* 模态框内容 */}
|
||||
<form onSubmit={handleSubmit} className="p-6">
|
||||
{/* 文件夹选择区域 */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
选择文件夹 *
|
||||
</label>
|
||||
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
|
||||
errors.folder_path
|
||||
? 'border-red-300 bg-red-50'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
{selectedFolder ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<CheckCircle className="w-8 h-8 text-green-500 mr-3" />
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
已选择文件夹
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 break-all">
|
||||
{selectedFolder}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<FolderOpen className="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
||||
<div className="text-sm text-gray-600 mb-2">
|
||||
选择包含剪映草稿文件的文件夹
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mb-3">
|
||||
系统将自动扫描文件夹中的所有 draft_content.json 文件
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFolderSelect}
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4 mr-2" />
|
||||
选择文件夹
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{errors.folder_path && (
|
||||
<div className="flex items-center mt-2 text-sm text-red-600">
|
||||
<AlertCircle className="w-4 h-4 mr-1" />
|
||||
{errors.folder_path}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 关联项目 */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
关联项目
|
||||
</label>
|
||||
<CustomSelect
|
||||
value={formData.project_id || ''}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, project_id: value }))}
|
||||
options={projectOptions}
|
||||
placeholder="选择项目(可选)"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 自动上传选项 */}
|
||||
<div className="mb-6">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.auto_upload}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, auto_upload: e.target.checked }))}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="ml-2 text-sm text-gray-700">
|
||||
自动上传素材到云端存储
|
||||
</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
启用后将自动上传所有模板中的素材文件到云端
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 高级设置 */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
className="flex items-center text-sm text-blue-600 hover:text-blue-700 transition-colors"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-1" />
|
||||
高级设置
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
最大并发数
|
||||
</label>
|
||||
<CustomSelect
|
||||
value={`${formData.max_concurrent || 3}`}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, max_concurrent: parseInt(value) }))}
|
||||
options={concurrentOptions}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
同时处理的模板数量,数值越大速度越快但占用资源越多
|
||||
</p>
|
||||
{errors.max_concurrent && (
|
||||
<div className="flex items-center mt-1 text-sm text-red-600">
|
||||
<AlertCircle className="w-4 h-4 mr-1" />
|
||||
{errors.max_concurrent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 提示信息 */}
|
||||
<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-600 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-blue-800">
|
||||
<div className="font-medium mb-1">批量导入说明:</div>
|
||||
<ul className="text-xs space-y-1">
|
||||
<li>• 系统将递归扫描选定文件夹中的所有 draft_content.json 文件</li>
|
||||
<li>• 每个文件将作为一个独立的模板进行导入</li>
|
||||
<li>• 导入过程中可以在模板管理页面查看进度</li>
|
||||
<li>• 如果某个模板导入失败,不会影响其他模板的导入</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isLoading ? '导入中...' : '开始批量导入'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user