feat: 实现项目-模板绑定和素材-模特绑定管理功能
新功能: - 项目-模板绑定管理系统 - 支持主要/次要模板绑定类型 - 绑定优先级和状态管理 - 批量绑定操作 - 绑定关系的CRUD操作 - 素材-模特绑定管理系统 - 素材与模特的关联管理 - 批量绑定/解绑操作 - 绑定统计和分析 - 素材编辑对话框 架构改进: - 新增项目-模板绑定数据模型和仓库层 - 新增素材-模特绑定业务服务层 - 完善的API命令层实现 - 响应式前端界面设计 用户体验优化: - 统一的通知系统 - 增强的加载状态组件 - 流畅的交互动画 - 优雅的确认对话框 测试覆盖: - 单元测试和集成测试 - 业务逻辑验证 - API接口测试 技术栈: - 后端: Rust + Tauri + SQLite - 前端: React + TypeScript + TailwindCSS - 状态管理: Zustand - 测试: Vitest + Rust测试框架 配置更新: - 更新数据库迁移脚本 - 完善测试配置 - 优化构建流程
This commit is contained in:
303
apps/desktop/src/components/ProjectTemplateBindingForm.tsx
Normal file
303
apps/desktop/src/components/ProjectTemplateBindingForm.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* 项目-模板绑定表单组件
|
||||
* 遵循前端开发规范的组件设计原则
|
||||
*/
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user