diff --git a/apps/desktop/src-tauri/src/data/models/mod.rs b/apps/desktop/src-tauri/src/data/models/mod.rs index 0a01add..63cc5aa 100644 --- a/apps/desktop/src-tauri/src/data/models/mod.rs +++ b/apps/desktop/src-tauri/src/data/models/mod.rs @@ -14,6 +14,7 @@ pub mod video_generation; pub mod conversation; pub mod outfit_search; pub mod gemini_analysis; +pub mod outfit_recommendation; pub mod custom_tag; pub mod watermark; pub mod thumbnail; diff --git a/apps/desktop/src-tauri/src/data/models/outfit_recommendation.rs b/apps/desktop/src-tauri/src/data/models/outfit_recommendation.rs new file mode 100644 index 0000000..d293e6f --- /dev/null +++ b/apps/desktop/src-tauri/src/data/models/outfit_recommendation.rs @@ -0,0 +1,191 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; + +/// 色彩信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ColorInfo { + /// 色彩名称 + pub name: String, + /// 十六进制色值 + pub hex: String, + /// HSV值 (0-1范围) + pub hsv: (f32, f32, f32), +} + +/// 穿搭单品 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OutfitItem { + /// 单品类别 (如: "上装", "下装", "鞋子", "配饰") + pub category: String, + /// 单品描述 + pub description: String, + /// 主要颜色 + pub primary_color: ColorInfo, + /// 次要颜色 (可选) + pub secondary_color: Option, + /// 材质 + pub material: String, + /// 风格标签 + pub style_tags: Vec, +} + +/// 场景建议 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SceneRecommendation { + /// 场景名称 + pub name: String, + /// 场景描述 + pub description: String, + /// 场景类型 ("室内", "室外", "特殊") + pub scene_type: String, + /// 适合的时间段 + pub time_of_day: Vec, + /// 光线条件 + pub lighting: String, + /// 拍摄建议 + pub photography_tips: Vec, +} + +/// 穿搭方案 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OutfitRecommendation { + /// 方案ID + pub id: String, + /// 方案标题 + pub title: String, + /// 方案描述 + pub description: String, + /// 整体风格 + pub overall_style: String, + /// 风格标签 + pub style_tags: Vec, + /// 适合场合 + pub occasions: Vec, + /// 适合季节 + pub seasons: Vec, + /// 穿搭单品列表 + pub items: Vec, + /// 色彩搭配主题 + pub color_theme: String, + /// 主要色彩 + pub primary_colors: Vec, + /// 场景建议 + pub scene_recommendations: Vec, + /// TikTok优化建议 + pub tiktok_tips: Vec, + /// 搭配要点 + pub styling_tips: Vec, + /// 创建时间 + pub created_at: DateTime, +} + +/// 穿搭方案生成请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OutfitRecommendationRequest { + /// 用户输入的关键词或描述 + pub query: String, + /// 目标风格 (可选) + pub target_style: Option, + /// 适合场合 (可选) + pub occasions: Option>, + /// 季节偏好 (可选) + pub season: Option, + /// 色彩偏好 (可选) + pub color_preferences: Option>, + /// 生成数量 + pub count: u32, +} + +/// 穿搭方案生成响应 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OutfitRecommendationResponse { + /// 生成的穿搭方案列表 + pub recommendations: Vec, + /// 生成时间 (毫秒) + pub generation_time_ms: u64, + /// 生成时间戳 + pub generated_at: DateTime, + /// 使用的提示词 (调试用) + pub prompt_used: Option, +} + +/// 场景检索请求 (用于方案详情到场景检索的集成) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SceneSearchRequest { + /// 穿搭方案ID + pub outfit_id: String, + /// 场景描述 + pub scene_description: String, + /// 风格标签 + pub style_tags: Vec, + /// 色彩信息 + pub colors: Vec, + /// 搜索参数 + pub search_params: SceneSearchParams, +} + +/// 场景检索参数 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SceneSearchParams { + /// 场景类型过滤 + pub scene_types: Option>, + /// 时间段过滤 + pub time_filters: Option>, + /// 光线条件过滤 + pub lighting_filters: Option>, + /// 结果数量限制 + pub limit: Option, +} + +impl Default for OutfitRecommendationRequest { + fn default() -> Self { + Self { + query: String::new(), + target_style: None, + occasions: None, + season: None, + color_preferences: None, + count: 3, // 默认生成3个方案 + } + } +} + +impl OutfitRecommendation { + /// 生成用于场景检索的描述 + pub fn generate_scene_search_description(&self) -> String { + let mut description = format!("{} - {}", self.title, self.description); + + if !self.style_tags.is_empty() { + description.push_str(&format!(" 风格: {}", self.style_tags.join(", "))); + } + + if !self.primary_colors.is_empty() { + let color_names: Vec = self.primary_colors.iter() + .map(|c| c.name.clone()) + .collect(); + description.push_str(&format!(" 主要色彩: {}", color_names.join(", "))); + } + + description + } + + /// 获取所有风格标签 (包括整体风格和单品风格) + pub fn get_all_style_tags(&self) -> Vec { + let mut all_tags = self.style_tags.clone(); + + // 添加整体风格 + if !self.overall_style.is_empty() { + all_tags.push(self.overall_style.clone()); + } + + // 添加单品风格标签 + for item in &self.items { + all_tags.extend(item.style_tags.clone()); + } + + // 去重并排序 + all_tags.sort(); + all_tags.dedup(); + all_tags + } +} diff --git a/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs b/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs index f4cf1b8..93bb4de 100644 --- a/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs +++ b/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs @@ -18,6 +18,12 @@ use crate::data::models::conversation::{ }; use crate::data::repositories::conversation_repository::ConversationRepository; +// 导入穿搭方案推荐相关模块 +use crate::data::models::outfit_recommendation::{ + OutfitRecommendation, OutfitRecommendationRequest, OutfitRecommendationResponse, + OutfitItem, ColorInfo, SceneRecommendation +}; + /// Gemini API配置 #[derive(Debug, Clone)] pub struct GeminiConfig { @@ -1690,3 +1696,462 @@ impl GeminiService { } } +// 穿搭方案推荐扩展 +impl GeminiService { + /// 生成穿搭方案推荐 + pub async fn generate_outfit_recommendations(&mut self, request: &OutfitRecommendationRequest) -> Result { + println!("🎨 开始生成穿搭方案推荐..."); + let start_time = std::time::Instant::now(); + + // 构建穿搭方案生成提示词 + let prompt = self.build_outfit_recommendation_prompt(request); + + // 获取访问令牌 + let access_token = self.get_access_token().await?; + + // 创建客户端配置 + let client_config = self.create_gemini_client(&access_token); + + // 准备请求数据 + let request_data = GenerateContentRequest { + contents: vec![ContentPart { + role: "user".to_string(), + parts: vec![Part::Text { text: prompt.clone() }], + }], + generation_config: GenerationConfig { + temperature: 0.8, // 稍高的温度以增加创意性 + top_k: 40, + top_p: 0.95, + max_output_tokens: self.config.max_tokens, + }, + }; + + // 发送请求到Cloudflare Gateway + let generate_url = format!("{}/{}:generateContent", client_config.gateway_url, self.config.model_name); + + // 重试机制 + let mut last_error = None; + + for attempt in 0..self.config.max_retries { + match self.send_generate_request(&generate_url, &client_config, &request_data).await { + Ok(result) => { + let raw_response = self.parse_gemini_response_content(&result)?; + + // 解析穿搭方案 + match self.parse_outfit_recommendations(&raw_response, request) { + Ok(recommendations) => { + let generation_time = start_time.elapsed().as_millis() as u64; + + println!("✅ 穿搭方案生成完成,共生成 {} 个方案", recommendations.len()); + + return Ok(OutfitRecommendationResponse { + recommendations, + generation_time_ms: generation_time, + generated_at: chrono::Utc::now(), + prompt_used: Some(prompt), + }); + } + Err(parse_error) => { + println!("⚠️ 解析穿搭方案失败: {}", parse_error); + last_error = Some(anyhow!("解析穿搭方案失败: {}", parse_error)); + } + } + } + Err(e) => { + last_error = Some(e); + + if attempt < self.config.max_retries - 1 { + tokio::time::sleep(tokio::time::Duration::from_secs(self.config.retry_delay)).await; + } + } + } + } + + Err(anyhow!("穿搭方案生成失败,已重试{}次: {}", self.config.max_retries, last_error.unwrap())) + } + + /// 构建穿搭方案生成提示词 + fn build_outfit_recommendation_prompt(&self, request: &OutfitRecommendationRequest) -> String { + let mut prompt = String::new(); + + // 基于TikTok模特拍摄选景专家的核心理念 + prompt.push_str(r#"你是一位专业的TikTok时尚穿搭顾问,深度理解短视频平台的视觉传播规律和用户审美趋势。 +请根据用户需求生成专业的穿搭方案推荐,确保每个方案都能最大化吸引TikTok用户注意力。 + +## 核心设计原则 +- **视觉冲击力优先**: 确保穿搭在3秒内抓住用户注意力 +- **平台特性导向**: 所有搭配都要考虑TikTok平台的传播特性和算法偏好 +- **色彩搭配和谐**: 优先选择能产生强烈视觉对比和吸引力的色彩组合 +- **场景适配性**: 确保穿搭与拍摄场景完美匹配 +- **趋势敏感性**: 紧跟TikTok视觉趋势,体现当下流行元素 + +## 当前热门趋势 +- **色彩趋势**: 奶油色系、多巴胺色彩、莫兰迪色系、渐变色彩、对比色搭配 +- **风格趋势**: 休闲简约、街头潮流、复古回潮、甜美可爱、酷帅中性 +- **场景偏好**: 咖啡厅、城市街道、公园绿地、家居环境、商场购物中心 + +"#); + + // 添加用户具体需求 + prompt.push_str(&format!("\n## 用户需求\n")); + prompt.push_str(&format!("关键词: {}\n", request.query)); + + if let Some(style) = &request.target_style { + prompt.push_str(&format!("目标风格: {}\n", style)); + } + + if let Some(occasions) = &request.occasions { + prompt.push_str(&format!("适合场合: {}\n", occasions.join(", "))); + } + + if let Some(season) = &request.season { + prompt.push_str(&format!("季节偏好: {}\n", season)); + } + + if let Some(colors) = &request.color_preferences { + prompt.push_str(&format!("色彩偏好: {}\n", colors.join(", "))); + } + + // 添加输出格式要求 + prompt.push_str(&format!(" + +## 输出要求 +请生成 {} 个不同风格的穿搭方案,以JSON格式返回,结构如下: + +```json +{{ + \"recommendations\": [ + {{ + \"id\": \"outfit_001\", + \"title\": \"方案标题\", + \"description\": \"详细的穿搭描述,突出亮点和特色\", + \"overall_style\": \"整体风格\", + \"style_tags\": [\"风格标签1\", \"风格标签2\"], + \"occasions\": [\"适合场合1\", \"适合场合2\"], + \"seasons\": [\"适合季节\"], + \"items\": [ + {{ + \"category\": \"上装\", + \"description\": \"具体单品描述\", + \"primary_color\": {{ + \"name\": \"颜色名称\", + \"hex\": \"#FFFFFF\", + \"hsv\": [0.0, 0.0, 1.0] + }}, + \"secondary_color\": {{ + \"name\": \"次要颜色\", + \"hex\": \"#000000\", + \"hsv\": [0.0, 0.0, 0.0] + }}, + \"material\": \"材质\", + \"style_tags\": [\"单品风格标签\"] + }} + ], + \"color_theme\": \"色彩搭配主题\", + \"primary_colors\": [ + {{ + \"name\": \"主色调名称\", + \"hex\": \"#FFFFFF\", + \"hsv\": [0.0, 0.0, 1.0] + }} + ], + \"scene_recommendations\": [ + {{ + \"name\": \"推荐场景名称\", + \"description\": \"场景详细描述\", + \"scene_type\": \"室内或室外或特殊\", + \"time_of_day\": [\"时间段\"], + \"lighting\": \"光线条件描述\", + \"photography_tips\": [\"拍摄建议1\", \"拍摄建议2\"] + }} + ], + \"tiktok_tips\": [\"TikTok优化建议1\", \"TikTok优化建议2\"], + \"styling_tips\": [\"搭配要点1\", \"搭配要点2\"] + }} + ] +}} +``` + +请确保: +1. 每个方案都有独特的风格定位 +2. 色彩搭配符合当前流行趋势 +3. 场景推荐具有强烈的视觉冲击力 +4. TikTok优化建议实用且具体 +5. 返回的是有效的JSON格式 +", request.count)); + + prompt + } + + /// 解析穿搭方案推荐响应 + fn parse_outfit_recommendations(&self, raw_response: &str, request: &OutfitRecommendationRequest) -> Result> { + println!("🔍 开始解析穿搭方案响应..."); + println!("📄 原始响应: {}", raw_response); + + // 尝试提取JSON部分 + let json_str = self.extract_json_from_response(raw_response)?; + + // 使用容错JSON解析器 + let config = ParserConfig { + max_text_length: 1024 * 1024, + enable_comments: true, + enable_unquoted_keys: true, + enable_trailing_commas: true, + timeout_ms: 30000, + recovery_strategies: vec![ + RecoveryStrategy::StandardJson, + RecoveryStrategy::ManualFix, + RecoveryStrategy::RegexExtract, + RecoveryStrategy::PartialParse, + ], + }; + + let mut parser = TolerantJsonParser::new(Some(config))?; + + match parser.parse(&json_str) { + Ok((parsed, _stats)) => { + if let Some(recommendations_array) = parsed.get("recommendations").and_then(|v| v.as_array()) { + let mut recommendations = Vec::new(); + + for (index, rec_value) in recommendations_array.iter().enumerate() { + match self.parse_single_outfit_recommendation(rec_value, index) { + Ok(recommendation) => recommendations.push(recommendation), + Err(e) => { + println!("⚠️ 解析第{}个穿搭方案失败: {}", index + 1, e); + continue; + } + } + } + + if recommendations.is_empty() { + return Err(anyhow!("未能解析出任何有效的穿搭方案")); + } + + println!("✅ 成功解析 {} 个穿搭方案", recommendations.len()); + Ok(recommendations) + } else { + Err(anyhow!("响应中未找到recommendations数组")) + } + } + Err(e) => { + println!("❌ JSON解析失败: {}", e); + Err(anyhow!("JSON解析失败: {}", e)) + } + } + } + + /// 解析单个穿搭方案 + fn parse_single_outfit_recommendation(&self, value: &serde_json::Value, index: usize) -> Result { + let id = value.get("id") + .and_then(|v| v.as_str()) + .unwrap_or(&format!("outfit_{:03}", index + 1)) + .to_string(); + + let title = value.get("title") + .and_then(|v| v.as_str()) + .unwrap_or("时尚穿搭方案") + .to_string(); + + let description = value.get("description") + .and_then(|v| v.as_str()) + .unwrap_or("精心设计的穿搭方案") + .to_string(); + + let overall_style = value.get("overall_style") + .and_then(|v| v.as_str()) + .unwrap_or("时尚") + .to_string(); + + let style_tags = value.get("style_tags") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) + .unwrap_or_else(|| vec!["时尚".to_string()]); + + let occasions = value.get("occasions") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) + .unwrap_or_else(|| vec!["日常".to_string()]); + + let seasons = value.get("seasons") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) + .unwrap_or_else(|| vec!["四季".to_string()]); + + let color_theme = value.get("color_theme") + .and_then(|v| v.as_str()) + .unwrap_or("和谐搭配") + .to_string(); + + // 解析穿搭单品 + let items = value.get("items") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|item_value| self.parse_outfit_item(item_value).ok()) + .collect() + }) + .unwrap_or_default(); + + // 解析主要色彩 + let primary_colors = value.get("primary_colors") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|color_value| self.parse_color_info(color_value).ok()) + .collect() + }) + .unwrap_or_default(); + + // 解析场景推荐 + let scene_recommendations = value.get("scene_recommendations") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|scene_value| self.parse_scene_recommendation(scene_value).ok()) + .collect() + }) + .unwrap_or_default(); + + let tiktok_tips = value.get("tiktok_tips") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) + .unwrap_or_default(); + + let styling_tips = value.get("styling_tips") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) + .unwrap_or_default(); + + Ok(OutfitRecommendation { + id, + title, + description, + overall_style, + style_tags, + occasions, + seasons, + items, + color_theme, + primary_colors, + scene_recommendations, + tiktok_tips, + styling_tips, + created_at: chrono::Utc::now(), + }) + } + + /// 解析穿搭单品 + fn parse_outfit_item(&self, value: &serde_json::Value) -> Result { + let category = value.get("category") + .and_then(|v| v.as_str()) + .unwrap_or("单品") + .to_string(); + + let description = value.get("description") + .and_then(|v| v.as_str()) + .unwrap_or("时尚单品") + .to_string(); + + let material = value.get("material") + .and_then(|v| v.as_str()) + .unwrap_or("优质面料") + .to_string(); + + let style_tags = value.get("style_tags") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) + .unwrap_or_default(); + + let primary_color = value.get("primary_color") + .and_then(|v| self.parse_color_info(v).ok()) + .unwrap_or_else(|| ColorInfo { + name: "经典色".to_string(), + hex: "#000000".to_string(), + hsv: (0.0, 0.0, 0.0), + }); + + let secondary_color = value.get("secondary_color") + .and_then(|v| self.parse_color_info(v).ok()); + + Ok(OutfitItem { + category, + description, + primary_color, + secondary_color, + material, + style_tags, + }) + } + + /// 解析色彩信息 + fn parse_color_info(&self, value: &serde_json::Value) -> Result { + let name = value.get("name") + .and_then(|v| v.as_str()) + .unwrap_or("颜色") + .to_string(); + + let hex = value.get("hex") + .and_then(|v| v.as_str()) + .unwrap_or("#000000") + .to_string(); + + let hsv = value.get("hsv") + .and_then(|v| v.as_array()) + .and_then(|arr| { + if arr.len() >= 3 { + Some(( + arr[0].as_f64().unwrap_or(0.0) as f32, + arr[1].as_f64().unwrap_or(0.0) as f32, + arr[2].as_f64().unwrap_or(0.0) as f32, + )) + } else { + None + } + }) + .unwrap_or((0.0, 0.0, 0.0)); + + Ok(ColorInfo { name, hex, hsv }) + } + + /// 解析场景推荐 + fn parse_scene_recommendation(&self, value: &serde_json::Value) -> Result { + let name = value.get("name") + .and_then(|v| v.as_str()) + .unwrap_or("推荐场景") + .to_string(); + + let description = value.get("description") + .and_then(|v| v.as_str()) + .unwrap_or("适合拍摄的场景") + .to_string(); + + let scene_type = value.get("scene_type") + .and_then(|v| v.as_str()) + .unwrap_or("室内") + .to_string(); + + let lighting = value.get("lighting") + .and_then(|v| v.as_str()) + .unwrap_or("自然光") + .to_string(); + + let time_of_day = value.get("time_of_day") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) + .unwrap_or_else(|| vec!["全天".to_string()]); + + let photography_tips = value.get("photography_tips") + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) + .unwrap_or_default(); + + Ok(SceneRecommendation { + name, + description, + scene_type, + time_of_day, + lighting, + photography_tips, + }) + } +} \ No newline at end of file diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 447defe..709997c 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -290,6 +290,7 @@ pub fn run() { commands::outfit_search_commands::get_supported_image_formats, commands::outfit_search_commands::get_default_search_config, commands::outfit_search_commands::get_outfit_search_config, + commands::outfit_search_commands::generate_outfit_recommendations, // 相似度检索工具命令 commands::similarity_search_commands::quick_similarity_search, commands::similarity_search_commands::get_similarity_search_suggestions, diff --git a/apps/desktop/src-tauri/src/presentation/commands/outfit_search_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/outfit_search_commands.rs index 4da2916..a20b9da 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/outfit_search_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/outfit_search_commands.rs @@ -7,6 +7,9 @@ use crate::data::models::outfit_search::{ LLMQueryRequest, LLMQueryResponse, OutfitSearchGlobalConfig, ProductInfo, SearchFilterBuilder, SearchRequest, SearchResponse, SearchResult, }; +use crate::data::models::outfit_recommendation::{ + OutfitRecommendationRequest, OutfitRecommendationResponse, +}; use crate::infrastructure::gemini_service::{GeminiConfig, GeminiService}; /// 分析服装图像 @@ -172,6 +175,32 @@ pub async fn get_supported_image_formats( ]) } +/// 生成穿搭方案推荐 +#[tauri::command] +pub async fn generate_outfit_recommendations( + _state: State<'_, AppState>, + request: OutfitRecommendationRequest, +) -> Result { + println!("🎨 收到穿搭方案生成请求: {:?}", request); + + // 创建Gemini服务 + let config = GeminiConfig::default(); + let mut gemini_service = GeminiService::new(Some(config)) + .map_err(|e| format!("Failed to create GeminiService: {}", e))?; + + // 生成穿搭方案 + let response = gemini_service + .generate_outfit_recommendations(&request) + .await + .map_err(|e| { + eprintln!("Failed to generate outfit recommendations: {}", e); + format!("穿搭方案生成失败: {}", e) + })?; + + println!("✅ 穿搭方案生成成功,共生成 {} 个方案", response.recommendations.len()); + Ok(response) +} + /// 获取默认搜索配置 #[tauri::command] pub async fn get_default_search_config( diff --git a/apps/desktop/src/components/outfit/OutfitRecommendationCard.tsx b/apps/desktop/src/components/outfit/OutfitRecommendationCard.tsx new file mode 100644 index 0000000..1ea199a --- /dev/null +++ b/apps/desktop/src/components/outfit/OutfitRecommendationCard.tsx @@ -0,0 +1,292 @@ +import React, { useState, useCallback } from 'react'; +import { + Sparkles, + MapPin, + Clock, + Palette, + Tag, + Camera, + ExternalLink, + ChevronRight, + Sun, + Moon, + Sunrise, + Sunset, +} from 'lucide-react'; +import { OutfitRecommendationCardProps } from '../../types/outfitRecommendation'; + +/** + * 穿搭方案推荐卡片组件 + * 遵循设计系统规范,提供统一的方案展示界面 + */ +export const OutfitRecommendationCard: React.FC = ({ + recommendation, + onSelect, + onSceneSearch, + showDetails = true, + compact = false, + className = '', +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + // 处理卡片点击 + const handleCardClick = useCallback((e: React.MouseEvent) => { + // 如果点击的是按钮或其子元素,不触发卡片选择 + const target = e.target as HTMLElement; + if (target.closest('button')) { + return; + } + + if (onSelect) { + onSelect(recommendation); + } + }, [recommendation, onSelect]); + + // 处理场景检索 + const handleSceneSearch = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + if (onSceneSearch) { + onSceneSearch(recommendation); + } + }, [recommendation, onSceneSearch]); + + // 处理展开/收起 + const handleToggleExpand = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + setIsExpanded(!isExpanded); + }, [isExpanded]); + + // 获取时间段图标 + const getTimeIcon = (timeOfDay: string) => { + switch (timeOfDay.toLowerCase()) { + case '早晨': + case '上午': + return ; + case '中午': + case '下午': + return ; + case '傍晚': + case '黄昏': + return ; + case '晚上': + case '夜晚': + return ; + default: + return ; + } + }; + + return ( +
+ {/* 装饰性背景 */} +
+ + {/* 顶部标识 */} +
+
+ + AI推荐 +
+
+ +
+ {/* 标题和描述 */} +
+

+ {recommendation.title} +

+

+ {recommendation.description} +

+
+ + {/* 风格标签 */} +
+
+ + {recommendation.overall_style} +
+ {recommendation.style_tags.slice(0, 2).map((tag, index) => ( +
+ {tag} +
+ ))} + {recommendation.style_tags.length > 2 && ( +
+ +{recommendation.style_tags.length - 2} +
+ )} +
+ + {/* 主要色彩 */} + {recommendation.primary_colors.length > 0 && ( +
+ +
+ {recommendation.primary_colors.slice(0, 4).map((color, index) => ( +
+ ))} + {recommendation.primary_colors.length > 4 && ( + + +{recommendation.primary_colors.length - 4} + + )} +
+
+ )} + + {/* 适合场合和季节 */} +
+
+ 适合场合: +
+ {recommendation.occasions.slice(0, 2).join('、')} + {recommendation.occasions.length > 2 && '...'} +
+
+
+ 适合季节: +
+ {recommendation.seasons.join('、')} +
+
+
+ + {/* 详细信息 (可展开) */} + {showDetails && ( + <> + {/* 展开/收起按钮 */} + + + {/* 展开的详细内容 */} + {isExpanded && ( +
+ {/* 穿搭单品 */} + {recommendation.items.length > 0 && ( +
+

穿搭单品

+
+ {recommendation.items.map((item, index) => ( +
+
+
+
{item.category}
+
{item.description}
+
+
+ ))} +
+
+ )} + + {/* 场景推荐 */} + {recommendation.scene_recommendations.length > 0 && ( +
+

推荐场景

+
+ {recommendation.scene_recommendations.slice(0, 2).map((scene, index) => ( +
+
+ + {scene.name} + + {scene.scene_type} + +
+

{scene.description}

+ {scene.time_of_day.length > 0 && ( +
+ {scene.time_of_day.slice(0, 3).map((time, timeIndex) => ( +
+ {getTimeIcon(time)} + {time} +
+ ))} +
+ )} +
+ ))} +
+
+ )} + + {/* TikTok优化建议 */} + {recommendation.tiktok_tips.length > 0 && ( +
+

+ + TikTok优化建议 +

+
+ {recommendation.tiktok_tips.slice(0, 3).map((tip, index) => ( +
+
+ {tip} +
+ ))} +
+
+ )} +
+ )} + + )} + + {/* 底部操作按钮 */} +
+
+ {recommendation.color_theme} +
+ + {onSceneSearch && ( + + )} +
+
+
+ ); +}; + +export default OutfitRecommendationCard; diff --git a/apps/desktop/src/components/outfit/OutfitRecommendationList.tsx b/apps/desktop/src/components/outfit/OutfitRecommendationList.tsx new file mode 100644 index 0000000..f1582ef --- /dev/null +++ b/apps/desktop/src/components/outfit/OutfitRecommendationList.tsx @@ -0,0 +1,211 @@ +import React from 'react'; +import { + RefreshCw, + AlertCircle, + Sparkles, + Loader2, + ShoppingBag, +} from 'lucide-react'; +import { OutfitRecommendationListProps } from '../../types/outfitRecommendation'; +import OutfitRecommendationCard from './OutfitRecommendationCard'; +import { EmptyState } from '../EmptyState'; + +/** + * 穿搭方案推荐列表组件 + * 遵循设计系统规范,提供统一的列表展示界面 + */ +export const OutfitRecommendationList: React.FC = ({ + recommendations, + isLoading = false, + error, + onRecommendationSelect, + onSceneSearch, + onRegenerate, + className = '', +}) => { + // 加载状态 + if (isLoading) { + return ( +
+ {/* 加载标题 */} +
+ +
+

+ AI正在生成穿搭方案... +

+

+ 请稍候,我们正在为您精心设计个性化的穿搭推荐 +

+
+
+ + {/* 加载骨架屏 */} +
+ {[1, 2, 3].map((index) => ( +
+
+ {/* 标题骨架 */} +
+ + {/* 描述骨架 */} +
+
+
+
+ + {/* 标签骨架 */} +
+
+
+
+ + {/* 色彩骨架 */} +
+ {[1, 2, 3, 4].map((colorIndex) => ( +
+ ))} +
+ + {/* 按钮骨架 */} +
+
+
+
+
+
+ ))} +
+
+ ); + } + + // 错误状态 + if (error) { + return ( +
+ } + title="生成失败" + description={error} + actionText="重新生成" + onAction={onRegenerate} + illustration="error" + size="md" + className="py-12" + /> +
+ ); + } + + // 空状态 + if (!recommendations || recommendations.length === 0) { + return ( +
+ } + title="暂无穿搭方案" + description="点击上方的 ✨ 图标开始生成个性化穿搭推荐" + actionText="开始生成" + onAction={onRegenerate} + illustration="folder" + size="md" + className="py-12" + showTips={true} + tips={[ + '输入关键词如"休闲"、"正式"、"约会"等', + '描述场合如"工作"、"聚会"、"旅行"等', + '指定风格如"简约"、"复古"、"街头"等', + ]} + /> +
+ ); + } + + // 正常状态 - 显示推荐列表 + return ( +
+ {/* 列表标题和操作 */} +
+
+
+ +
+
+

+ AI穿搭推荐 +

+

+ 共为您生成 {recommendations.length} 个个性化穿搭方案 +

+
+
+ + {onRegenerate && ( + + )} +
+ + {/* 推荐卡片网格 */} +
+ {recommendations.map((recommendation, index) => ( + + ))} +
+ + {/* 底部提示 */} +
+
+
+ +
+
+

💡 使用提示

+
    +
  • +
    + 点击方案卡片查看详细信息和搭配建议 +
  • +
  • +
    + 点击"场景检索"按钮查找适合的拍摄场景 +
  • +
  • +
    + 每个方案都包含TikTok优化建议,助力内容创作 +
  • +
  • +
    + 可以重新生成获得更多不同风格的穿搭方案 +
  • +
+
+
+
+
+ ); +}; + +export default OutfitRecommendationList; diff --git a/apps/desktop/src/components/outfit/OutfitRecommendationModal.tsx b/apps/desktop/src/components/outfit/OutfitRecommendationModal.tsx new file mode 100644 index 0000000..22ec078 --- /dev/null +++ b/apps/desktop/src/components/outfit/OutfitRecommendationModal.tsx @@ -0,0 +1,142 @@ +import React, { useCallback } from 'react'; +import { X, Sparkles } from 'lucide-react'; +import { OutfitRecommendation } from '../../types/outfitRecommendation'; +import OutfitRecommendationList from './OutfitRecommendationList'; + +interface OutfitRecommendationModalProps { + /** 是否显示模态框 */ + isOpen: boolean; + /** 关闭模态框回调 */ + onClose: () => void; + /** 穿搭方案列表 */ + recommendations: OutfitRecommendation[]; + /** 是否正在加载 */ + isLoading?: boolean; + /** 错误信息 */ + error?: string; + /** 方案选择回调 */ + onRecommendationSelect?: (recommendation: OutfitRecommendation) => void; + /** 场景检索回调 */ + onSceneSearch?: (recommendation: OutfitRecommendation) => void; + /** 重新生成回调 */ + onRegenerate?: () => void; + /** 生成查询 */ + query?: string; +} + +/** + * 穿搭方案推荐模态框组件 + * 遵循设计系统规范,提供统一的模态框展示界面 + */ +export const OutfitRecommendationModal: React.FC = ({ + isOpen, + onClose, + recommendations, + isLoading = false, + error, + onRecommendationSelect, + onSceneSearch, + onRegenerate, + query = '', +}) => { + // 处理背景点击关闭 + const handleBackdropClick = useCallback((e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }, [onClose]); + + // 处理ESC键关闭 + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }, [onClose]); + + if (!isOpen) { + return null; + } + + return ( +
+
+ {/* 模态框头部 */} +
+
+
+
+ +
+
+

+ AI穿搭方案推荐 +

+

+ {query ? `基于"${query}"的个性化穿搭推荐` : '个性化穿搭推荐'} +

+
+
+ + +
+
+ + {/* 模态框内容 */} +
+
+ +
+
+ + {/* 模态框底部 (可选) */} + {!isLoading && !error && recommendations.length > 0 && ( +
+
+
+ 共 {recommendations.length} 个推荐方案 +
+ +
+ {onRegenerate && ( + + )} + + +
+
+
+ )} +
+
+ ); +}; + +export default OutfitRecommendationModal; diff --git a/apps/desktop/src/components/similarity/SimilaritySearchPanel.tsx b/apps/desktop/src/components/similarity/SimilaritySearchPanel.tsx index c15a0df..12d4492 100644 --- a/apps/desktop/src/components/similarity/SimilaritySearchPanel.tsx +++ b/apps/desktop/src/components/similarity/SimilaritySearchPanel.tsx @@ -24,6 +24,7 @@ export const SimilaritySearchPanel: React.FC = ({ onSearch, onSuggestionSelect, onSuggestionsToggle, + onOutfitRecommendation, }) => { const inputRef = useRef(null); @@ -41,6 +42,16 @@ export const SimilaritySearchPanel: React.FC = ({ onSearch(request); }, [query, selectedThreshold, onSearch]); + // 处理穿搭方案生成 + const handleOutfitRecommendation = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (onOutfitRecommendation) { + onOutfitRecommendation(); + } + }, [onOutfitRecommendation]); + // 处理回车键搜索 const handleKeyPress = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -105,9 +116,15 @@ export const SimilaritySearchPanel: React.FC = ({ className="form-input pr-12 group-hover:border-primary-300 focus:border-primary-500 transition-colors duration-200" disabled={isSearching} /> -
+
+
)} + + {/* 穿搭方案推荐模态框 */} + ); }; diff --git a/apps/desktop/src/services/outfitRecommendationService.ts b/apps/desktop/src/services/outfitRecommendationService.ts new file mode 100644 index 0000000..64c0d6e --- /dev/null +++ b/apps/desktop/src/services/outfitRecommendationService.ts @@ -0,0 +1,278 @@ +/** + * 穿搭方案推荐服务 + * 遵循 Tauri 开发规范的服务层设计 + */ + +import { invoke } from '@tauri-apps/api/core'; +import { + OutfitRecommendationRequest, + OutfitRecommendationResponse, + OutfitRecommendation, + SceneSearchRequest, + DEFAULT_OUTFIT_RECOMMENDATION_REQUEST, +} from '../types/outfitRecommendation'; + +export class OutfitRecommendationService { + /** + * 生成穿搭方案推荐 + */ + static async generateRecommendations( + request: Partial + ): Promise { + try { + const fullRequest: OutfitRecommendationRequest = { + ...DEFAULT_OUTFIT_RECOMMENDATION_REQUEST, + ...request, + } as OutfitRecommendationRequest; + + console.log('🎨 发送穿搭方案生成请求:', fullRequest); + + const response = await invoke( + 'generate_outfit_recommendations', + { request: fullRequest } + ); + + console.log('✅ 穿搭方案生成成功:', response); + return response; + } catch (error) { + console.error('❌ 穿搭方案生成失败:', error); + throw new Error(`穿搭方案生成失败: ${error}`); + } + } + + /** + * 基于查询快速生成推荐 + */ + static async quickGenerate( + query: string, + count: number = 3 + ): Promise { + return this.generateRecommendations({ + query, + count, + }); + } + + /** + * 基于风格生成推荐 + */ + static async generateByStyle( + query: string, + targetStyle: string, + count: number = 3 + ): Promise { + return this.generateRecommendations({ + query, + target_style: targetStyle, + count, + }); + } + + /** + * 基于场合生成推荐 + */ + static async generateByOccasion( + query: string, + occasions: string[], + count: number = 3 + ): Promise { + return this.generateRecommendations({ + query, + occasions, + count, + }); + } + + /** + * 基于季节生成推荐 + */ + static async generateBySeason( + query: string, + season: string, + count: number = 3 + ): Promise { + return this.generateRecommendations({ + query, + season, + count, + }); + } + + /** + * 基于色彩偏好生成推荐 + */ + static async generateByColors( + query: string, + colorPreferences: string[], + count: number = 3 + ): Promise { + return this.generateRecommendations({ + query, + color_preferences: colorPreferences, + count, + }); + } + + /** + * 生成场景检索请求 + * 用于将穿搭方案转换为场景检索参数 + */ + static generateSceneSearchRequest( + recommendation: OutfitRecommendation, + searchParams?: Partial + ): SceneSearchRequest { + // 生成场景描述 + const sceneDescription = this.generateSceneDescription(recommendation); + + // 获取所有风格标签 + const styleTags = this.getAllStyleTags(recommendation); + + return { + outfit_id: recommendation.id, + scene_description: sceneDescription, + style_tags: styleTags, + colors: recommendation.primary_colors, + search_params: { + scene_types: recommendation.scene_recommendations.map(s => s.scene_type), + time_filters: recommendation.scene_recommendations.flatMap(s => s.time_of_day), + lighting_filters: recommendation.scene_recommendations.map(s => s.lighting), + limit: 20, + ...searchParams, + }, + }; + } + + /** + * 生成场景描述 + */ + private static generateSceneDescription(recommendation: OutfitRecommendation): string { + let description = `${recommendation.title} - ${recommendation.description}`; + + if (recommendation.style_tags.length > 0) { + description += ` 风格: ${recommendation.style_tags.join(', ')}`; + } + + if (recommendation.primary_colors.length > 0) { + const colorNames = recommendation.primary_colors.map(c => c.name); + description += ` 主要色彩: ${colorNames.join(', ')}`; + } + + if (recommendation.occasions.length > 0) { + description += ` 适合场合: ${recommendation.occasions.join(', ')}`; + } + + return description; + } + + /** + * 获取所有风格标签 + */ + private static getAllStyleTags(recommendation: OutfitRecommendation): string[] { + const allTags = [...recommendation.style_tags]; + + // 添加整体风格 + if (recommendation.overall_style) { + allTags.push(recommendation.overall_style); + } + + // 添加单品风格标签 + recommendation.items.forEach(item => { + allTags.push(...item.style_tags); + }); + + // 去重并排序 + return [...new Set(allTags)].sort(); + } + + /** + * 格式化色彩信息 + */ + static formatColorInfo(color: { name: string; hex: string }): string { + return `${color.name} (${color.hex})`; + } + + /** + * 检查方案是否适合指定季节 + */ + static isSeasonAppropriate(recommendation: OutfitRecommendation, season: string): boolean { + return recommendation.seasons.includes(season); + } + + /** + * 检查方案是否适合指定场合 + */ + static isOccasionAppropriate(recommendation: OutfitRecommendation, occasion: string): boolean { + return recommendation.occasions.includes(occasion); + } + + /** + * 获取推荐的主要色彩名称 + */ + static getPrimaryColorNames(recommendation: OutfitRecommendation): string[] { + return recommendation.primary_colors.map(color => color.name); + } + + /** + * 获取推荐的TikTok优化建议摘要 + */ + static getTikTokTipsSummary(recommendation: OutfitRecommendation): string { + if (recommendation.tiktok_tips.length === 0) { + return '暂无TikTok优化建议'; + } + + if (recommendation.tiktok_tips.length === 1) { + return recommendation.tiktok_tips[0]; + } + + return `${recommendation.tiktok_tips[0]}等${recommendation.tiktok_tips.length}条建议`; + } + + /** + * 获取推荐的场景类型摘要 + */ + static getSceneTypesSummary(recommendation: OutfitRecommendation): string { + if (recommendation.scene_recommendations.length === 0) { + return '暂无场景推荐'; + } + + const sceneTypes = [...new Set(recommendation.scene_recommendations.map(s => s.scene_type))]; + return sceneTypes.join('、'); + } + + /** + * 验证推荐数据的完整性 + */ + static validateRecommendation(recommendation: OutfitRecommendation): { + isValid: boolean; + errors: string[]; + } { + const errors: string[] = []; + + if (!recommendation.id) { + errors.push('缺少方案ID'); + } + + if (!recommendation.title) { + errors.push('缺少方案标题'); + } + + if (!recommendation.description) { + errors.push('缺少方案描述'); + } + + if (recommendation.items.length === 0) { + errors.push('缺少穿搭单品'); + } + + if (recommendation.primary_colors.length === 0) { + errors.push('缺少主要色彩信息'); + } + + return { + isValid: errors.length === 0, + errors, + }; + } +} + +export default OutfitRecommendationService; diff --git a/apps/desktop/src/types/outfitRecommendation.ts b/apps/desktop/src/types/outfitRecommendation.ts new file mode 100644 index 0000000..e28c425 --- /dev/null +++ b/apps/desktop/src/types/outfitRecommendation.ts @@ -0,0 +1,297 @@ +/** + * 穿搭方案推荐相关类型定义 + * 遵循 Tauri 开发规范的类型安全设计 + */ + +// 色彩信息 +export interface ColorInfo { + /** 色彩名称 */ + name: string; + /** 十六进制色值 */ + hex: string; + /** HSV值 (0-1范围) */ + hsv: [number, number, number]; +} + +// 穿搭单品 +export interface OutfitItem { + /** 单品类别 (如: "上装", "下装", "鞋子", "配饰") */ + category: string; + /** 单品描述 */ + description: string; + /** 主要颜色 */ + primary_color: ColorInfo; + /** 次要颜色 (可选) */ + secondary_color?: ColorInfo; + /** 材质 */ + material: string; + /** 风格标签 */ + style_tags: string[]; +} + +// 场景建议 +export interface SceneRecommendation { + /** 场景名称 */ + name: string; + /** 场景描述 */ + description: string; + /** 场景类型 ("室内", "室外", "特殊") */ + scene_type: string; + /** 适合的时间段 */ + time_of_day: string[]; + /** 光线条件 */ + lighting: string; + /** 拍摄建议 */ + photography_tips: string[]; +} + +// 穿搭方案 +export interface OutfitRecommendation { + /** 方案ID */ + id: string; + /** 方案标题 */ + title: string; + /** 方案描述 */ + description: string; + /** 整体风格 */ + overall_style: string; + /** 风格标签 */ + style_tags: string[]; + /** 适合场合 */ + occasions: string[]; + /** 适合季节 */ + seasons: string[]; + /** 穿搭单品列表 */ + items: OutfitItem[]; + /** 色彩搭配主题 */ + color_theme: string; + /** 主要色彩 */ + primary_colors: ColorInfo[]; + /** 场景建议 */ + scene_recommendations: SceneRecommendation[]; + /** TikTok优化建议 */ + tiktok_tips: string[]; + /** 搭配要点 */ + styling_tips: string[]; + /** 创建时间 */ + created_at: string; +} + +// 穿搭方案生成请求 +export interface OutfitRecommendationRequest { + /** 用户输入的关键词或描述 */ + query: string; + /** 目标风格 (可选) */ + target_style?: string; + /** 适合场合 (可选) */ + occasions?: string[]; + /** 季节偏好 (可选) */ + season?: string; + /** 色彩偏好 (可选) */ + color_preferences?: string[]; + /** 生成数量 */ + count: number; +} + +// 穿搭方案生成响应 +export interface OutfitRecommendationResponse { + /** 生成的穿搭方案列表 */ + recommendations: OutfitRecommendation[]; + /** 生成时间 (毫秒) */ + generation_time_ms: number; + /** 生成时间戳 */ + generated_at: string; + /** 使用的提示词 (调试用) */ + prompt_used?: string; +} + +// 场景检索请求 (用于方案详情到场景检索的集成) +export interface SceneSearchRequest { + /** 穿搭方案ID */ + outfit_id: string; + /** 场景描述 */ + scene_description: string; + /** 风格标签 */ + style_tags: string[]; + /** 色彩信息 */ + colors: ColorInfo[]; + /** 搜索参数 */ + search_params: SceneSearchParams; +} + +// 场景检索参数 +export interface SceneSearchParams { + /** 场景类型过滤 */ + scene_types?: string[]; + /** 时间段过滤 */ + time_filters?: string[]; + /** 光线条件过滤 */ + lighting_filters?: string[]; + /** 结果数量限制 */ + limit?: number; +} + +// 穿搭方案卡片组件属性 +export interface OutfitRecommendationCardProps { + /** 穿搭方案数据 */ + recommendation: OutfitRecommendation; + /** 点击事件处理 */ + onSelect?: (recommendation: OutfitRecommendation) => void; + /** 场景检索事件处理 */ + onSceneSearch?: (recommendation: OutfitRecommendation) => void; + /** 是否显示详细信息 */ + showDetails?: boolean; + /** 是否紧凑模式 */ + compact?: boolean; + /** 自定义样式类名 */ + className?: string; +} + +// 穿搭方案列表组件属性 +export interface OutfitRecommendationListProps { + /** 穿搭方案列表 */ + recommendations: OutfitRecommendation[]; + /** 是否正在加载 */ + isLoading?: boolean; + /** 错误信息 */ + error?: string; + /** 卡片选择事件 */ + onRecommendationSelect?: (recommendation: OutfitRecommendation) => void; + /** 场景检索事件 */ + onSceneSearch?: (recommendation: OutfitRecommendation) => void; + /** 重新生成事件 */ + onRegenerate?: () => void; + /** 自定义样式类名 */ + className?: string; +} + +// 穿搭方案生成状态 +export interface OutfitRecommendationState { + /** 当前查询 */ + query: string; + /** 生成的方案列表 */ + recommendations: OutfitRecommendation[]; + /** 是否正在生成 */ + isGenerating: boolean; + /** 错误信息 */ + error: string | null; + /** 生成历史 */ + history: OutfitRecommendationResponse[]; +} + +// 默认请求参数 +export const DEFAULT_OUTFIT_RECOMMENDATION_REQUEST: Partial = { + count: 3, + target_style: undefined, + occasions: undefined, + season: undefined, + color_preferences: undefined, +}; + +// 常用风格选项 +export const STYLE_OPTIONS = [ + '休闲', + '正式', + '运动', + '街头', + '简约', + '复古', + '甜美', + '酷帅', + '文艺', + '优雅', +] as const; + +// 常用场合选项 +export const OCCASION_OPTIONS = [ + '日常', + '工作', + '约会', + '聚会', + '旅行', + '运动', + '正式场合', + '休闲娱乐', +] as const; + +// 季节选项 +export const SEASON_OPTIONS = [ + '春季', + '夏季', + '秋季', + '冬季', +] as const; + +// 色彩偏好选项 +export const COLOR_PREFERENCE_OPTIONS = [ + '暖色调', + '冷色调', + '中性色', + '亮色', + '深色', + '浅色', + '单色', + '撞色', +] as const; + +// 工具函数 +export const OutfitRecommendationUtils = { + /** + * 生成用于场景检索的描述 + */ + generateSceneSearchDescription(recommendation: OutfitRecommendation): string { + let description = `${recommendation.title} - ${recommendation.description}`; + + if (recommendation.style_tags.length > 0) { + description += ` 风格: ${recommendation.style_tags.join(', ')}`; + } + + if (recommendation.primary_colors.length > 0) { + const colorNames = recommendation.primary_colors.map(c => c.name); + description += ` 主要色彩: ${colorNames.join(', ')}`; + } + + return description; + }, + + /** + * 获取所有风格标签 + */ + getAllStyleTags(recommendation: OutfitRecommendation): string[] { + const allTags = [...recommendation.style_tags]; + + // 添加整体风格 + if (recommendation.overall_style) { + allTags.push(recommendation.overall_style); + } + + // 添加单品风格标签 + recommendation.items.forEach(item => { + allTags.push(...item.style_tags); + }); + + // 去重并排序 + return [...new Set(allTags)].sort(); + }, + + /** + * 格式化色彩信息为显示文本 + */ + formatColorInfo(color: ColorInfo): string { + return `${color.name} (${color.hex})`; + }, + + /** + * 检查方案是否适合指定季节 + */ + isSeasonAppropriate(recommendation: OutfitRecommendation, season: string): boolean { + return recommendation.seasons.includes(season); + }, + + /** + * 检查方案是否适合指定场合 + */ + isOccasionAppropriate(recommendation: OutfitRecommendation, occasion: string): boolean { + return recommendation.occasions.includes(occasion); + }, +}; diff --git a/apps/desktop/src/types/similaritySearch.ts b/apps/desktop/src/types/similaritySearch.ts index 140b94c..b6980c4 100644 --- a/apps/desktop/src/types/similaritySearch.ts +++ b/apps/desktop/src/types/similaritySearch.ts @@ -77,6 +77,7 @@ export interface SimilaritySearchPanelProps { onSearch: (request: SimilaritySearchRequest) => void; onSuggestionSelect: (suggestion: string) => void; onSuggestionsToggle: (show: boolean) => void; + onOutfitRecommendation?: () => void; // 穿搭方案生成回调 } export interface SimilaritySearchResultsProps {