feat: 实现AI穿搭方案推荐功能
- 新增穿搭方案推荐数据模型和类型定义 - 实现基于TikTok视觉趋势的Gemini AI穿搭方案生成 - 创建穿搭方案卡片和列表展示组件 - 集成Sparkles图标点击触发穿搭方案生成 - 实现穿搭方案到场景检索的无缝集成 - 添加完整的前后端API和服务层 - 遵循promptx开发规范和设计系统标准 功能特点: - 基于用户输入关键词生成个性化穿搭推荐 - 包含色彩搭配、风格标签、场景建议等详细信息 - 提供TikTok优化建议和拍摄技巧 - 支持一键场景检索功能 - 美观的卡片式展示界面 - 完整的加载状态和错误处理
This commit is contained in:
292
apps/desktop/src/components/outfit/OutfitRecommendationCard.tsx
Normal file
292
apps/desktop/src/components/outfit/OutfitRecommendationCard.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user