Files
mixvideo-v2/apps/desktop/src/components/ProjectTemplateBindingForm.tsx
imeepos 730402aba0 feat: 实现项目-模板绑定和素材-模特绑定管理功能
新功能:
- 项目-模板绑定管理系统
  - 支持主要/次要模板绑定类型
  - 绑定优先级和状态管理
  - 批量绑定操作
  - 绑定关系的CRUD操作

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

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

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

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

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

 配置更新:
- 更新数据库迁移脚本
- 完善测试配置
- 优化构建流程
2025-07-15 12:50:30 +08:00

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