- 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.
720 lines
26 KiB
TypeScript
720 lines
26 KiB
TypeScript
import React, { useState, useCallback, useEffect } from 'react';
|
||
import {
|
||
Sparkles,
|
||
Wand2,
|
||
Shirt,
|
||
Download,
|
||
Copy,
|
||
RefreshCw,
|
||
Settings,
|
||
Info,
|
||
Heart,
|
||
ArrowLeftRight
|
||
} from 'lucide-react';
|
||
import OutfitRecommendationService from '../../services/outfitRecommendationService';
|
||
import {
|
||
OutfitRecommendation,
|
||
OutfitRecommendationGroup,
|
||
GroupingStrategy,
|
||
STYLE_OPTIONS,
|
||
OCCASION_OPTIONS,
|
||
SEASON_OPTIONS
|
||
} from '../../types/outfitRecommendation';
|
||
import OutfitRecommendationList from '../../components/outfit/OutfitRecommendationList';
|
||
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 设计标准
|
||
*/
|
||
const OutfitRecommendationTool: React.FC = () => {
|
||
// 状态管理
|
||
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 [groups, setGroups] = useState<OutfitRecommendationGroup[]>([]);
|
||
const [groupingStrategy, setGroupingStrategy] = useState<GroupingStrategy | null>(null);
|
||
const [recommendations, setRecommendations] = useState<OutfitRecommendation[]>([]);
|
||
const [isGenerating, setIsGenerating] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||
|
||
// 素材检索状态
|
||
const [showMaterialSearch, setShowMaterialSearch] = useState(false);
|
||
const [selectedRecommendation, setSelectedRecommendation] = useState<OutfitRecommendation | null>(null);
|
||
|
||
// 素材详情状态
|
||
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()) {
|
||
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,
|
||
});
|
||
|
||
// 更新分组和方案数据
|
||
if (response.groups && response.groups.length > 0) {
|
||
setGroups(response.groups);
|
||
setGroupingStrategy(response.grouping_strategy);
|
||
// 为向后兼容,也设置recommendations
|
||
setRecommendations(response.recommendations || []);
|
||
} else {
|
||
// 向后兼容模式
|
||
setGroups([]);
|
||
setGroupingStrategy(null);
|
||
setRecommendations(response.recommendations || []);
|
||
}
|
||
|
||
// 生成成功后立即保存状态
|
||
setTimeout(() => {
|
||
saveState();
|
||
}, 100);
|
||
} 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 handleLoadMoreForGroup = useCallback(async (groupId: string, styleKeywords: string[]) => {
|
||
try {
|
||
console.log(`🎨 获取分组 ${groupId} 的更多方案,关键词:`, styleKeywords);
|
||
|
||
// 构建增强的查询
|
||
const enhancedQuery = `${query} ${styleKeywords.join(' ')}`;
|
||
|
||
const response = await OutfitRecommendationService.generateRecommendations({
|
||
query: enhancedQuery,
|
||
target_style: targetStyle || undefined,
|
||
occasions: occasions.length > 0 ? occasions : undefined,
|
||
season: season || undefined,
|
||
color_preferences: colorPreferences.length > 0 ? colorPreferences : undefined,
|
||
count: 3, // 每次获取3个新方案
|
||
});
|
||
|
||
// 找到对应的分组并添加新方案
|
||
if (response.groups && response.groups.length > 0) {
|
||
// 如果返回了分组,合并到现有分组中
|
||
setGroups(prevGroups => {
|
||
return prevGroups.map(group => {
|
||
if (group.group_id === groupId) {
|
||
// 找到匹配的新分组并合并方案
|
||
const newGroup = response.groups.find(g =>
|
||
g.style_keywords.some(keyword =>
|
||
styleKeywords.includes(keyword)
|
||
)
|
||
);
|
||
if (newGroup) {
|
||
return {
|
||
...group,
|
||
children: [...group.children, ...newGroup.children]
|
||
};
|
||
}
|
||
}
|
||
return group;
|
||
});
|
||
});
|
||
} else if (response.recommendations) {
|
||
// 向后兼容:直接添加到对应分组
|
||
setGroups(prevGroups => {
|
||
return prevGroups.map(group => {
|
||
if (group.group_id === groupId) {
|
||
return {
|
||
...group,
|
||
children: [...group.children, ...(response.recommendations || [])]
|
||
};
|
||
}
|
||
return group;
|
||
});
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error('获取更多方案失败:', err);
|
||
setError(err instanceof Error ? err.message : '获取更多方案失败');
|
||
}
|
||
}, [query, targetStyle, occasions, season, colorPreferences]);
|
||
|
||
// 清空表单
|
||
const handleClear = useCallback(() => {
|
||
setQuery('');
|
||
setTargetStyle('');
|
||
setOccasions([]);
|
||
setSeason('');
|
||
setColorPreferences([]);
|
||
setCount(3);
|
||
setGroups([]);
|
||
setGroupingStrategy(null);
|
||
setRecommendations([]);
|
||
setError(null);
|
||
setShowRestoredTip(false);
|
||
|
||
// 清除保存的状态
|
||
try {
|
||
localStorage.removeItem(STORAGE_KEY);
|
||
} catch (error) {
|
||
console.warn('清除保存状态失败:', error);
|
||
}
|
||
}, []);
|
||
|
||
// 导出结果
|
||
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 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) {
|
||
return;
|
||
}
|
||
|
||
const text = recommendations.map(rec =>
|
||
`${rec.title}\n${rec.description}\n风格: ${rec.style_tags.join(', ')}\n\n`
|
||
).join('');
|
||
|
||
navigator.clipboard.writeText(text);
|
||
}, [recommendations]);
|
||
|
||
// 处理素材检索
|
||
const handleMaterialSearch = useCallback((recommendation: OutfitRecommendation) => {
|
||
setSelectedRecommendation(recommendation);
|
||
setShowMaterialSearch(true);
|
||
}, []);
|
||
|
||
// 关闭素材检索面板
|
||
const handleCloseMaterialSearch = useCallback(() => {
|
||
setShowMaterialSearch(false);
|
||
setSelectedRecommendation(null);
|
||
}, []);
|
||
|
||
// 处理素材选择
|
||
const handleMaterialSelect = useCallback((material: any) => {
|
||
console.log('选择素材:', material);
|
||
setSelectedMaterial(material);
|
||
setShowMaterialDetail(true);
|
||
}, []);
|
||
|
||
// 关闭素材详情
|
||
const handleCloseMaterialDetail = useCallback(() => {
|
||
setShowMaterialDetail(false);
|
||
setSelectedMaterial(null);
|
||
}, []);
|
||
|
||
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">
|
||
<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>
|
||
<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
|
||
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>
|
||
<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>
|
||
|
||
{/* 右侧结果区域 */}
|
||
<div className="lg:col-span-3">
|
||
<OutfitRecommendationList
|
||
groups={groups}
|
||
groupingStrategy={groupingStrategy || undefined}
|
||
recommendations={recommendations}
|
||
isLoading={isGenerating}
|
||
error={error || undefined}
|
||
onRegenerate={handleRegenerate}
|
||
onMaterialSearch={handleMaterialSearch}
|
||
onLoadMoreForGroup={handleLoadMoreForGroup}
|
||
onAddToComparison={handleAddToComparison}
|
||
selectedForComparison={selectedForComparison}
|
||
className="min-h-[600px]"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 素材检索面板 */}
|
||
{selectedRecommendation && (
|
||
<MaterialSearchPanel
|
||
recommendation={selectedRecommendation}
|
||
isVisible={showMaterialSearch}
|
||
onClose={handleCloseMaterialSearch}
|
||
onMaterialSelect={handleMaterialSelect}
|
||
/>
|
||
)}
|
||
|
||
{/* 素材详情模态框 */}
|
||
{selectedMaterial && (
|
||
<MaterialDetailModal
|
||
material={selectedMaterial}
|
||
isVisible={showMaterialDetail}
|
||
onClose={handleCloseMaterialDetail}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default OutfitRecommendationTool;
|