feat: 实现项目-模板绑定和素材-模特绑定管理功能

新功能:
- 项目-模板绑定管理系统
  - 支持主要/次要模板绑定类型
  - 绑定优先级和状态管理
  - 批量绑定操作
  - 绑定关系的CRUD操作

- 素材-模特绑定管理系统
  - 素材与模特的关联管理
  - 批量绑定/解绑操作
  - 绑定统计和分析
  - 素材编辑对话框

 架构改进:
- 新增项目-模板绑定数据模型和仓库层
- 新增素材-模特绑定业务服务层
- 完善的API命令层实现
- 响应式前端界面设计

 用户体验优化:
- 统一的通知系统
- 增强的加载状态组件
- 流畅的交互动画
- 优雅的确认对话框

 测试覆盖:
- 单元测试和集成测试
- 业务逻辑验证
- API接口测试

 技术栈:
- 后端: Rust + Tauri + SQLite
- 前端: React + TypeScript + TailwindCSS
- 状态管理: Zustand
- 测试: Vitest + Rust测试框架

 配置更新:
- 更新数据库迁移脚本
- 完善测试配置
- 优化构建流程
This commit is contained in:
imeepos
2025-07-15 12:50:30 +08:00
parent 9f222b562d
commit 730402aba0
41 changed files with 7259 additions and 49 deletions

View 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>
);
};