feat: 实现服装搭配功能的图像上传和AI分析

新功能:
- 实现完整的图像上传组件 (ImageUploader)
  - 支持拖拽上传、文件选择、预览和验证
  - 文件大小和格式限制
  - 实时预览和文件管理
- 实现AI图像分析结果展示 (OutfitAnalysisResult)
  - 实时状态更新和进度跟踪
  - 分析结果详情查看
  - 自动刷新机制
- 添加文件保存功能 (save_outfit_image 命令)
- 更新OutfitMatch页面为现代化标签页设计

 技术改进:
- 修复服装搭配相关模型的编译错误
- 添加缺失的trait实现 (Eq, Hash)
- 修复生命周期参数问题
- 完善错误处理和用户反馈

 UI/UX优化:
- 现代化的标签页设计
- 响应式布局和优雅动画
- 统一的设计语言和交互体验
- 完善的加载状态和错误处理
This commit is contained in:
imeepos
2025-07-17 19:05:40 +08:00
parent 7fb1dfa95a
commit 7b1bb2fb0e
19 changed files with 4310 additions and 3 deletions

View File

@@ -0,0 +1,296 @@
/**
* 服装搭配功能集成测试
* 测试从图像分析到搭配推荐的完整流程
*/
#[cfg(test)]
mod outfit_integration_tests {
use std::sync::Arc;
use chrono::Utc;
use crate::data::models::outfit_analysis::{
OutfitAnalysis, CreateOutfitAnalysisRequest, AnalysisStatus, ColorHSV
};
use crate::data::models::outfit_item::{
OutfitItem, CreateOutfitItemRequest, OutfitCategory, OutfitStyle
};
use crate::data::models::outfit_matching::{
OutfitMatching, CreateOutfitMatchingRequest, MatchingType
};
use crate::data::repositories::outfit_analysis_repository::OutfitAnalysisRepository;
use crate::data::repositories::outfit_item_repository::OutfitItemRepository;
use crate::data::repositories::outfit_matching_repository::OutfitMatchingRepository;
use crate::business::services::outfit_analysis_service::OutfitAnalysisService;
use crate::business::services::outfit_item_service::OutfitItemService;
use crate::business::services::outfit_matching_service::OutfitMatchingService;
use crate::infrastructure::database::Database;
use crate::infrastructure::gemini_service::GeminiService;
/// 测试完整的服装分析流程
#[tokio::test]
async fn test_complete_outfit_analysis_workflow() {
// 创建内存数据库
let database = Arc::new(Database::new_in_memory().unwrap());
// 创建仓库
let analysis_repo = Arc::new(OutfitAnalysisRepository::new(database.clone()).unwrap());
let item_repo = Arc::new(OutfitItemRepository::new(database.clone()).unwrap());
let matching_repo = Arc::new(OutfitMatchingRepository::new(database.clone()).unwrap());
// 创建服务注意在实际测试中应该使用模拟的GeminiService
let gemini_service = Arc::new(GeminiService::new().unwrap());
let analysis_service = OutfitAnalysisService::new(analysis_repo.clone(), gemini_service);
let item_service = OutfitItemService::new(item_repo.clone());
let matching_service = OutfitMatchingService::new(matching_repo.clone(), item_repo.clone());
// 1. 创建分析记录
let analysis_request = CreateOutfitAnalysisRequest {
project_id: "test_project".to_string(),
image_path: "/test/image.jpg".to_string(),
image_name: "test_image.jpg".to_string(),
};
let analysis = analysis_service.create_analysis(analysis_request).await.unwrap();
assert_eq!(analysis.analysis_status, AnalysisStatus::Pending);
// 2. 模拟分析完成,手动创建服装单品
let item1_request = CreateOutfitItemRequest {
project_id: "test_project".to_string(),
analysis_id: Some(analysis.id.clone()),
name: "红色T恤".to_string(),
category: OutfitCategory::Top,
brand: Some("测试品牌".to_string()),
model: None,
color_primary: ColorHSV::new(0.0, 0.8, 0.9), // 红色
color_secondary: None,
styles: vec![OutfitStyle::Casual],
design_elements: vec!["纯色".to_string()],
size: None,
material: None,
price: Some(99.0),
purchase_date: None,
image_urls: vec![],
tags: vec!["夏季".to_string(), "休闲".to_string()],
notes: Some("舒适的棉质T恤".to_string()),
};
let item1 = item_service.create_item(item1_request).await.unwrap();
assert_eq!(item1.category, OutfitCategory::Top);
let item2_request = CreateOutfitItemRequest {
project_id: "test_project".to_string(),
analysis_id: Some(analysis.id.clone()),
name: "蓝色牛仔裤".to_string(),
category: OutfitCategory::Bottom,
brand: Some("测试品牌".to_string()),
model: None,
color_primary: ColorHSV::new(0.6, 0.7, 0.8), // 蓝色
color_secondary: None,
styles: vec![OutfitStyle::Casual],
design_elements: vec!["水洗".to_string()],
size: None,
material: None,
price: Some(199.0),
purchase_date: None,
image_urls: vec![],
tags: vec!["四季".to_string(), "百搭".to_string()],
notes: Some("经典款牛仔裤".to_string()),
};
let item2 = item_service.create_item(item2_request).await.unwrap();
assert_eq!(item2.category, OutfitCategory::Bottom);
// 3. 创建搭配
let matching_request = CreateOutfitMatchingRequest {
project_id: "test_project".to_string(),
matching_name: "休闲夏日搭配".to_string(),
matching_type: MatchingType::StyleConsistent,
item_ids: vec![item1.id.clone(), item2.id.clone()],
occasion_tags: vec!["日常".to_string(), "休闲".to_string()],
season_tags: vec!["夏季".to_string()],
style_description: Some("轻松休闲的夏日搭配".to_string()),
};
let matching = matching_service.create_matching(matching_request).await.unwrap();
assert_eq!(matching.matching_type, MatchingType::StyleConsistent);
assert_eq!(matching.items.len(), 2);
// 4. 验证搭配评分
assert!(matching.score_details.overall_score >= 0.0);
assert!(matching.score_details.overall_score <= 1.0);
// 5. 测试搭配推荐
let recommended_items = item_service.recommend_matching_items(
"test_project",
&item1.id,
Some(3)
).await.unwrap();
// 应该推荐item2因为它与item1风格匹配且类别互补
assert!(recommended_items.iter().any(|item| item.id == item2.id));
println!("✅ 服装分析完整流程测试通过");
}
/// 测试颜色匹配功能
#[tokio::test]
async fn test_color_matching_functionality() {
let database = Arc::new(Database::new_in_memory().unwrap());
let item_repo = Arc::new(OutfitItemRepository::new(database.clone()).unwrap());
let item_service = OutfitItemService::new(item_repo.clone());
// 创建不同颜色的服装单品
let red_item = CreateOutfitItemRequest {
project_id: "test_project".to_string(),
analysis_id: None,
name: "红色上衣".to_string(),
category: OutfitCategory::Top,
brand: None,
model: None,
color_primary: ColorHSV::new(0.0, 0.8, 0.9), // 纯红色
color_secondary: None,
styles: vec![OutfitStyle::Casual],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
};
let blue_item = CreateOutfitItemRequest {
project_id: "test_project".to_string(),
analysis_id: None,
name: "蓝色上衣".to_string(),
category: OutfitCategory::Top,
brand: None,
model: None,
color_primary: ColorHSV::new(0.6, 0.8, 0.9), // 纯蓝色
color_secondary: None,
styles: vec![OutfitStyle::Casual],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
};
let similar_red_item = CreateOutfitItemRequest {
project_id: "test_project".to_string(),
analysis_id: None,
name: "深红色上衣".to_string(),
category: OutfitCategory::Top,
brand: None,
model: None,
color_primary: ColorHSV::new(0.02, 0.75, 0.85), // 相似的红色
color_secondary: None,
styles: vec![OutfitStyle::Casual],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
};
// 创建服装单品
let red = item_service.create_item(red_item).await.unwrap();
let blue = item_service.create_item(blue_item).await.unwrap();
let similar_red = item_service.create_item(similar_red_item).await.unwrap();
// 测试颜色搜索
let target_color = ColorHSV::new(0.0, 0.8, 0.9); // 红色
let matching_items = item_service.search_by_color(
"test_project",
&target_color,
0.7 // 70% 相似度阈值
).await.unwrap();
// 应该找到红色和相似红色的单品,但不包括蓝色
assert!(matching_items.iter().any(|item| item.id == red.id));
assert!(matching_items.iter().any(|item| item.id == similar_red.id));
assert!(!matching_items.iter().any(|item| item.id == blue.id));
println!("✅ 颜色匹配功能测试通过");
}
/// 测试搭配评分算法
#[tokio::test]
async fn test_outfit_scoring_algorithm() {
let database = Arc::new(Database::new_in_memory().unwrap());
let matching_repo = Arc::new(OutfitMatchingRepository::new(database.clone()).unwrap());
let item_repo = Arc::new(OutfitItemRepository::new(database.clone()).unwrap());
let matching_service = OutfitMatchingService::new(matching_repo, item_repo);
// 创建测试用的服装单品
let items = vec![
OutfitItem {
id: "1".to_string(),
project_id: "test".to_string(),
analysis_id: None,
name: "白色T恤".to_string(),
category: OutfitCategory::Top,
brand: None,
model: None,
color_primary: ColorHSV::new(0.0, 0.0, 1.0), // 白色
color_secondary: None,
styles: vec![OutfitStyle::Casual, OutfitStyle::Minimalist],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
created_at: Utc::now(),
updated_at: Utc::now(),
},
OutfitItem {
id: "2".to_string(),
project_id: "test".to_string(),
analysis_id: None,
name: "黑色裤子".to_string(),
category: OutfitCategory::Bottom,
brand: None,
model: None,
color_primary: ColorHSV::new(0.0, 0.0, 0.0), // 黑色
color_secondary: None,
styles: vec![OutfitStyle::Casual, OutfitStyle::Minimalist],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
created_at: Utc::now(),
updated_at: Utc::now(),
},
];
// 测试评分计算
let (score_details, suggestions) = matching_service.calculate_matching_score_and_suggestions(&items);
// 验证评分范围
assert!(score_details.color_harmony_score >= 0.0 && score_details.color_harmony_score <= 1.0);
assert!(score_details.style_consistency_score >= 0.0 && score_details.style_consistency_score <= 1.0);
assert!(score_details.overall_score >= 0.0 && score_details.overall_score <= 1.0);
// 黑白搭配应该有较高的风格一致性(都是极简风格)
assert!(score_details.style_consistency_score > 0.5);
println!("✅ 搭配评分算法测试通过");
println!(" 颜色和谐度: {:.2}", score_details.color_harmony_score);
println!(" 风格一致性: {:.2}", score_details.style_consistency_score);
println!(" 综合评分: {:.2}", score_details.overall_score);
}
}