mixvideo-v2/apps/desktop/src/components/ProjectTemplateBindingForm.tsx

304 lines
10 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, useEffect } from 'react';
import { X, Save } from 'lucide-react';
import {
ProjectTemplateBinding,
CreateProjectTemplateBindingRequest,
UpdateProjectTemplateBindingRequest,
BindingType,
BINDING_TYPE_OPTIONS,
validateBindingFormData,
ProjectTemplateBindingFormData,
} from '../types/projectTemplateBinding';
import { Template } from '../types/template';
import { CustomSelect } from './CustomSelect';
import { LoadingSpinner } from './LoadingSpinner';
import { ErrorMessage } from './ErrorMessage';
interface ProjectTemplateBindingFormProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (data: CreateProjectTemplateBindingRequest | UpdateProjectTemplateBindingRequest) => Promise<void>;
projectId: string;
templates: Template[];
binding?: ProjectTemplateBinding;
loading?: boolean;
error?: string | null;
}
export const ProjectTemplateBindingForm: React.FC<ProjectTemplateBindingFormProps> = ({
isOpen,
onClose,
onSubmit,
projectId,
templates,
binding,
loading = false,
error = null,
}) => {
const [formData, setFormData] = useState<ProjectTemplateBindingFormData>({
template_id: '',
binding_name: '',
description: '',
priority: 0,
binding_type: BindingType.Secondary,
});
const [validationErrors, setValidationErrors] = useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
// 初始化表单数据
useEffect(() => {
if (binding) {
setFormData({
template_id: binding.template_id,
binding_name: binding.binding_name || '',
description: binding.description || '',
priority: binding.priority,
binding_type: binding.binding_type,
});
} else {
setFormData({
template_id: '',
binding_name: '',
description: '',
priority: 0,
binding_type: BindingType.Secondary,
});
}
setValidationErrors([]);
}, [binding, isOpen]);
// 处理表单字段变化
const handleFieldChange = (field: keyof ProjectTemplateBindingFormData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 清除相关的验证错误
setValidationErrors([]);
};
// 验证表单
const validateForm = (): boolean => {
const errors = validateBindingFormData(formData);
setValidationErrors(errors);
return errors.length === 0;
};
// 处理表单提交
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
if (binding) {
// 更新模式
const updateData: UpdateProjectTemplateBindingRequest = {
binding_name: formData.binding_name || undefined,
description: formData.description || undefined,
priority: formData.priority,
binding_type: formData.binding_type,
};
await onSubmit(updateData);
} else {
// 创建模式
const createData: CreateProjectTemplateBindingRequest = {
project_id: projectId,
template_id: formData.template_id,
binding_name: formData.binding_name || undefined,
description: formData.description || undefined,
priority: formData.priority,
binding_type: formData.binding_type,
};
await onSubmit(createData);
}
onClose();
} catch (error) {
// 错误由父组件处理
} finally {
setIsSubmitting(false);
}
};
// 获取可用的模板选项
const getTemplateOptions = () => {
return templates.map(template => ({
value: template.id,
label: template.name,
description: template.description,
}));
};
// 获取选中的模板信息
const getSelectedTemplate = () => {
return templates.find(t => t.id === formData.template_id);
};
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 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
{/* 头部 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">
{binding ? '编辑模板绑定' : '添加模板绑定'}
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
disabled={isSubmitting}
>
<X className="w-6 h-6" />
</button>
</div>
{/* 表单内容 */}
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* 错误信息 */}
{(error || validationErrors.length > 0) && (
<div className="space-y-2">
{error && <ErrorMessage message={error} />}
{validationErrors.map((err, index) => (
<ErrorMessage key={index} message={err} />
))}
</div>
)}
{/* 模板选择 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
<span className="text-red-500">*</span>
</label>
<CustomSelect
value={formData.template_id}
onChange={(value) => handleFieldChange('template_id', value)}
options={getTemplateOptions()}
placeholder="请选择模板"
disabled={!!binding || isSubmitting}
className="w-full"
/>
{formData.template_id && (
<div className="text-sm text-gray-600">
{getSelectedTemplate()?.description && (
<p className="mt-1">{getSelectedTemplate()?.description}</p>
)}
</div>
)}
</div>
{/* 绑定名称 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
</label>
<input
type="text"
value={formData.binding_name}
onChange={(e) => handleFieldChange('binding_name', e.target.value)}
placeholder="为此绑定设置一个自定义名称(可选)"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={isSubmitting}
maxLength={100}
/>
<p className="text-xs text-gray-500">
{formData.binding_name.length}/100
</p>
</div>
{/* 绑定类型 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
<span className="text-red-500">*</span>
</label>
<CustomSelect
value={formData.binding_type}
onChange={(value) => handleFieldChange('binding_type', value as BindingType)}
options={BINDING_TYPE_OPTIONS.map(option => ({
value: option.value,
label: option.label,
description: option.description,
}))}
disabled={isSubmitting}
className="w-full"
/>
</div>
{/* 优先级 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
</label>
<input
type="number"
value={formData.priority}
onChange={(e) => handleFieldChange('priority', parseInt(e.target.value) || 0)}
min="0"
max="999"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={isSubmitting}
/>
<p className="text-xs text-gray-500">
0-999
</p>
</div>
{/* 描述 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
</label>
<textarea
value={formData.description}
onChange={(e) => handleFieldChange('description', e.target.value)}
placeholder="描述此绑定的用途或备注信息(可选)"
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
disabled={isSubmitting}
maxLength={500}
/>
<p className="text-xs text-gray-500">
{formData.description.length}/500
</p>
</div>
{/* 操作按钮 */}
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
disabled={isSubmitting}
>
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
disabled={isSubmitting || loading}
>
{(isSubmitting || loading) ? (
<>
<LoadingSpinner size="small" />
<span>...</span>
</>
) : (
<>
<Save className="w-4 h-4" />
<span>{binding ? '更新绑定' : '创建绑定'}</span>
</>
)}
</button>
</div>
</form>
</div>
</div>
);
};