304 lines
10 KiB
TypeScript
304 lines
10 KiB
TypeScript
/**
|
||
* 项目-模板绑定表单组件
|
||
* 遵循前端开发规范的组件设计原则
|
||
*/
|
||
|
||
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>
|
||
);
|
||
};
|