feat: 实现AI穿搭方案推荐功能

- 新增穿搭方案推荐数据模型和类型定义
- 实现基于TikTok视觉趋势的Gemini AI穿搭方案生成
- 创建穿搭方案卡片和列表展示组件
- 集成Sparkles图标点击触发穿搭方案生成
- 实现穿搭方案到场景检索的无缝集成
- 添加完整的前后端API和服务层
- 遵循promptx开发规范和设计系统标准

功能特点:
- 基于用户输入关键词生成个性化穿搭推荐
- 包含色彩搭配、风格标签、场景建议等详细信息
- 提供TikTok优化建议和拍摄技巧
- 支持一键场景检索功能
- 美观的卡片式展示界面
- 完整的加载状态和错误处理
This commit is contained in:
imeepos
2025-07-25 10:38:11 +08:00
parent 9d1f962853
commit c4bb073507
13 changed files with 2027 additions and 2 deletions

View File

@@ -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;

View File

@@ -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<ColorInfo>,
/// 材质
pub material: String,
/// 风格标签
pub style_tags: Vec<String>,
}
/// 场景建议
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneRecommendation {
/// 场景名称
pub name: String,
/// 场景描述
pub description: String,
/// 场景类型 ("室内", "室外", "特殊")
pub scene_type: String,
/// 适合的时间段
pub time_of_day: Vec<String>,
/// 光线条件
pub lighting: String,
/// 拍摄建议
pub photography_tips: Vec<String>,
}
/// 穿搭方案
#[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<String>,
/// 适合场合
pub occasions: Vec<String>,
/// 适合季节
pub seasons: Vec<String>,
/// 穿搭单品列表
pub items: Vec<OutfitItem>,
/// 色彩搭配主题
pub color_theme: String,
/// 主要色彩
pub primary_colors: Vec<ColorInfo>,
/// 场景建议
pub scene_recommendations: Vec<SceneRecommendation>,
/// TikTok优化建议
pub tiktok_tips: Vec<String>,
/// 搭配要点
pub styling_tips: Vec<String>,
/// 创建时间
pub created_at: DateTime<Utc>,
}
/// 穿搭方案生成请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitRecommendationRequest {
/// 用户输入的关键词或描述
pub query: String,
/// 目标风格 (可选)
pub target_style: Option<String>,
/// 适合场合 (可选)
pub occasions: Option<Vec<String>>,
/// 季节偏好 (可选)
pub season: Option<String>,
/// 色彩偏好 (可选)
pub color_preferences: Option<Vec<String>>,
/// 生成数量
pub count: u32,
}
/// 穿搭方案生成响应
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitRecommendationResponse {
/// 生成的穿搭方案列表
pub recommendations: Vec<OutfitRecommendation>,
/// 生成时间 (毫秒)
pub generation_time_ms: u64,
/// 生成时间戳
pub generated_at: DateTime<Utc>,
/// 使用的提示词 (调试用)
pub prompt_used: Option<String>,
}
/// 场景检索请求 (用于方案详情到场景检索的集成)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneSearchRequest {
/// 穿搭方案ID
pub outfit_id: String,
/// 场景描述
pub scene_description: String,
/// 风格标签
pub style_tags: Vec<String>,
/// 色彩信息
pub colors: Vec<ColorInfo>,
/// 搜索参数
pub search_params: SceneSearchParams,
}
/// 场景检索参数
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneSearchParams {
/// 场景类型过滤
pub scene_types: Option<Vec<String>>,
/// 时间段过滤
pub time_filters: Option<Vec<String>>,
/// 光线条件过滤
pub lighting_filters: Option<Vec<String>>,
/// 结果数量限制
pub limit: Option<u32>,
}
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<String> = 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<String> {
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
}
}

View File

@@ -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<OutfitRecommendationResponse> {
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<Vec<OutfitRecommendation>> {
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<OutfitRecommendation> {
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<OutfitItem> {
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<ColorInfo> {
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<SceneRecommendation> {
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,
})
}
}

View File

@@ -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,

View File

@@ -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<OutfitRecommendationResponse, String> {
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(

View File

@@ -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<OutfitRecommendationCardProps> = ({
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 <Sunrise className="w-3 h-3" />;
case '中午':
case '下午':
return <Sun className="w-3 h-3" />;
case '傍晚':
case '黄昏':
return <Sunset className="w-3 h-3" />;
case '晚上':
case '夜晚':
return <Moon className="w-3 h-3" />;
default:
return <Clock className="w-3 h-3" />;
}
};
return (
<div
className={`
card card-interactive group cursor-pointer animate-fade-in-up
relative overflow-hidden bg-gradient-to-br from-white to-gray-50/50
hover:from-white hover:to-primary-50/30 transition-all duration-500
hover:shadow-lg hover:shadow-primary-500/10 hover:-translate-y-1
border border-gray-200 hover:border-primary-300
${compact ? 'p-4' : 'p-6'}
${className}
`}
onClick={handleCardClick}
>
{/* 装饰性背景 */}
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-br from-primary-100 to-primary-200 rounded-full -translate-y-12 translate-x-12 opacity-40 group-hover:opacity-60 transition-all duration-500 group-hover:scale-110"></div>
{/* 顶部标识 */}
<div className="absolute top-3 right-3 flex items-center gap-2">
<div className="flex items-center gap-1 px-2 py-1 bg-primary-100 text-primary-700 rounded-full text-xs font-medium">
<Sparkles className="w-3 h-3" />
AI推荐
</div>
</div>
<div className="relative">
{/* 标题和描述 */}
<div className="mb-4">
<h3 className="text-lg font-bold text-high-emphasis mb-2 group-hover:text-primary-600 transition-colors duration-200">
{recommendation.title}
</h3>
<p className="text-sm text-medium-emphasis line-clamp-2">
{recommendation.description}
</p>
</div>
{/* 风格标签 */}
<div className="flex flex-wrap gap-2 mb-4">
<div className="flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-700 rounded-full text-xs font-medium">
<Tag className="w-3 h-3" />
{recommendation.overall_style}
</div>
{recommendation.style_tags.slice(0, 2).map((tag, index) => (
<div
key={index}
className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs"
>
{tag}
</div>
))}
{recommendation.style_tags.length > 2 && (
<div className="px-2 py-1 bg-gray-100 text-gray-500 rounded-full text-xs">
+{recommendation.style_tags.length - 2}
</div>
)}
</div>
{/* 主要色彩 */}
{recommendation.primary_colors.length > 0 && (
<div className="flex items-center gap-2 mb-4">
<Palette className="w-4 h-4 text-gray-500" />
<div className="flex items-center gap-2">
{recommendation.primary_colors.slice(0, 4).map((color, index) => (
<div
key={index}
className="w-6 h-6 rounded-full border-2 border-white shadow-sm"
style={{ backgroundColor: color.hex }}
title={color.name}
/>
))}
{recommendation.primary_colors.length > 4 && (
<span className="text-xs text-gray-500">
+{recommendation.primary_colors.length - 4}
</span>
)}
</div>
</div>
)}
{/* 适合场合和季节 */}
<div className="grid grid-cols-2 gap-4 mb-4 text-xs text-medium-emphasis">
<div>
<span className="font-medium">:</span>
<div className="mt-1">
{recommendation.occasions.slice(0, 2).join('、')}
{recommendation.occasions.length > 2 && '...'}
</div>
</div>
<div>
<span className="font-medium">:</span>
<div className="mt-1">
{recommendation.seasons.join('、')}
</div>
</div>
</div>
{/* 详细信息 (可展开) */}
{showDetails && (
<>
{/* 展开/收起按钮 */}
<button
onClick={handleToggleExpand}
className="flex items-center gap-2 w-full text-left text-sm text-primary-600 hover:text-primary-700 mb-3 transition-colors duration-200"
>
<span>{isExpanded ? '收起详情' : '查看详情'}</span>
<ChevronRight
className={`w-4 h-4 transition-transform duration-200 ${
isExpanded ? 'rotate-90' : ''
}`}
/>
</button>
{/* 展开的详细内容 */}
{isExpanded && (
<div className="space-y-4 animate-fade-in">
{/* 穿搭单品 */}
{recommendation.items.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-high-emphasis mb-2">穿</h4>
<div className="space-y-2">
{recommendation.items.map((item, index) => (
<div key={index} className="flex items-center gap-3 p-2 bg-gray-50 rounded-lg">
<div
className="w-4 h-4 rounded-full border border-gray-300"
style={{ backgroundColor: item.primary_color.hex }}
/>
<div className="flex-1">
<div className="text-sm font-medium">{item.category}</div>
<div className="text-xs text-gray-600">{item.description}</div>
</div>
</div>
))}
</div>
</div>
)}
{/* 场景推荐 */}
{recommendation.scene_recommendations.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-high-emphasis mb-2"></h4>
<div className="space-y-2">
{recommendation.scene_recommendations.slice(0, 2).map((scene, index) => (
<div key={index} className="p-2 bg-blue-50 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<MapPin className="w-3 h-3 text-blue-600" />
<span className="text-sm font-medium text-blue-900">{scene.name}</span>
<span className="text-xs text-blue-600 bg-blue-100 px-2 py-0.5 rounded">
{scene.scene_type}
</span>
</div>
<p className="text-xs text-blue-700 mb-2">{scene.description}</p>
{scene.time_of_day.length > 0 && (
<div className="flex items-center gap-2">
{scene.time_of_day.slice(0, 3).map((time, timeIndex) => (
<div
key={timeIndex}
className="flex items-center gap-1 text-xs text-blue-600"
>
{getTimeIcon(time)}
<span>{time}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* TikTok优化建议 */}
{recommendation.tiktok_tips.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-high-emphasis mb-2 flex items-center gap-2">
<Camera className="w-4 h-4" />
TikTok优化建议
</h4>
<div className="space-y-1">
{recommendation.tiktok_tips.slice(0, 3).map((tip, index) => (
<div key={index} className="text-xs text-gray-600 flex items-start gap-2">
<div className="w-1 h-1 bg-primary-500 rounded-full mt-2 flex-shrink-0" />
<span>{tip}</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</>
)}
{/* 底部操作按钮 */}
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<div className="text-xs text-gray-500">
{recommendation.color_theme}
</div>
{onSceneSearch && (
<button
onClick={handleSceneSearch}
className="flex items-center gap-2 px-3 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors duration-200 text-sm font-medium"
>
<MapPin className="w-4 h-4" />
<ExternalLink className="w-3 h-3" />
</button>
)}
</div>
</div>
</div>
);
};
export default OutfitRecommendationCard;

View File

@@ -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<OutfitRecommendationListProps> = ({
recommendations,
isLoading = false,
error,
onRecommendationSelect,
onSceneSearch,
onRegenerate,
className = '',
}) => {
// 加载状态
if (isLoading) {
return (
<div className={`space-y-6 ${className}`}>
{/* 加载标题 */}
<div className="flex items-center justify-center gap-3 p-6">
<Loader2 className="w-6 h-6 text-primary-500 animate-spin" />
<div className="text-center">
<h3 className="text-lg font-semibold text-high-emphasis mb-1">
AI正在生成穿搭方案...
</h3>
<p className="text-sm text-medium-emphasis">
穿
</p>
</div>
</div>
{/* 加载骨架屏 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3].map((index) => (
<div
key={index}
className="card p-6 animate-pulse"
>
<div className="space-y-4">
{/* 标题骨架 */}
<div className="h-6 bg-gray-200 rounded-lg w-3/4"></div>
{/* 描述骨架 */}
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded w-full"></div>
<div className="h-4 bg-gray-200 rounded w-2/3"></div>
</div>
{/* 标签骨架 */}
<div className="flex gap-2">
<div className="h-6 bg-gray-200 rounded-full w-16"></div>
<div className="h-6 bg-gray-200 rounded-full w-12"></div>
</div>
{/* 色彩骨架 */}
<div className="flex gap-2">
{[1, 2, 3, 4].map((colorIndex) => (
<div
key={colorIndex}
className="w-6 h-6 bg-gray-200 rounded-full"
></div>
))}
</div>
{/* 按钮骨架 */}
<div className="flex justify-between items-center pt-4 border-t border-gray-100">
<div className="h-4 bg-gray-200 rounded w-20"></div>
<div className="h-8 bg-gray-200 rounded-lg w-24"></div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
// 错误状态
if (error) {
return (
<div className={`${className}`}>
<EmptyState
variant="error"
icon={<AlertCircle className="w-12 h-12 text-red-500" />}
title="生成失败"
description={error}
actionText="重新生成"
onAction={onRegenerate}
illustration="error"
size="md"
className="py-12"
/>
</div>
);
}
// 空状态
if (!recommendations || recommendations.length === 0) {
return (
<div className={`${className}`}>
<EmptyState
variant="default"
icon={<ShoppingBag className="w-12 h-12 text-gray-400" />}
title="暂无穿搭方案"
description="点击上方的 ✨ 图标开始生成个性化穿搭推荐"
actionText="开始生成"
onAction={onRegenerate}
illustration="folder"
size="md"
className="py-12"
showTips={true}
tips={[
'输入关键词如"休闲"、"正式"、"约会"等',
'描述场合如"工作"、"聚会"、"旅行"等',
'指定风格如"简约"、"复古"、"街头"等',
]}
/>
</div>
);
}
// 正常状态 - 显示推荐列表
return (
<div className={`space-y-6 ${className}`}>
{/* 列表标题和操作 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="icon-container primary w-8 h-8">
<Sparkles className="w-4 h-4" />
</div>
<div>
<h3 className="text-lg font-semibold text-high-emphasis">
AI穿搭推荐
</h3>
<p className="text-sm text-medium-emphasis">
{recommendations.length} 穿
</p>
</div>
</div>
{onRegenerate && (
<button
onClick={onRegenerate}
className="flex items-center gap-2 px-4 py-2 text-primary-600 hover:text-primary-700 hover:bg-primary-50 rounded-lg transition-all duration-200"
>
<RefreshCw className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</button>
)}
</div>
{/* 推荐卡片网格 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{recommendations.map((recommendation, index) => (
<OutfitRecommendationCard
key={recommendation.id || index}
recommendation={recommendation}
onSelect={onRecommendationSelect}
onSceneSearch={onSceneSearch}
showDetails={true}
compact={false}
className="animate-fade-in-up"
/>
))}
</div>
{/* 底部提示 */}
<div className="card p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200">
<div className="flex items-start gap-3">
<div className="icon-container blue w-6 h-6 mt-0.5 shadow-sm">
<Sparkles className="w-3 h-3" />
</div>
<div className="flex-1">
<h4 className="text-sm font-semibold text-blue-900 mb-2">💡 使</h4>
<ul className="text-xs text-blue-700 space-y-1.5">
<li className="flex items-center gap-2">
<div className="w-1 h-1 bg-blue-400 rounded-full"></div>
</li>
<li className="flex items-center gap-2">
<div className="w-1 h-1 bg-blue-400 rounded-full"></div>
"场景检索"
</li>
<li className="flex items-center gap-2">
<div className="w-1 h-1 bg-blue-400 rounded-full"></div>
TikTok优化建议
</li>
<li className="flex items-center gap-2">
<div className="w-1 h-1 bg-blue-400 rounded-full"></div>
穿
</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default OutfitRecommendationList;

View File

@@ -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<OutfitRecommendationModalProps> = ({
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 (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm animate-fade-in"
onClick={handleBackdropClick}
onKeyDown={handleKeyDown}
tabIndex={-1}
>
<div className="relative w-full max-w-7xl max-h-[90vh] mx-4 bg-white rounded-2xl shadow-2xl animate-scale-in overflow-hidden">
{/* 模态框头部 */}
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="icon-container primary w-10 h-10">
<Sparkles className="w-5 h-5" />
</div>
<div>
<h2 className="text-xl font-bold text-high-emphasis">
AI穿搭方案推荐
</h2>
<p className="text-sm text-medium-emphasis">
{query ? `基于"${query}"的个性化穿搭推荐` : '个性化穿搭推荐'}
</p>
</div>
</div>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors duration-200"
aria-label="关闭"
>
<X className="w-6 h-6" />
</button>
</div>
</div>
{/* 模态框内容 */}
<div className="overflow-y-auto max-h-[calc(90vh-80px)]">
<div className="p-6">
<OutfitRecommendationList
recommendations={recommendations}
isLoading={isLoading}
error={error}
onRecommendationSelect={onRecommendationSelect}
onSceneSearch={onSceneSearch}
onRegenerate={onRegenerate}
/>
</div>
</div>
{/* 模态框底部 (可选) */}
{!isLoading && !error && recommendations.length > 0 && (
<div className="sticky bottom-0 bg-gradient-to-t from-white to-transparent px-6 py-4 border-t border-gray-100">
<div className="flex items-center justify-between">
<div className="text-sm text-medium-emphasis">
{recommendations.length}
</div>
<div className="flex items-center gap-3">
{onRegenerate && (
<button
onClick={onRegenerate}
className="btn btn-outline btn-sm"
>
</button>
)}
<button
onClick={onClose}
className="btn btn-primary btn-sm"
>
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default OutfitRecommendationModal;

View File

@@ -24,6 +24,7 @@ export const SimilaritySearchPanel: React.FC<SimilaritySearchPanelProps> = ({
onSearch,
onSuggestionSelect,
onSuggestionsToggle,
onOutfitRecommendation,
}) => {
const inputRef = useRef<HTMLInputElement>(null);
@@ -41,6 +42,16 @@ export const SimilaritySearchPanel: React.FC<SimilaritySearchPanelProps> = ({
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<SimilaritySearchPanelProps> = ({
className="form-input pr-12 group-hover:border-primary-300 focus:border-primary-500 transition-colors duration-200"
disabled={isSearching}
/>
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 group-hover:text-primary-500 transition-colors duration-200">
<button
type="button"
onClick={handleOutfitRecommendation}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-primary-500 transition-colors duration-200 p-1 rounded-full hover:bg-primary-50"
title="生成AI穿搭方案推荐"
disabled={isSearching}
>
<Sparkles className="w-5 h-5" />
</div>
</button>
</div>
<button

View File

@@ -19,6 +19,9 @@ import {
import { SimilaritySearchPanel } from '../../components/similarity/SimilaritySearchPanel';
import SimilaritySearchResults from '../../components/similarity/SimilaritySearchResults';
import { SimilaritySearchRequest } from '../../types/similaritySearch';
import OutfitRecommendationModal from '../../components/outfit/OutfitRecommendationModal';
import OutfitRecommendationService from '../../services/outfitRecommendationService';
import { OutfitRecommendation } from '../../types/outfitRecommendation';
/**
* 相似度检索工具页面
@@ -31,6 +34,12 @@ const SimilaritySearchTool: React.FC = () => {
const [selectedResult, setSelectedResult] = useState<any>(null);
const [showDetailModal, setShowDetailModal] = useState(false);
// 穿搭方案推荐状态
const [showOutfitModal, setShowOutfitModal] = useState(false);
const [outfitRecommendations, setOutfitRecommendations] = useState<OutfitRecommendation[]>([]);
const [isGeneratingOutfits, setIsGeneratingOutfits] = useState(false);
const [outfitError, setOutfitError] = useState<string | null>(null);
// 状态管理
const {
query,
@@ -142,6 +151,83 @@ const SimilaritySearchTool: React.FC = () => {
navigate('/tools');
}, [navigate]);
// 处理穿搭方案生成
const handleOutfitRecommendation = useCallback(async () => {
if (!query.trim()) {
setOutfitError('请先输入搜索关键词');
return;
}
setIsGeneratingOutfits(true);
setOutfitError(null);
setShowOutfitModal(true);
try {
const response = await OutfitRecommendationService.quickGenerate(query.trim(), 3);
setOutfitRecommendations(response.recommendations);
} catch (error) {
console.error('穿搭方案生成失败:', error);
setOutfitError(error instanceof Error ? error.message : '穿搭方案生成失败');
} finally {
setIsGeneratingOutfits(false);
}
}, [query]);
// 处理穿搭方案重新生成
const handleOutfitRegenerate = useCallback(async () => {
if (!query.trim()) {
return;
}
setIsGeneratingOutfits(true);
setOutfitError(null);
try {
const response = await OutfitRecommendationService.quickGenerate(query.trim(), 3);
setOutfitRecommendations(response.recommendations);
} catch (error) {
console.error('穿搭方案重新生成失败:', error);
setOutfitError(error instanceof Error ? error.message : '穿搭方案生成失败');
} finally {
setIsGeneratingOutfits(false);
}
}, [query]);
// 处理穿搭方案选择
const handleOutfitSelect = useCallback((recommendation: OutfitRecommendation) => {
console.log('选择穿搭方案:', recommendation);
// 这里可以添加更多的处理逻辑,比如显示详情等
}, []);
// 处理场景检索
const handleSceneSearch = useCallback((recommendation: OutfitRecommendation) => {
console.log('场景检索:', recommendation);
// 生成场景检索请求
const sceneSearchRequest = OutfitRecommendationService.generateSceneSearchRequest(recommendation);
// 将场景描述设置为搜索查询
setQuery(sceneSearchRequest.scene_description);
// 关闭穿搭方案模态框
setShowOutfitModal(false);
// 执行搜索
const searchRequest: SimilaritySearchRequest = {
query: sceneSearchRequest.scene_description,
relevance_threshold: selectedThreshold,
page_size: configState.config?.max_results_per_page || 12,
page_offset: 0,
};
executeSearch(searchRequest);
}, [selectedThreshold, configState.config?.max_results_per_page, executeSearch, setQuery]);
// 关闭穿搭方案模态框
const handleCloseOutfitModal = useCallback(() => {
setShowOutfitModal(false);
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-50">
{/* 页面头部 */}
@@ -212,6 +298,7 @@ const SimilaritySearchTool: React.FC = () => {
onSearch={handleSearch}
onSuggestionSelect={handleSuggestionSelect}
onSuggestionsToggle={setShowSuggestions}
onOutfitRecommendation={handleOutfitRecommendation}
/>
</div>
@@ -400,6 +487,19 @@ const SimilaritySearchTool: React.FC = () => {
</div>
</div>
)}
{/* 穿搭方案推荐模态框 */}
<OutfitRecommendationModal
isOpen={showOutfitModal}
onClose={handleCloseOutfitModal}
recommendations={outfitRecommendations}
isLoading={isGeneratingOutfits}
error={outfitError || undefined}
onRecommendationSelect={handleOutfitSelect}
onSceneSearch={handleSceneSearch}
onRegenerate={handleOutfitRegenerate}
query={query}
/>
</div>
);
};

View File

@@ -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<OutfitRecommendationRequest>
): Promise<OutfitRecommendationResponse> {
try {
const fullRequest: OutfitRecommendationRequest = {
...DEFAULT_OUTFIT_RECOMMENDATION_REQUEST,
...request,
} as OutfitRecommendationRequest;
console.log('🎨 发送穿搭方案生成请求:', fullRequest);
const response = await invoke<OutfitRecommendationResponse>(
'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<OutfitRecommendationResponse> {
return this.generateRecommendations({
query,
count,
});
}
/**
* 基于风格生成推荐
*/
static async generateByStyle(
query: string,
targetStyle: string,
count: number = 3
): Promise<OutfitRecommendationResponse> {
return this.generateRecommendations({
query,
target_style: targetStyle,
count,
});
}
/**
* 基于场合生成推荐
*/
static async generateByOccasion(
query: string,
occasions: string[],
count: number = 3
): Promise<OutfitRecommendationResponse> {
return this.generateRecommendations({
query,
occasions,
count,
});
}
/**
* 基于季节生成推荐
*/
static async generateBySeason(
query: string,
season: string,
count: number = 3
): Promise<OutfitRecommendationResponse> {
return this.generateRecommendations({
query,
season,
count,
});
}
/**
* 基于色彩偏好生成推荐
*/
static async generateByColors(
query: string,
colorPreferences: string[],
count: number = 3
): Promise<OutfitRecommendationResponse> {
return this.generateRecommendations({
query,
color_preferences: colorPreferences,
count,
});
}
/**
* 生成场景检索请求
* 用于将穿搭方案转换为场景检索参数
*/
static generateSceneSearchRequest(
recommendation: OutfitRecommendation,
searchParams?: Partial<SceneSearchRequest['search_params']>
): 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;

View File

@@ -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<OutfitRecommendationRequest> = {
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);
},
};

View File

@@ -77,6 +77,7 @@ export interface SimilaritySearchPanelProps {
onSearch: (request: SimilaritySearchRequest) => void;
onSuggestionSelect: (suggestion: string) => void;
onSuggestionsToggle: (show: boolean) => void;
onOutfitRecommendation?: () => void; // 穿搭方案生成回调
}
export interface SimilaritySearchResultsProps {