feat: add Outfit Comparison Tool and Outfit Favorites Tool
- Implemented OutfitComparisonTool for comparing two favorite outfits side by side. - Added OutfitFavoritesTool for managing and searching favorite outfits. - Created OutfitFavoriteService for handling API interactions related to outfit favorites. - Defined types for material search and outfit favorites to ensure type safety. - Enhanced UI components for better user experience in selecting and displaying outfits.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
Sparkles,
|
||||
Wand2,
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
Copy,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Info
|
||||
Info,
|
||||
Heart,
|
||||
ArrowLeftRight
|
||||
} from 'lucide-react';
|
||||
import OutfitRecommendationService from '../../services/outfitRecommendationService';
|
||||
import {
|
||||
@@ -22,6 +24,23 @@ import OutfitRecommendationList from '../../components/outfit/OutfitRecommendati
|
||||
import { CustomSelect } from '../../components/CustomSelect';
|
||||
import { MaterialSearchPanel, MaterialDetailModal } from '../../components/material';
|
||||
|
||||
// localStorage 键名
|
||||
const STORAGE_KEY = 'outfit-recommendation-state';
|
||||
|
||||
// 状态接口
|
||||
interface SavedState {
|
||||
query: string;
|
||||
targetStyle: string;
|
||||
occasions: string[];
|
||||
season: string;
|
||||
colorPreferences: string[];
|
||||
count: number;
|
||||
groups: OutfitRecommendationGroup[];
|
||||
groupingStrategy: GroupingStrategy | null;
|
||||
recommendations: OutfitRecommendation[];
|
||||
showAdvanced: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI穿搭方案推荐小工具
|
||||
* 遵循 Tauri 开发规范和 UI/UX 设计标准
|
||||
@@ -51,6 +70,110 @@ const OutfitRecommendationTool: React.FC = () => {
|
||||
const [showMaterialDetail, setShowMaterialDetail] = useState(false);
|
||||
const [selectedMaterial, setSelectedMaterial] = useState<any>(null);
|
||||
|
||||
// 方案对比状态
|
||||
const [selectedForComparison, setSelectedForComparison] = useState<OutfitRecommendation[]>([]);
|
||||
|
||||
// 状态恢复提示
|
||||
const [showRestoredTip, setShowRestoredTip] = useState(false);
|
||||
|
||||
// 保存状态到localStorage
|
||||
const saveState = useCallback(() => {
|
||||
try {
|
||||
const state: SavedState = {
|
||||
query,
|
||||
targetStyle,
|
||||
occasions,
|
||||
season,
|
||||
colorPreferences,
|
||||
count,
|
||||
groups,
|
||||
groupingStrategy,
|
||||
recommendations,
|
||||
showAdvanced,
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch (error) {
|
||||
console.warn('保存状态失败:', error);
|
||||
}
|
||||
}, [query, targetStyle, occasions, season, colorPreferences, count, groups, groupingStrategy, recommendations, showAdvanced]);
|
||||
|
||||
// 从localStorage恢复状态
|
||||
const restoreState = useCallback(() => {
|
||||
try {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedState) {
|
||||
const state: SavedState = JSON.parse(savedState);
|
||||
setQuery(state.query || '');
|
||||
setTargetStyle(state.targetStyle || '');
|
||||
setOccasions(state.occasions || []);
|
||||
setSeason(state.season || '');
|
||||
setColorPreferences(state.colorPreferences || []);
|
||||
setCount(state.count || 3);
|
||||
setGroups(state.groups || []);
|
||||
setGroupingStrategy(state.groupingStrategy || null);
|
||||
setRecommendations(state.recommendations || []);
|
||||
setShowAdvanced(state.showAdvanced || false);
|
||||
return true; // 表示成功恢复了状态
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('恢复状态失败:', error);
|
||||
}
|
||||
return false; // 表示没有恢复状态
|
||||
}, []);
|
||||
|
||||
// 组件加载时恢复状态
|
||||
useEffect(() => {
|
||||
const restored = restoreState();
|
||||
if (restored) {
|
||||
setShowRestoredTip(true);
|
||||
// 3秒后自动隐藏提示
|
||||
setTimeout(() => {
|
||||
setShowRestoredTip(false);
|
||||
}, 3000);
|
||||
}
|
||||
}, [restoreState]);
|
||||
|
||||
// 状态变化时自动保存(防抖)
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (recommendations.length > 0) {
|
||||
saveState();
|
||||
}
|
||||
}, 1000); // 1秒防抖
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [recommendations, saveState]);
|
||||
|
||||
// 监听对比选择,当选择了2个方案时自动跳转到对比页面
|
||||
useEffect(() => {
|
||||
if (selectedForComparison.length === 2) {
|
||||
// 将选中的方案保存到localStorage,供对比页面使用
|
||||
try {
|
||||
localStorage.setItem('outfit-comparison-data', JSON.stringify({
|
||||
leftFavorite: {
|
||||
id: selectedForComparison[0].id,
|
||||
recommendation_data: selectedForComparison[0],
|
||||
custom_name: selectedForComparison[0].title,
|
||||
created_at: new Date().toISOString()
|
||||
},
|
||||
rightFavorite: {
|
||||
id: selectedForComparison[1].id,
|
||||
recommendation_data: selectedForComparison[1],
|
||||
custom_name: selectedForComparison[1].title,
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
}));
|
||||
|
||||
// 短暂延迟后跳转,让用户看到选择反馈
|
||||
setTimeout(() => {
|
||||
window.location.href = '/tools/outfit-comparison';
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('保存对比数据失败:', error);
|
||||
}
|
||||
}
|
||||
}, [selectedForComparison]);
|
||||
|
||||
// 生成穿搭方案
|
||||
const handleGenerate = useCallback(async () => {
|
||||
if (!query.trim()) {
|
||||
@@ -83,6 +206,11 @@ const OutfitRecommendationTool: React.FC = () => {
|
||||
setGroupingStrategy(null);
|
||||
setRecommendations(response.recommendations || []);
|
||||
}
|
||||
|
||||
// 生成成功后立即保存状态
|
||||
setTimeout(() => {
|
||||
saveState();
|
||||
}, 100);
|
||||
} catch (err) {
|
||||
console.error('穿搭方案生成失败:', err);
|
||||
setError(err instanceof Error ? err.message : '穿搭方案生成失败');
|
||||
@@ -167,6 +295,14 @@ const OutfitRecommendationTool: React.FC = () => {
|
||||
setGroupingStrategy(null);
|
||||
setRecommendations([]);
|
||||
setError(null);
|
||||
setShowRestoredTip(false);
|
||||
|
||||
// 清除保存的状态
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.warn('清除保存状态失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 导出结果
|
||||
@@ -203,6 +339,34 @@ const OutfitRecommendationTool: React.FC = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, [recommendations, query]);
|
||||
|
||||
// 打开收藏管理
|
||||
const handleOpenFavoriteManagement = useCallback(() => {
|
||||
// 使用路由跳转到收藏管理页面
|
||||
window.location.href = '/tools/outfit-favorites';
|
||||
}, []);
|
||||
|
||||
// 打开方案对比
|
||||
const handleOpenComparison = useCallback(() => {
|
||||
// 使用路由跳转到方案对比页面
|
||||
window.location.href = '/tools/outfit-comparison';
|
||||
}, []);
|
||||
|
||||
// 添加到对比
|
||||
const handleAddToComparison = useCallback((recommendation: OutfitRecommendation) => {
|
||||
setSelectedForComparison(prev => {
|
||||
if (prev.find(r => r.id === recommendation.id)) {
|
||||
// 如果已存在,则移除
|
||||
return prev.filter(r => r.id !== recommendation.id);
|
||||
} else if (prev.length < 2) {
|
||||
// 如果少于2个,则添加
|
||||
return [...prev, recommendation];
|
||||
} else {
|
||||
// 如果已有2个,替换第一个
|
||||
return [prev[1], recommendation];
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 复制结果
|
||||
const handleCopy = useCallback(() => {
|
||||
if (recommendations.length === 0) {
|
||||
@@ -255,13 +419,54 @@ const OutfitRecommendationTool: React.FC = () => {
|
||||
<Sparkles className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">AI穿搭方案推荐</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl font-bold text-gray-900">AI穿搭方案推荐</h1>
|
||||
{showRestoredTip && (
|
||||
<div className="px-2 py-1 bg-green-100 text-green-700 text-xs rounded-full animate-fade-in">
|
||||
已恢复上次结果
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">基于TikTok视觉趋势的智能穿搭建议</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 收藏管理入口 */}
|
||||
<button
|
||||
onClick={handleOpenFavoriteManagement}
|
||||
className="flex items-center gap-2 px-3 py-2 text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors duration-200"
|
||||
title="收藏管理"
|
||||
>
|
||||
<Heart className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">收藏管理</span>
|
||||
</button>
|
||||
|
||||
{/* 方案对比入口 */}
|
||||
<button
|
||||
onClick={handleOpenComparison}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-lg transition-colors duration-200 relative ${
|
||||
selectedForComparison.length > 0
|
||||
? 'text-blue-600 bg-blue-50 hover:bg-blue-100'
|
||||
: 'text-gray-600 hover:text-blue-600 hover:bg-blue-50'
|
||||
}`}
|
||||
title={`方案对比 ${selectedForComparison.length > 0 ? `(已选择${selectedForComparison.length}个)` : ''}`}
|
||||
>
|
||||
<ArrowLeftRight className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">方案对比</span>
|
||||
{selectedForComparison.length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 w-5 h-5 bg-blue-500 text-white text-xs rounded-full flex items-center justify-center">
|
||||
{selectedForComparison.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 分隔线 */}
|
||||
{recommendations.length > 0 && (
|
||||
<div className="h-6 w-px bg-gray-300 mx-1"></div>
|
||||
)}
|
||||
|
||||
{recommendations.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
@@ -433,10 +638,39 @@ const OutfitRecommendationTool: React.FC = () => {
|
||||
<li>• 可以指定场合、风格、颜色偏好</li>
|
||||
<li>• 每个方案都包含TikTok优化建议</li>
|
||||
<li>• 支持导出和复制结果</li>
|
||||
<li>• 点击方案卡片的"对比"按钮选择方案进行对比</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 对比状态面板 */}
|
||||
{selectedForComparison.length > 0 && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<ArrowLeftRight className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-green-800 flex-1">
|
||||
<p className="font-medium mb-1">方案对比</p>
|
||||
<p className="text-xs mb-2">
|
||||
已选择 {selectedForComparison.length}/2 个方案
|
||||
{selectedForComparison.length === 2 && ',即将自动跳转到对比页面...'}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{selectedForComparison.map((rec, index) => (
|
||||
<div key={rec.id} className="text-xs bg-white bg-opacity-50 rounded px-2 py-1">
|
||||
{index + 1}. {rec.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{selectedForComparison.length === 1 && (
|
||||
<p className="text-xs mt-2 opacity-75">
|
||||
再选择一个方案即可开始对比
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -452,6 +686,8 @@ const OutfitRecommendationTool: React.FC = () => {
|
||||
onRegenerate={handleRegenerate}
|
||||
onMaterialSearch={handleMaterialSearch}
|
||||
onLoadMoreForGroup={handleLoadMoreForGroup}
|
||||
onAddToComparison={handleAddToComparison}
|
||||
selectedForComparison={selectedForComparison}
|
||||
className="min-h-[600px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user