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 tauri::{command, State};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
use crate::business::services::outfit_analysis_service::OutfitAnalysisService;
|
use crate::business::services::outfit_analysis_service::OutfitAnalysisService;
|
||||||
|
|
@ -146,8 +147,92 @@ pub async fn create_outfit_items_from_analysis(
|
||||||
project_id: String,
|
project_id: String,
|
||||||
analysis_id: String,
|
analysis_id: String,
|
||||||
) -> Result<Vec<OutfitItem>, String> {
|
) -> Result<Vec<OutfitItem>, String> {
|
||||||
// 这是一个简化的实现,实际应该从分析结果中提取产品信息
|
let database = state.get_database();
|
||||||
Err("功能开发中".to_string())
|
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())
|
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