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

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

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

274 lines
9.9 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, { 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>
);
};