feat: 创建AI穿搭方案推荐小工具
- 新增独立的AI穿搭方案推荐工具页面 - 集成到便捷工具列表中,提供完整的工具体验 - 支持高级设置:风格选择、场合匹配、季节偏好等 - 实现结果导出和复制功能 - 优化用户界面和交互体验 - 添加使用提示和帮助信息 功能特点: - 简洁易用的输入界面 - 可折叠的高级设置选项 - 实时生成个性化穿搭方案 - 支持JSON格式导出结果 - 一键复制穿搭建议文本 - 响应式设计,适配不同屏幕尺寸
This commit is contained in:
parent
c4bb073507
commit
f1fd62b59b
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue