feat: 创建AI穿搭方案推荐小工具

- 新增独立的AI穿搭方案推荐工具页面
- 集成到便捷工具列表中,提供完整的工具体验
- 支持高级设置:风格选择、场合匹配、季节偏好等
- 实现结果导出和复制功能
- 优化用户界面和交互体验
- 添加使用提示和帮助信息

功能特点:
- 简洁易用的输入界面
- 可折叠的高级设置选项
- 实时生成个性化穿搭方案
- 支持JSON格式导出结果
- 一键复制穿搭建议文本
- 响应式设计,适配不同屏幕尺寸
This commit is contained in:
imeepos 2025-07-25 10:53:35 +08:00
parent c4bb073507
commit f1fd62b59b
3 changed files with 380 additions and 1 deletions

View File

@ -19,6 +19,7 @@ import ChatTestPage from './pages/tools/ChatTestPage';
import WatermarkTool from './pages/tools/WatermarkTool';
import SimilaritySearchTool from './pages/tools/SimilaritySearchTool';
import BatchThumbnailGenerator from './pages/tools/BatchThumbnailGenerator';
import OutfitRecommendationTool from './pages/tools/OutfitRecommendationTool';
import Navigation from './components/Navigation';
import { NotificationSystem, useNotifications } from './components/NotificationSystem';
@ -121,6 +122,7 @@ function App() {
<Route path="/tools/watermark" element={<WatermarkTool />} />
<Route path="/tools/similarity-search" element={<SimilaritySearchTool />} />
<Route path="/tools/batch-thumbnail-generator" element={<BatchThumbnailGenerator />} />
<Route path="/tools/outfit-recommendation" element={<OutfitRecommendationTool />} />
</Routes>
</div>
</main>

View File

@ -8,7 +8,8 @@ import {
MessageCircle,
Droplets,
ImageIcon,
Search
Search,
Sparkles
} from 'lucide-react';
import { Tool, ToolCategory, ToolStatus } from '../types/tool';
@ -117,6 +118,21 @@ export const TOOLS_DATA: Tool[] = [
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-25'
},
{
id: 'outfit-recommendation',
name: 'AI穿搭方案推荐',
description: '基于TikTok视觉趋势的智能穿搭建议工具提供个性化的时尚搭配方案',
longDescription: '专业的AI穿搭顾问工具基于TikTok视觉趋势和时尚潮流为用户生成个性化的穿搭方案。支持多种风格选择、场合匹配、色彩搭配建议并提供TikTok优化建议和拍摄技巧助力内容创作和时尚搭配。',
icon: Sparkles,
route: '/tools/outfit-recommendation',
category: ToolCategory.AI_TOOLS,
status: ToolStatus.STABLE,
tags: ['AI穿搭', '时尚搭配', 'TikTok', '个性化推荐', '视觉趋势'],
isNew: true,
isPopular: true,
version: '1.0.0',
lastUpdated: '2024-01-25'
}
];

View File

@ -0,0 +1,361 @@
import React, { useState, useCallback } from 'react';
import {
Sparkles,
ArrowLeft,
Wand2,
Shirt,
Download,
Copy,
RefreshCw,
Settings,
Info
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import OutfitRecommendationService from '../../services/outfitRecommendationService';
import { OutfitRecommendation, STYLE_OPTIONS, OCCASION_OPTIONS, SEASON_OPTIONS } from '../../types/outfitRecommendation';
import OutfitRecommendationList from '../../components/outfit/OutfitRecommendationList';
import { CustomSelect } from '../../components/CustomSelect';
/**
* AI穿搭方案推荐小工具
* Tauri UI/UX
*/
const OutfitRecommendationTool: React.FC = () => {
const navigate = useNavigate();
// 状态管理
const [query, setQuery] = useState('');
const [targetStyle, setTargetStyle] = useState<string>('');
const [occasions, setOccasions] = useState<string[]>([]);
const [season, setSeason] = useState<string>('');
const [colorPreferences, setColorPreferences] = useState<string[]>([]);
const [count, setCount] = useState(3);
const [recommendations, setRecommendations] = useState<OutfitRecommendation[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showAdvanced, setShowAdvanced] = useState(false);
// 返回工具列表
const handleBackToTools = useCallback(() => {
navigate('/tools');
}, [navigate]);
// 生成穿搭方案
const handleGenerate = useCallback(async () => {
if (!query.trim()) {
setError('请输入穿搭关键词或描述');
return;
}
setIsGenerating(true);
setError(null);
try {
const response = await OutfitRecommendationService.generateRecommendations({
query: query.trim(),
target_style: targetStyle || undefined,
occasions: occasions.length > 0 ? occasions : undefined,
season: season || undefined,
color_preferences: colorPreferences.length > 0 ? colorPreferences : undefined,
count,
});
setRecommendations(response.recommendations);
} catch (err) {
console.error('穿搭方案生成失败:', err);
setError(err instanceof Error ? err.message : '穿搭方案生成失败');
} finally {
setIsGenerating(false);
}
}, [query, targetStyle, occasions, season, colorPreferences, count]);
// 重新生成
const handleRegenerate = useCallback(() => {
handleGenerate();
}, [handleGenerate]);
// 清空表单
const handleClear = useCallback(() => {
setQuery('');
setTargetStyle('');
setOccasions([]);
setSeason('');
setColorPreferences([]);
setCount(3);
setRecommendations([]);
setError(null);
}, []);
// 导出结果
const handleExport = useCallback(() => {
if (recommendations.length === 0) {
return;
}
const exportData = {
query,
generated_at: new Date().toISOString(),
recommendations: recommendations.map(rec => ({
title: rec.title,
description: rec.description,
overall_style: rec.overall_style,
style_tags: rec.style_tags,
occasions: rec.occasions,
seasons: rec.seasons,
color_theme: rec.color_theme,
items: rec.items,
tiktok_tips: rec.tiktok_tips,
styling_tips: rec.styling_tips,
}))
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `outfit-recommendations-${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, [recommendations, query]);
// 复制结果
const handleCopy = useCallback(() => {
if (recommendations.length === 0) {
return;
}
const text = recommendations.map(rec =>
`${rec.title}\n${rec.description}\n风格: ${rec.style_tags.join(', ')}\n\n`
).join('');
navigator.clipboard.writeText(text);
}, [recommendations]);
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white">
{/* 顶部导航 */}
<div className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={handleBackToTools}
className="flex items-center gap-2 px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200"
>
<ArrowLeft className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</button>
<div className="h-6 w-px bg-gray-300"></div>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl flex items-center justify-center shadow-lg">
<Sparkles className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-gray-900">AI穿搭方案推荐</h1>
<p className="text-sm text-gray-600">TikTok视觉趋势的智能穿搭建议</p>
</div>
</div>
</div>
<div className="flex items-center gap-2">
{recommendations.length > 0 && (
<>
<button
onClick={handleCopy}
className="flex items-center gap-2 px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200"
title="复制结果"
>
<Copy className="w-4 h-4" />
</button>
<button
onClick={handleExport}
className="flex items-center gap-2 px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200"
title="导出结果"
>
<Download className="w-4 h-4" />
</button>
</>
)}
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors duration-200 ${
showAdvanced
? 'text-primary-600 bg-primary-50'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
title="高级设置"
>
<Settings className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
{/* 主要内容 */}
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* 左侧输入区域 */}
<div className="lg:col-span-1">
<div className="card p-6 sticky top-24">
<div className="space-y-6">
{/* 基础输入 */}
<div>
<label className="block text-sm font-semibold text-gray-900 mb-3">
<Shirt className="w-4 h-4 inline mr-2" />
穿
</label>
<textarea
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="描述您想要的穿搭风格,如:休闲约会装、正式商务装、街头潮流风等..."
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent resize-none"
rows={4}
/>
</div>
{/* 高级设置 */}
{showAdvanced && (
<div className="space-y-4 animate-fade-in">
<div className="border-t border-gray-200 pt-4">
<h3 className="text-sm font-semibold text-gray-900 mb-3"></h3>
</div>
{/* 目标风格 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<CustomSelect
value={targetStyle}
onChange={setTargetStyle}
options={[
{ value: '', label: '不限制' },
...STYLE_OPTIONS.map(style => ({ value: style, label: style }))
]}
placeholder="选择风格"
/>
</div>
{/* 适合场合 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<div className="grid grid-cols-2 gap-2">
{OCCASION_OPTIONS.map(occasion => (
<label key={occasion} className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={occasions.includes(occasion)}
onChange={(e) => {
if (e.target.checked) {
setOccasions([...occasions, occasion]);
} else {
setOccasions(occasions.filter(o => o !== occasion));
}
}}
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
{occasion}
</label>
))}
</div>
</div>
{/* 季节 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<CustomSelect
value={season}
onChange={setSeason}
options={[
{ value: '', label: '不限制' },
...SEASON_OPTIONS.map(s => ({ value: s, label: s }))
]}
placeholder="选择季节"
/>
</div>
{/* 生成数量 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<select
value={count}
onChange={(e) => setCount(Number(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={5}>5</option>
</select>
</div>
</div>
)}
{/* 操作按钮 */}
<div className="space-y-3">
<button
onClick={handleGenerate}
disabled={!query.trim() || isGenerating}
className="w-full btn btn-primary flex items-center justify-center gap-2"
>
{isGenerating ? (
<>
<RefreshCw className="w-4 h-4 animate-spin" />
...
</>
) : (
<>
<Wand2 className="w-4 h-4" />
穿
</>
)}
</button>
<button
onClick={handleClear}
className="w-full btn btn-outline"
>
</button>
</div>
{/* 使用提示 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<div className="text-sm text-blue-800">
<p className="font-medium mb-1">使</p>
<ul className="space-y-1 text-xs">
<li> </li>
<li> </li>
<li> TikTok优化建议</li>
<li> </li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 右侧结果区域 */}
<div className="lg:col-span-2">
<OutfitRecommendationList
recommendations={recommendations}
isLoading={isGenerating}
error={error || undefined}
onRegenerate={handleRegenerate}
className="min-h-[600px]"
/>
</div>
</div>
</div>
</div>
);
};
export default OutfitRecommendationTool;