feat: 实现模特管理功能
- 新增模特数据模型和数据库表结构 - 实现模特的完整CRUD操作 - 添加模特照片管理功能 - 实现素材与模特关联功能 - 创建模特管理前端界面 - 集成到主应用导航和路由 - 修复数据库连接死锁问题 功能特性: - 模特基本信息管理(姓名、艺名、性别、年龄等) - 照片管理和封面设置 - 标签系统 - 状态管理(活跃、不活跃、退役、暂停) - 评分系统 - 搜索和过滤功能 - 素材关联功能
This commit is contained in:
390
apps/desktop/src-tauri/src/data/models/model.rs
Normal file
390
apps/desktop/src-tauri/src/data/models/model.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// 模特实体模型
|
||||
/// 遵循 Tauri 开发规范的数据模型设计原则
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Model {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub stage_name: Option<String>, // 艺名
|
||||
pub gender: Gender,
|
||||
pub age: Option<u32>,
|
||||
pub height: Option<u32>, // 身高(厘米)
|
||||
pub weight: Option<u32>, // 体重(公斤)
|
||||
pub measurements: Option<Measurements>, // 三围
|
||||
pub description: Option<String>,
|
||||
pub tags: Vec<String>, // 标签,如"性感"、"清纯"、"成熟"等
|
||||
pub avatar_path: Option<String>, // 头像照片路径
|
||||
pub photos: Vec<ModelPhoto>, // 模特照片集
|
||||
pub contact_info: Option<ContactInfo>, // 联系信息
|
||||
pub social_media: Option<SocialMedia>, // 社交媒体信息
|
||||
pub status: ModelStatus,
|
||||
pub rating: Option<f32>, // 评分 (1.0-5.0)
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
/// 性别枚举
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Gender {
|
||||
Male,
|
||||
Female,
|
||||
Other,
|
||||
}
|
||||
|
||||
/// 三围信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Measurements {
|
||||
pub bust: u32, // 胸围
|
||||
pub waist: u32, // 腰围
|
||||
pub hips: u32, // 臀围
|
||||
}
|
||||
|
||||
/// 模特照片
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModelPhoto {
|
||||
pub id: String,
|
||||
pub model_id: String,
|
||||
pub file_path: String,
|
||||
pub file_name: String,
|
||||
pub file_size: u64,
|
||||
pub photo_type: PhotoType,
|
||||
pub description: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub is_cover: bool, // 是否为封面照片
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 照片类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum PhotoType {
|
||||
Portrait, // 肖像照
|
||||
FullBody, // 全身照
|
||||
Headshot, // 头像照
|
||||
Artistic, // 艺术照
|
||||
Commercial, // 商业照
|
||||
Casual, // 生活照
|
||||
Other,
|
||||
}
|
||||
|
||||
/// 联系信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContactInfo {
|
||||
pub phone: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub wechat: Option<String>,
|
||||
pub qq: Option<String>,
|
||||
pub address: Option<String>,
|
||||
}
|
||||
|
||||
/// 社交媒体信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SocialMedia {
|
||||
pub weibo: Option<String>,
|
||||
pub instagram: Option<String>,
|
||||
pub tiktok: Option<String>,
|
||||
pub xiaohongshu: Option<String>, // 小红书
|
||||
pub douyin: Option<String>, // 抖音
|
||||
}
|
||||
|
||||
/// 模特状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ModelStatus {
|
||||
Active, // 活跃
|
||||
Inactive, // 不活跃
|
||||
Retired, // 退役
|
||||
Suspended, // 暂停
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// 创建新的模特实例
|
||||
pub fn new(
|
||||
name: String,
|
||||
gender: Gender,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name,
|
||||
stage_name: None,
|
||||
gender,
|
||||
age: None,
|
||||
height: None,
|
||||
weight: None,
|
||||
measurements: None,
|
||||
description: None,
|
||||
tags: Vec::new(),
|
||||
avatar_path: None,
|
||||
photos: Vec::new(),
|
||||
contact_info: None,
|
||||
social_media: None,
|
||||
status: ModelStatus::Active,
|
||||
rating: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
is_active: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新模特基本信息
|
||||
pub fn update_basic_info(
|
||||
&mut self,
|
||||
name: Option<String>,
|
||||
stage_name: Option<String>,
|
||||
age: Option<u32>,
|
||||
height: Option<u32>,
|
||||
weight: Option<u32>,
|
||||
description: Option<String>,
|
||||
) {
|
||||
if let Some(name) = name {
|
||||
self.name = name;
|
||||
}
|
||||
if stage_name.is_some() {
|
||||
self.stage_name = stage_name;
|
||||
}
|
||||
if age.is_some() {
|
||||
self.age = age;
|
||||
}
|
||||
if height.is_some() {
|
||||
self.height = height;
|
||||
}
|
||||
if weight.is_some() {
|
||||
self.weight = weight;
|
||||
}
|
||||
if description.is_some() {
|
||||
self.description = description;
|
||||
}
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 设置三围信息
|
||||
pub fn set_measurements(&mut self, measurements: Option<Measurements>) {
|
||||
self.measurements = measurements;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 添加标签
|
||||
pub fn add_tag(&mut self, tag: String) {
|
||||
if !self.tags.contains(&tag) {
|
||||
self.tags.push(tag);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// 移除标签
|
||||
pub fn remove_tag(&mut self, tag: &str) {
|
||||
if let Some(pos) = self.tags.iter().position(|t| t == tag) {
|
||||
self.tags.remove(pos);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置头像
|
||||
pub fn set_avatar(&mut self, avatar_path: Option<String>) {
|
||||
self.avatar_path = avatar_path;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 添加照片
|
||||
pub fn add_photo(&mut self, photo: ModelPhoto) {
|
||||
self.photos.push(photo);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 移除照片
|
||||
pub fn remove_photo(&mut self, photo_id: &str) {
|
||||
if let Some(pos) = self.photos.iter().position(|p| p.id == photo_id) {
|
||||
self.photos.remove(pos);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置联系信息
|
||||
pub fn set_contact_info(&mut self, contact_info: Option<ContactInfo>) {
|
||||
self.contact_info = contact_info;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 设置社交媒体信息
|
||||
pub fn set_social_media(&mut self, social_media: Option<SocialMedia>) {
|
||||
self.social_media = social_media;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 更新状态
|
||||
pub fn update_status(&mut self, status: ModelStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// 设置评分
|
||||
pub fn set_rating(&mut self, rating: Option<f32>) {
|
||||
if let Some(r) = rating {
|
||||
if r >= 1.0 && r <= 5.0 {
|
||||
self.rating = Some(r);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
} else {
|
||||
self.rating = None;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证模特数据
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.name.trim().is_empty() {
|
||||
return Err("模特姓名不能为空".to_string());
|
||||
}
|
||||
|
||||
if self.name.len() > 50 {
|
||||
return Err("模特姓名不能超过50个字符".to_string());
|
||||
}
|
||||
|
||||
if let Some(ref stage_name) = self.stage_name {
|
||||
if stage_name.len() > 50 {
|
||||
return Err("艺名不能超过50个字符".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(age) = self.age {
|
||||
if age > 100 {
|
||||
return Err("年龄不能超过100岁".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(height) = self.height {
|
||||
if height < 50 || height > 250 {
|
||||
return Err("身高必须在50-250厘米之间".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(weight) = self.weight {
|
||||
if weight < 20 || weight > 200 {
|
||||
return Err("体重必须在20-200公斤之间".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref description) = self.description {
|
||||
if description.len() > 1000 {
|
||||
return Err("描述不能超过1000个字符".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rating) = self.rating {
|
||||
if rating < 1.0 || rating > 5.0 {
|
||||
return Err("评分必须在1.0-5.0之间".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取封面照片
|
||||
pub fn get_cover_photo(&self) -> Option<&ModelPhoto> {
|
||||
self.photos.iter().find(|p| p.is_cover)
|
||||
}
|
||||
|
||||
/// 获取显示名称(优先使用艺名)
|
||||
pub fn get_display_name(&self) -> &str {
|
||||
self.stage_name.as_ref().unwrap_or(&self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelPhoto {
|
||||
/// 创建新的模特照片
|
||||
pub fn new(
|
||||
model_id: String,
|
||||
file_path: String,
|
||||
file_name: String,
|
||||
file_size: u64,
|
||||
photo_type: PhotoType,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
model_id,
|
||||
file_path,
|
||||
file_name,
|
||||
file_size,
|
||||
photo_type,
|
||||
description: None,
|
||||
tags: Vec::new(),
|
||||
is_cover: false,
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置为封面照片
|
||||
pub fn set_as_cover(&mut self) {
|
||||
self.is_cover = true;
|
||||
}
|
||||
|
||||
/// 取消封面照片
|
||||
pub fn unset_as_cover(&mut self) {
|
||||
self.is_cover = false;
|
||||
}
|
||||
|
||||
/// 添加标签
|
||||
pub fn add_tag(&mut self, tag: String) {
|
||||
if !self.tags.contains(&tag) {
|
||||
self.tags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
/// 移除标签
|
||||
pub fn remove_tag(&mut self, tag: &str) {
|
||||
if let Some(pos) = self.tags.iter().position(|t| t == tag) {
|
||||
self.tags.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建模特请求模型
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateModelRequest {
|
||||
pub name: String,
|
||||
pub stage_name: Option<String>,
|
||||
pub gender: Gender,
|
||||
pub age: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub weight: Option<u32>,
|
||||
pub measurements: Option<Measurements>,
|
||||
pub description: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub contact_info: Option<ContactInfo>,
|
||||
pub social_media: Option<SocialMedia>,
|
||||
}
|
||||
|
||||
/// 更新模特请求模型
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateModelRequest {
|
||||
pub name: Option<String>,
|
||||
pub stage_name: Option<String>,
|
||||
pub age: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub weight: Option<u32>,
|
||||
pub measurements: Option<Measurements>,
|
||||
pub description: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub contact_info: Option<ContactInfo>,
|
||||
pub social_media: Option<SocialMedia>,
|
||||
pub status: Option<ModelStatus>,
|
||||
pub rating: Option<f32>,
|
||||
}
|
||||
|
||||
/// 模特查询参数
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ModelQueryParams {
|
||||
pub name: Option<String>,
|
||||
pub gender: Option<Gender>,
|
||||
pub min_age: Option<u32>,
|
||||
pub max_age: Option<u32>,
|
||||
pub min_height: Option<u32>,
|
||||
pub max_height: Option<u32>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub status: Option<ModelStatus>,
|
||||
pub min_rating: Option<f32>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
}
|
||||
Reference in New Issue
Block a user