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

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

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

277 lines
9.4 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, Upload, File, AlertCircle, CheckCircle } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { ImportTemplateRequest } from '../../types/template';
import { CustomSelect } from '../CustomSelect';
import { useProjectStore } from '../../store/projectStore';
interface ImportTemplateModalProps {
onClose: () => void;
onImport: (request: ImportTemplateRequest) => Promise<void>;
isLoading: boolean;
}
export const ImportTemplateModal: React.FC<ImportTemplateModalProps> = ({
onClose,
onImport,
isLoading,
}) => {
const [formData, setFormData] = useState<Partial<ImportTemplateRequest>>({
file_path: '',
template_name: '',
project_id: '',
auto_upload: true,
});
const [dragOver, setDragOver] = useState(false);
const [selectedFile, setSelectedFile] = useState<string>('');
const [errors, setErrors] = useState<Record<string, string>>({});
const { projects } = useProjectStore();
// 处理文件选择
const handleFileSelect = async () => {
try {
// 使用 Tauri 命令选择文件
const selected = await invoke<string | null>('select_file', {
filters: [['剪映草稿文件', ['json']]]
});
if (selected) {
setSelectedFile(selected);
setFormData(prev => ({
...prev,
file_path: selected,
template_name: prev.template_name || extractTemplateNameFromPath(selected),
}));
setErrors(prev => ({ ...prev, file_path: '' }));
}
} catch (error) {
console.error('文件选择失败:', error);
}
};
// 从文件路径提取模板名称
const extractTemplateNameFromPath = (filePath: string) => {
const parts = filePath.split(/[/\\]/);
const fileName = parts[parts.length - 1];
return fileName.replace('.json', '').replace('draft_content', '模板');
};
// 处理拖拽
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
const files = Array.from(e.dataTransfer.files);
const jsonFile = files.find(file => file.name.endsWith('.json'));
if (jsonFile) {
// 注意:在 Tauri 中,我们需要使用文件路径而不是 File 对象
// 这里需要根据实际的 Tauri API 来处理文件路径获取
console.log('拖拽文件:', jsonFile.name);
// 实际实现中需要获取文件的完整路径
}
};
// 表单验证
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.file_path) {
newErrors.file_path = '请选择剪映草稿文件';
}
if (!formData.template_name?.trim()) {
newErrors.template_name = '请输入模板名称';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理提交
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
try {
await onImport(formData as ImportTemplateRequest);
} catch (error) {
console.error('导入失败:', error);
}
};
const projectOptions = [
{ value: '', label: '不关联项目' },
...projects.map(project => ({
value: project.id,
label: project.name,
})),
];
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">
{/* 模态框头部 */}
<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 ${
dragOver
? 'border-blue-400 bg-blue-50'
: errors.file_path
? 'border-red-300 bg-red-50'
: 'border-gray-300 hover:border-gray-400'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{selectedFile ? (
<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">
{selectedFile.split(/[/\\]/).pop()}
</div>
<div className="text-xs text-gray-500">
{selectedFile}
</div>
</div>
</div>
) : (
<>
<Upload className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<div className="text-sm text-gray-600 mb-2">
draft_content.json
</div>
<div className="text-xs text-gray-500 mb-3">
</div>
</>
)}
<button
type="button"
onClick={handleFileSelect}
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"
>
<File className="w-4 h-4 mr-2" />
</button>
</div>
{errors.file_path && (
<div className="flex items-center mt-2 text-sm text-red-600">
<AlertCircle className="w-4 h-4 mr-1" />
{errors.file_path}
</div>
)}
</div>
{/* 模板名称 */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
value={formData.template_name || ''}
onChange={(e) => setFormData(prev => ({ ...prev, template_name: e.target.value }))}
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.template_name ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="输入模板名称"
/>
{errors.template_name && (
<div className="flex items-center mt-1 text-sm text-red-600">
<AlertCircle className="w-4 h-4 mr-1" />
{errors.template_name}
</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="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-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? '导入中...' : '开始导入'}
</button>
</div>
</form>
</div>
</div>
);
};