feat: 完善服装搭配功能 - 服装单品管理和AI分析结果转换
新功能: - 完善 create_outfit_items_from_analysis 功能 - 从AI分析结果自动创建服装单品 - 智能类别映射和数据转换 - HSV颜色信息解析和处理 - 批量创建流程和错误处理 - 实现完整的服装单品管理系统 - OutfitItemList: 列表展示、搜索、筛选、详情查看 - OutfitItemForm: 创建/编辑表单,动态标签管理 - 完整的CRUD操作和状态管理 - 集成到OutfitMatch页面的标签页系统 技术改进: - 修复编译错误和类型不匹配问题 - 完善错误处理和用户反馈机制 - 实现类型安全的数据转换 - 添加详细的操作日志和状态跟踪 UI/UX优化: - 现代化的服装单品卡片设计 - 智能搜索和分类筛选 - 响应式网格布局 - 优雅的表单设计和交互 - 统一的模态框和通知系统 数据流程: - 图像上传 AI分析 结果展示 一键创建单品 单品管理 - 完整的用户体验闭环 - 实时状态更新和进度跟踪 功能完成度: - 图像上传和保存 - AI图像分析 - 分析结果展示 - 从分析结果创建服装单品 - 服装单品管理 - 下一步: 智能搭配推荐
This commit is contained in:
parent
c31a8c5ba9
commit
ebe4a24bc0
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
use tauri::{command, State};
|
||||
use std::sync::Arc;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::business::services::outfit_analysis_service::OutfitAnalysisService;
|
||||
|
|
@ -146,8 +147,92 @@ pub async fn create_outfit_items_from_analysis(
|
|||
project_id: String,
|
||||
analysis_id: String,
|
||||
) -> Result<Vec<OutfitItem>, String> {
|
||||
// 这是一个简化的实现,实际应该从分析结果中提取产品信息
|
||||
Err("功能开发中".to_string())
|
||||
let database = state.get_database();
|
||||
let analysis_service = create_analysis_service(database.clone())?;
|
||||
let item_service = create_item_service(database)?;
|
||||
|
||||
// 1. 获取分析结果
|
||||
let analysis = analysis_service.get_analysis_by_id(&analysis_id).await
|
||||
.map_err(|e| format!("获取分析结果失败: {}", e))?
|
||||
.ok_or("分析结果不存在")?;
|
||||
|
||||
// 2. 检查分析状态
|
||||
if analysis.analysis_status != crate::data::models::outfit_analysis::AnalysisStatus::Completed {
|
||||
return Err("分析尚未完成,无法创建服装单品".to_string());
|
||||
}
|
||||
|
||||
// 3. 解析分析结果
|
||||
let analysis_result = analysis.analysis_result
|
||||
.ok_or("分析结果为空")?;
|
||||
|
||||
// 4. 从分析结果中提取产品信息
|
||||
let products = extract_products_from_image_analysis(&analysis_result);
|
||||
|
||||
if products.is_empty() {
|
||||
return Err("分析结果中没有找到服装单品".to_string());
|
||||
}
|
||||
|
||||
// 5. 为每个产品创建服装单品
|
||||
let mut created_items = Vec::new();
|
||||
for (index, product) in products.iter().enumerate() {
|
||||
// 转换类别字符串为枚举
|
||||
let category = match product.category.as_str() {
|
||||
"上衣" | "T恤" | "衬衫" | "毛衣" | "背心" => crate::data::models::outfit_item::OutfitCategory::Top,
|
||||
"下装" | "裤子" | "短裤" | "裙子" => crate::data::models::outfit_item::OutfitCategory::Bottom,
|
||||
"外套" | "夹克" | "大衣" => crate::data::models::outfit_item::OutfitCategory::Outerwear,
|
||||
"连衣裙" => crate::data::models::outfit_item::OutfitCategory::Dress,
|
||||
"鞋子" | "运动鞋" | "高跟鞋" | "靴子" => crate::data::models::outfit_item::OutfitCategory::Footwear,
|
||||
"包包" | "手提包" | "背包" | "配饰" | "帽子" | "围巾" | "手表" => crate::data::models::outfit_item::OutfitCategory::Accessory,
|
||||
_ => crate::data::models::outfit_item::OutfitCategory::Other,
|
||||
};
|
||||
|
||||
// 转换颜色信息
|
||||
let color_primary = if let Some(color_value) = &product.color_pattern {
|
||||
parse_color_from_value(color_value).unwrap_or_else(|| {
|
||||
crate::data::models::outfit_analysis::ColorHSV::new(0.0, 0.0, 0.5)
|
||||
})
|
||||
} else {
|
||||
crate::data::models::outfit_analysis::ColorHSV::new(0.0, 0.0, 0.5)
|
||||
};
|
||||
|
||||
let request = CreateOutfitItemRequest {
|
||||
project_id: project_id.clone(),
|
||||
analysis_id: Some(analysis_id.clone()),
|
||||
name: product.name.clone(),
|
||||
category,
|
||||
brand: None, // AI分析通常不能识别品牌
|
||||
model: None,
|
||||
color_primary,
|
||||
color_secondary: None,
|
||||
styles: vec![], // 暂时为空,可以后续扩展
|
||||
design_elements: product.design_styles.clone().unwrap_or_default(),
|
||||
size: None, // AI分析通常不能识别尺寸
|
||||
material: None,
|
||||
price: None, // AI分析不包含价格信息
|
||||
purchase_date: None,
|
||||
image_urls: vec![analysis.image_path.clone()],
|
||||
tags: product.tags.clone().unwrap_or_default(),
|
||||
notes: Some(format!("从AI分析结果自动创建 (分析ID: {})", analysis_id)),
|
||||
};
|
||||
|
||||
match item_service.create_item(request).await {
|
||||
Ok(item) => {
|
||||
created_items.push(item);
|
||||
println!("✅ 成功创建服装单品 {}: {}", index + 1, product.name);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ 创建服装单品失败 {}: {} - {}", index + 1, product.name, e);
|
||||
// 继续创建其他单品,不因为一个失败而停止
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if created_items.is_empty() {
|
||||
return Err("没有成功创建任何服装单品".to_string());
|
||||
}
|
||||
|
||||
println!("🎉 成功从分析结果创建了 {} 个服装单品", created_items.len());
|
||||
Ok(created_items)
|
||||
}
|
||||
|
||||
/// 获取服装单品列表
|
||||
|
|
@ -331,3 +416,159 @@ pub async fn save_outfit_image(
|
|||
// 返回文件路径
|
||||
Ok(file_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// 从ImageAnalysisResult中提取产品信息
|
||||
fn extract_products_from_image_analysis(analysis_result: &crate::data::models::outfit_analysis::ImageAnalysisResult) -> Vec<ProductInfo> {
|
||||
analysis_result.products.iter().map(|product| {
|
||||
ProductInfo {
|
||||
name: format!("{}", product.category),
|
||||
category: product.category.clone(),
|
||||
description: Some(product.description.clone()),
|
||||
color_pattern: Some(serde_json::to_value(&product.color_pattern).unwrap_or(serde_json::Value::Null)),
|
||||
design_styles: Some(product.design_styles.clone()),
|
||||
tags: Some(vec![product.category.clone()]),
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// 从分析结果中提取产品信息(备用方法)
|
||||
fn extract_products_from_analysis(analysis_result: &Value) -> anyhow::Result<Vec<ProductInfo>> {
|
||||
let mut products = Vec::new();
|
||||
|
||||
// 尝试从不同的结构中提取产品信息
|
||||
if let Some(products_array) = analysis_result.get("products").and_then(|v| v.as_array()) {
|
||||
// 标准格式:{ "products": [...] }
|
||||
for (index, product) in products_array.iter().enumerate() {
|
||||
if let Some(product_info) = parse_product_from_json(product, index)? {
|
||||
products.push(product_info);
|
||||
}
|
||||
}
|
||||
} else if let Some(items_array) = analysis_result.get("items").and_then(|v| v.as_array()) {
|
||||
// 备选格式:{ "items": [...] }
|
||||
for (index, item) in items_array.iter().enumerate() {
|
||||
if let Some(product_info) = parse_product_from_json(item, index)? {
|
||||
products.push(product_info);
|
||||
}
|
||||
}
|
||||
} else if analysis_result.is_array() {
|
||||
// 直接是数组格式:[...]
|
||||
if let Some(array) = analysis_result.as_array() {
|
||||
for (index, item) in array.iter().enumerate() {
|
||||
if let Some(product_info) = parse_product_from_json(item, index)? {
|
||||
products.push(product_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 尝试解析单个产品对象
|
||||
if let Some(product_info) = parse_product_from_json(analysis_result, 0)? {
|
||||
products.push(product_info);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(products)
|
||||
}
|
||||
|
||||
/// 从JSON对象解析单个产品信息
|
||||
fn parse_product_from_json(json: &Value, index: usize) -> anyhow::Result<Option<ProductInfo>> {
|
||||
// 提取类别信息
|
||||
let category = json.get("category")
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| json.get("type").and_then(|v| v.as_str()))
|
||||
.or_else(|| json.get("clothing_type").and_then(|v| v.as_str()))
|
||||
.unwrap_or("未分类")
|
||||
.to_string();
|
||||
|
||||
// 如果没有有效的类别,跳过这个产品
|
||||
if category == "未分类" || category.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 提取名称
|
||||
let name = json.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| json.get("description").and_then(|v| v.as_str()))
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("{} {}", category, index + 1));
|
||||
|
||||
// 提取描述
|
||||
let description = json.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| json.get("details").and_then(|v| v.as_str()))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// 提取颜色信息
|
||||
let color_pattern = json.get("color_pattern")
|
||||
.or_else(|| json.get("colors"))
|
||||
.or_else(|| json.get("color"))
|
||||
.cloned();
|
||||
|
||||
// 提取设计风格
|
||||
let design_styles = json.get("design_styles")
|
||||
.or_else(|| json.get("styles"))
|
||||
.or_else(|| json.get("style"))
|
||||
.and_then(|v| {
|
||||
if let Some(array) = v.as_array() {
|
||||
Some(array.iter()
|
||||
.filter_map(|s| s.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.collect())
|
||||
} else if let Some(s) = v.as_str() {
|
||||
Some(vec![s.to_string()])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// 提取标签
|
||||
let tags = json.get("tags")
|
||||
.and_then(|v| {
|
||||
if let Some(array) = v.as_array() {
|
||||
Some(array.iter()
|
||||
.filter_map(|s| s.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.collect())
|
||||
} else if let Some(s) = v.as_str() {
|
||||
Some(vec![s.to_string()])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Some(ProductInfo {
|
||||
name,
|
||||
category,
|
||||
description,
|
||||
color_pattern,
|
||||
design_styles,
|
||||
tags,
|
||||
}))
|
||||
}
|
||||
|
||||
/// 产品信息结构体
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProductInfo {
|
||||
name: String,
|
||||
category: String,
|
||||
description: Option<String>,
|
||||
color_pattern: Option<Value>,
|
||||
design_styles: Option<Vec<String>>,
|
||||
tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// 从JSON值解析颜色信息
|
||||
fn parse_color_from_value(value: &Value) -> Option<crate::data::models::outfit_analysis::ColorHSV> {
|
||||
if let Some(obj) = value.as_object() {
|
||||
let hue = obj.get("hue")?.as_f64().unwrap_or(0.0);
|
||||
let saturation = obj.get("saturation")?.as_f64().unwrap_or(0.0);
|
||||
let value = obj.get("value")?.as_f64().unwrap_or(0.5);
|
||||
|
||||
Some(crate::data::models::outfit_analysis::ColorHSV::new(
|
||||
hue.clamp(0.0, 1.0),
|
||||
saturation.clamp(0.0, 1.0),
|
||||
value.clamp(0.0, 1.0)
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue