mixvideo-v2/apps/desktop/src/components/outfit/OutfitRecommendationCard.tsx

307 lines
11 KiB
TypeScript

import React, { useState, useCallback } from 'react';
import {
Sparkles,
MapPin,
Clock,
Palette,
Tag,
Camera,
ExternalLink,
ChevronRight,
Sun,
Moon,
Sunrise,
Sunset,
Search,
} from 'lucide-react';
import { OutfitRecommendationCardProps } from '../../types/outfitRecommendation';
/**
* 穿搭方案推荐卡片组件
* 遵循设计系统规范,提供统一的方案展示界面
*/
export const OutfitRecommendationCard: React.FC<OutfitRecommendationCardProps> = ({
recommendation,
onSelect,
onSceneSearch,
onMaterialSearch,
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 handleMaterialSearch = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
if (onMaterialSearch) {
onMaterialSearch(recommendation);
}
}, [recommendation, onMaterialSearch]);
// 处理展开/收起
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="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>
<div className="flex items-center gap-2">
{onMaterialSearch && (
<button
onClick={handleMaterialSearch}
className="flex items-center gap-2 px-3 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors duration-200 text-sm font-medium"
>
<Search className="w-4 h-4" />
</button>
)}
{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>
</div>
);
};
export default OutfitRecommendationCard;