mixvideo-v2/apps/desktop/src/pages/tools/OutfitRecommendationTool.tsx

362 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="mx-auto px-6 py-4">
<div className="page-header 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="mx-auto px-6 py-8">
<div className="grid grid-cols-1 lg:grid-cols-4 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-3">
<OutfitRecommendationList
recommendations={recommendations}
isLoading={isGenerating}
error={error || undefined}
onRegenerate={handleRegenerate}
className="min-h-[600px]"
/>
</div>
</div>
</div>
</div>
);
};
export default OutfitRecommendationTool;