feat: 完成CustomMultiSelect多选组件开发并集成到顶部导航栏

This commit is contained in:
imeepos
2025-07-17 20:09:37 +08:00
parent 045de8850a
commit 7f3a59282d
14 changed files with 571 additions and 246 deletions

View File

@@ -81,7 +81,7 @@ function App() {
<Route path="/templates" element={<TemplateManagement />} /> <Route path="/templates" element={<TemplateManagement />} />
<Route path="/material-model-binding" element={<MaterialModelBinding />} /> <Route path="/material-model-binding" element={<MaterialModelBinding />} />
<Route path="/tools" element={<Tools />} /> <Route path="/tools" element={<Tools />} />
<Route path="/outfit-match/:projectId" element={<OutfitMatch />} /> <Route path="/outfit-match" element={<OutfitMatch />} />
</Routes> </Routes>
</div> </div>
</main> </main>

View File

@@ -1,6 +1,8 @@
import React, { useState, useRef, useEffect } from 'react';
import { ChevronDownIcon } from "lucide-react"; import { ChevronDownIcon } from "lucide-react";
import { XMarkIcon, CheckIcon } from '@heroicons/react/24/outline';
// 自定义下拉选择组件 // 单选下拉选择组件
export const CustomSelect: React.FC<{ export const CustomSelect: React.FC<{
value: string | number | null | undefined; value: string | number | null | undefined;
onChange: (value: string) => void; onChange: (value: string) => void;
@@ -31,4 +33,260 @@ export const CustomSelect: React.FC<{
</div> </div>
</div> </div>
); );
};
// 多选下拉选择组件
interface MultiSelectOption {
value: string;
label: string;
disabled?: boolean;
}
interface CustomMultiSelectProps {
options: MultiSelectOption[];
value: string[];
onChange: (value: string[]) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
maxDisplayItems?: number;
searchable?: boolean;
}
export const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
options,
value = [],
onChange,
placeholder = "请选择...",
disabled = false,
className = "",
maxDisplayItems = 3,
searchable = false
}) => {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const dropdownRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
// 过滤选项
const filteredOptions = searchable && searchTerm
? options.filter(option =>
option.label.toLowerCase().includes(searchTerm.toLowerCase())
)
: options;
// 获取选中的选项标签
const getSelectedLabels = () => {
return value.map(val => {
const option = options.find(opt => opt.value === val);
return option ? option.label : val;
});
};
// 显示的文本
const getDisplayText = () => {
const selectedLabels = getSelectedLabels();
if (selectedLabels.length === 0) {
return placeholder;
}
if (selectedLabels.length <= maxDisplayItems) {
return selectedLabels.join(', ');
}
return `${selectedLabels.slice(0, maxDisplayItems).join(', ')} +${selectedLabels.length - maxDisplayItems}`;
};
// 处理选项点击
const handleOptionClick = (optionValue: string) => {
if (value.includes(optionValue)) {
// 取消选择
onChange(value.filter(val => val !== optionValue));
} else {
// 添加选择
onChange([...value, optionValue]);
}
};
// 移除单个选项
const removeOption = (optionValue: string, e: React.MouseEvent) => {
e.stopPropagation();
onChange(value.filter(val => val !== optionValue));
};
// 清空所有选择
const clearAll = (e: React.MouseEvent) => {
e.stopPropagation();
onChange([]);
};
// 点击外部关闭下拉框
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
setSearchTerm('');
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// 打开下拉框时聚焦搜索框
useEffect(() => {
if (isOpen && searchable && searchInputRef.current) {
searchInputRef.current.focus();
}
}, [isOpen, searchable]);
return (
<div className={`relative ${className}`} ref={dropdownRef}>
{/* 主选择框 */}
<div
className={`
relative w-full cursor-pointer rounded-lg border border-gray-200 bg-white py-2 pl-3 pr-10 text-left shadow-sm
focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500
${disabled ? 'cursor-not-allowed bg-gray-50 text-gray-500' : 'hover:border-gray-300'}
${isOpen ? 'border-primary-500 ring-2 ring-primary-500' : ''}
`}
onClick={() => !disabled && setIsOpen(!isOpen)}
>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
{value.length > 0 ? (
<div className="flex flex-wrap gap-1">
{value.length <= maxDisplayItems ? (
// 显示所有选中的标签
getSelectedLabels().map((label, index) => (
<span
key={value[index]}
className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-primary-100 text-primary-800"
>
{label}
{!disabled && (
<button
type="button"
onClick={(e) => removeOption(value[index], e)}
className="ml-1 inline-flex items-center justify-center w-4 h-4 rounded-full hover:bg-primary-200 focus:outline-none"
>
<XMarkIcon className="w-3 h-3" />
</button>
)}
</span>
))
) : (
// 显示简化文本
<span className="text-gray-900 truncate text-sm">
{getDisplayText()}
</span>
)}
</div>
) : (
<span className="text-gray-500 truncate text-sm">{placeholder}</span>
)}
</div>
<div className="flex items-center space-x-1">
{value.length > 0 && !disabled && (
<button
type="button"
onClick={clearAll}
className="inline-flex items-center justify-center w-5 h-5 rounded-full hover:bg-gray-200 focus:outline-none"
>
<XMarkIcon className="w-4 h-4 text-gray-400" />
</button>
)}
<ChevronDownIcon
className={`w-4 h-4 text-gray-400 transition-transform duration-200 ${
isOpen ? 'transform rotate-180' : ''
}`}
/>
</div>
</div>
</div>
{/* 下拉选项 */}
{isOpen && (
<div className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-lg py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
{/* 搜索框 */}
{searchable && (
<div className="px-3 py-2 border-b border-gray-200">
<input
ref={searchInputRef}
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="搜索选项..."
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
/>
</div>
)}
{/* 全选/取消全选 */}
{filteredOptions.length > 1 && (
<div className="px-3 py-2 border-b border-gray-200">
<button
type="button"
onClick={() => {
const allValues = filteredOptions.map(opt => opt.value);
const isAllSelected = allValues.every(val => value.includes(val));
if (isAllSelected) {
// 取消选择当前过滤的所有选项
onChange(value.filter(val => !allValues.includes(val)));
} else {
// 选择当前过滤的所有选项
const newValue = [...new Set([...value, ...allValues])];
onChange(newValue);
}
}}
className="text-sm text-primary-600 hover:text-primary-800 font-medium"
>
{filteredOptions.every(opt => value.includes(opt.value)) ? '取消全选' : '全选'}
</button>
</div>
)}
{/* 选项列表 */}
{filteredOptions.length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">
{searchTerm ? '没有找到匹配的选项' : '暂无选项'}
</div>
) : (
filteredOptions.map((option) => {
const isSelected = value.includes(option.value);
return (
<div
key={option.value}
className={`
relative cursor-pointer select-none py-2 pl-3 pr-9 hover:bg-gray-50
${option.disabled ? 'cursor-not-allowed opacity-50' : ''}
${isSelected ? 'bg-primary-50 text-primary-900' : 'text-gray-900'}
`}
onClick={() => !option.disabled && handleOptionClick(option.value)}
>
<div className="flex items-center">
<span className={`block truncate text-sm ${isSelected ? 'font-medium' : 'font-normal'}`}>
{option.label}
</span>
</div>
{isSelected && (
<span className="absolute inset-y-0 right-0 flex items-center pr-4 text-primary-600">
<CheckIcon className="w-4 h-4" />
</span>
)}
</div>
);
})
)}
</div>
)}
</div>
);
}; };

View File

@@ -6,7 +6,8 @@ import {
CpuChipIcon, CpuChipIcon,
DocumentDuplicateIcon, DocumentDuplicateIcon,
LinkIcon, LinkIcon,
WrenchScrewdriverIcon WrenchScrewdriverIcon,
SparklesIcon
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
const Navigation: React.FC = () => { const Navigation: React.FC = () => {
@@ -43,6 +44,12 @@ const Navigation: React.FC = () => {
icon: CpuChipIcon, icon: CpuChipIcon,
description: '管理AI视频分类规则' description: '管理AI视频分类规则'
}, },
{
name: '服装搭配',
href: '/outfit-match',
icon: SparklesIcon,
description: 'AI智能服装搭配推荐'
},
{ {
name: '便捷工具', name: '便捷工具',
href: '/tools', href: '/tools',

View File

@@ -0,0 +1,163 @@
import React, { useState } from 'react';
import { CustomSelect, CustomMultiSelect } from '../CustomSelect';
// 示例:如何使用单选和多选组件
const MultiSelectExample: React.FC = () => {
const [singleValue, setSingleValue] = useState<string>('');
const [multiValue, setMultiValue] = useState<string[]>([]);
const [searchableMultiValue, setSearchableMultiValue] = useState<string[]>([]);
// 示例选项
const categoryOptions = [
{ value: 'top', label: '上装' },
{ value: 'bottom', label: '下装' },
{ value: 'dress', label: '连衣裙' },
{ value: 'outerwear', label: '外套' },
{ value: 'footwear', label: '鞋类' },
{ value: 'accessory', label: '配饰' },
{ value: 'other', label: '其他' }
];
const styleOptions = [
{ value: 'casual', label: '休闲风格' },
{ value: 'formal', label: '正式风格' },
{ value: 'business', label: '商务风格' },
{ value: 'street', label: '街头风格' },
{ value: 'elegant', label: '优雅风格' },
{ value: 'sporty', label: '运动风格' },
{ value: 'vintage', label: '复古风格' },
{ value: 'minimalist', label: '简约风格' },
{ value: 'bohemian', label: '波西米亚风格' },
{ value: 'gothic', label: '哥特风格' }
];
return (
<div className="max-w-4xl mx-auto p-6 space-y-8">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">
CustomSelect 使
</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 单选组件示例 */}
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-800"> (CustomSelect)</h2>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<CustomSelect
value={singleValue}
onChange={setSingleValue}
options={categoryOptions}
placeholder="请选择一个类别"
className="w-full"
/>
<p className="mt-2 text-sm text-gray-600">
: {singleValue || '未选择'}
</p>
</div>
</div>
{/* 多选组件示例 */}
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-800"> (CustomMultiSelect)</h2>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
()
</label>
<CustomMultiSelect
value={multiValue}
onChange={setMultiValue}
options={categoryOptions}
placeholder="请选择多个类别"
className="w-full"
maxDisplayItems={2}
/>
<p className="mt-2 text-sm text-gray-600">
: {multiValue.length > 0 ? multiValue.join(', ') : '未选择'}
</p>
</div>
</div>
</div>
{/* 带搜索的多选组件示例 */}
<div className="mt-8">
<h2 className="text-lg font-semibold text-gray-800 mb-4"></h2>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
()
</label>
<CustomMultiSelect
value={searchableMultiValue}
onChange={setSearchableMultiValue}
options={styleOptions}
placeholder="请选择风格,支持搜索"
className="w-full"
maxDisplayItems={3}
searchable={true}
/>
<p className="mt-2 text-sm text-gray-600">
: {searchableMultiValue.length > 0 ? searchableMultiValue.join(', ') : '未选择'}
</p>
</div>
</div>
{/* 使用说明 */}
<div className="mt-8 bg-gray-50 rounded-lg p-4">
<h3 className="text-md font-semibold text-gray-800 mb-3">使</h3>
<div className="space-y-2 text-sm text-gray-600">
<div>
<strong>CustomSelect ():</strong>
<ul className="list-disc list-inside ml-4 mt-1">
<li></li>
<li>使 value (string) onChange ((value: string) =&gt; void)</li>
</ul>
</div>
<div>
<strong>CustomMultiSelect ():</strong>
<ul className="list-disc list-inside ml-4 mt-1">
<li></li>
<li>使 value (string[]) onChange ((value: string[]) =&gt; void)</li>
<li> (searchable=true)</li>
<li>/</li>
<li> (maxDisplayItems)</li>
<li> × </li>
<li> × </li>
</ul>
</div>
</div>
</div>
{/* 代码示例 */}
<div className="mt-8 bg-gray-900 rounded-lg p-4 text-white">
<h3 className="text-md font-semibold mb-3"></h3>
<pre className="text-sm overflow-x-auto">
{`// 单选组件
<CustomSelect
value={singleValue}
onChange={setSingleValue}
options={categoryOptions}
placeholder="请选择一个类别"
/>
// 多选组件
<CustomMultiSelect
value={multiValue}
onChange={setMultiValue}
options={categoryOptions}
placeholder="请选择多个类别"
maxDisplayItems={2}
searchable={true}
/>`}
</pre>
</div>
</div>
</div>
);
};
export default MultiSelectExample;

View File

@@ -9,6 +9,7 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useNotifications } from '../NotificationSystem'; import { useNotifications } from '../NotificationSystem';
import { PROJECT_ID } from './const';
interface OutfitAnalysis { interface OutfitAnalysis {
id: string; id: string;
@@ -24,12 +25,10 @@ interface OutfitAnalysis {
} }
interface OutfitAnalysisResultProps { interface OutfitAnalysisResultProps {
projectId: string;
onCreateItems?: (analysisId: string) => void; onCreateItems?: (analysisId: string) => void;
} }
const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({ const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({
projectId,
onCreateItems onCreateItems
}) => { }) => {
const [analyses, setAnalyses] = useState<OutfitAnalysis[]>([]); const [analyses, setAnalyses] = useState<OutfitAnalysis[]>([]);
@@ -42,7 +41,7 @@ const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({
try { try {
setLoading(true); setLoading(true);
const options = { const options = {
project_id: projectId, project_id: PROJECT_ID,
status: null, status: null,
limit: 50, limit: 50,
offset: 0 offset: 0
@@ -66,7 +65,7 @@ const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({
const handleCreateItems = async (analysisId: string) => { const handleCreateItems = async (analysisId: string) => {
try { try {
await invoke('create_outfit_items_from_analysis', { await invoke('create_outfit_items_from_analysis', {
projectId, projectId: PROJECT_ID,
analysisId analysisId
}); });
@@ -127,17 +126,15 @@ const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({
}; };
useEffect(() => { useEffect(() => {
if (projectId) { loadAnalyses();
// 设置定时刷新,检查分析状态
const interval = setInterval(() => {
loadAnalyses(); loadAnalyses();
}, 5000); // 每5秒刷新一次
// 设置定时刷新,检查分析状态 return () => clearInterval(interval);
const interval = setInterval(() => { }, []);
loadAnalyses();
}, 5000); // 每5秒刷新一次
return () => clearInterval(interval);
}
}, [projectId]);
if (loading) { if (loading) {
return ( return (

View File

@@ -100,7 +100,7 @@ const OutfitCard: React.FC<OutfitCardProps> = ({
} }
}; };
const getCategoryIcon = (category: string) => { const getCategoryIcon = (_category: string) => {
// 这里可以根据类别返回不同的图标 // 这里可以根据类别返回不同的图标
return '👕'; // 简化处理,实际应用中可以使用更具体的图标 return '👕'; // 简化处理,实际应用中可以使用更具体的图标
}; };
@@ -171,7 +171,7 @@ const OutfitCard: React.FC<OutfitCardProps> = ({
</h4> </h4>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{matching.items.slice(0, 4).map((item, index) => ( {matching.items.slice(0, 4).map((item) => (
<div key={item.item_id} className="flex items-center space-x-2 p-2 bg-gray-50 rounded-md"> <div key={item.item_id} className="flex items-center space-x-2 p-2 bg-gray-50 rounded-md">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<div <div

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { import {
CheckCircleIcon,
PhotoIcon, PhotoIcon,
TagIcon, TagIcon,
SparklesIcon SparklesIcon

View File

@@ -1,12 +1,12 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
XMarkIcon, XMarkIcon,
PhotoIcon,
PlusIcon, PlusIcon,
XCircleIcon XCircleIcon
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useNotifications } from '../NotificationSystem'; import { useNotifications } from '../NotificationSystem';
import { PROJECT_ID } from './const';
interface OutfitItem { interface OutfitItem {
id: string; id: string;
@@ -50,7 +50,6 @@ interface OutfitItemFormProps {
} }
const OutfitItemForm: React.FC<OutfitItemFormProps> = ({ const OutfitItemForm: React.FC<OutfitItemFormProps> = ({
projectId,
item, item,
isOpen, isOpen,
onClose, onClose,
@@ -142,7 +141,7 @@ const OutfitItemForm: React.FC<OutfitItemFormProps> = ({
try { try {
const requestData = { const requestData = {
project_id: projectId, project_id: PROJECT_ID,
analysis_id: item?.analysis_id || null, analysis_id: item?.analysis_id || null,
name: formData.name.trim(), name: formData.name.trim(),
category: formData.category.trim(), category: formData.category.trim(),

View File

@@ -9,6 +9,7 @@ import {
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useNotifications } from '../NotificationSystem'; import { useNotifications } from '../NotificationSystem';
import { PROJECT_ID } from './const';
interface OutfitItem { interface OutfitItem {
id: string; id: string;
@@ -53,7 +54,7 @@ const OutfitItemList: React.FC<OutfitItemListProps> = ({
try { try {
setLoading(true); setLoading(true);
const options = { const options = {
project_id: projectId, project_id: PROJECT_ID,
category: selectedCategory || null, category: selectedCategory || null,
limit: 100, limit: 100,
offset: 0 offset: 0

View File

@@ -1,17 +1,16 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
SparklesIcon, SparklesIcon,
HeartIcon, HeartIcon,
ShareIcon,
BookmarkIcon, BookmarkIcon,
EyeIcon, EyeIcon,
PlusIcon,
ArrowPathIcon, ArrowPathIcon,
AdjustmentsHorizontalIcon AdjustmentsHorizontalIcon
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import { HeartIcon as HeartSolidIcon, BookmarkIcon as BookmarkSolidIcon } from '@heroicons/react/24/solid'; import { HeartIcon as HeartSolidIcon, BookmarkIcon as BookmarkSolidIcon } from '@heroicons/react/24/solid';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useNotifications } from '../NotificationSystem'; import { useNotifications } from '../NotificationSystem';
import OutfitSearchPanel from './OutfitSearchPanel';
interface OutfitItem { interface OutfitItem {
id: string; id: string;
@@ -51,11 +50,29 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
const [favorites, setFavorites] = useState<Set<string>>(new Set()); const [favorites, setFavorites] = useState<Set<string>>(new Set());
const [savedItems, setSavedItems] = useState<Set<string>>(new Set()); const [savedItems, setSavedItems] = useState<Set<string>>(new Set());
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
occasion: '', categories: [] as string[],
season: '', styles: [] as string[],
style: '', occasions: [] as string[],
minScore: 0.7 seasons: [] as string[],
colors: [] as string[],
minScore: 0.7,
confidenceLevel: '',
matchingTypes: [] as string[]
}); });
// 处理筛选器变化
const handleFiltersChange = (newFilters: any) => {
setFilters({
categories: newFilters.categories || [],
styles: newFilters.styles || [],
occasions: newFilters.occasions || [],
seasons: newFilters.seasons || [],
colors: newFilters.colors || [],
minScore: newFilters.minScore || 0.7,
confidenceLevel: newFilters.confidenceLevel || '',
matchingTypes: newFilters.matchingTypes || []
});
};
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
const { addNotification } = useNotifications(); const { addNotification } = useNotifications();
@@ -88,9 +105,9 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
project_id: projectId, project_id: projectId,
max_recommendations: 12, max_recommendations: 12,
min_score_threshold: filters.minScore, min_score_threshold: filters.minScore,
occasion_filter: filters.occasion || null, occasion_filter: filters.occasions.length > 0 ? filters.occasions[0] : null,
season_filter: filters.season || null, season_filter: filters.seasons.length > 0 ? filters.seasons[0] : null,
style_filter: filters.style || null style_filter: filters.styles.length > 0 ? filters.styles.join(',') : null
}; };
const result = await invoke('generate_outfit_recommendations', { request }); const result = await invoke('generate_outfit_recommendations', { request });
@@ -185,9 +202,23 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
// 过滤推荐 // 过滤推荐
const filteredRecommendations = recommendations.filter(rec => { const filteredRecommendations = recommendations.filter(rec => {
if (filters.occasion && !rec.occasion_tags.includes(filters.occasion)) return false; // 场合筛选
if (filters.season && !rec.season_tags.includes(filters.season)) return false; if (filters.occasions.length > 0 && !filters.occasions.some(occasion => rec.occasion_tags.includes(occasion))) {
if (filters.style && !rec.style_description.toLowerCase().includes(filters.style.toLowerCase())) return false; return false;
}
// 季节筛选
if (filters.seasons.length > 0 && !filters.seasons.some(season => rec.season_tags.includes(season))) {
return false;
}
// 风格筛选
if (filters.styles.length > 0 && !filters.styles.some(style =>
rec.style_description.toLowerCase().includes(style.toLowerCase())
)) {
return false;
}
return rec.score >= filters.minScore; return rec.score >= filters.minScore;
}); });
@@ -243,84 +274,11 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
{/* 筛选面板 */} {/* 筛选面板 */}
{showFilters && ( {showFilters && (
<div className="bg-gray-50 rounded-lg p-4 space-y-4"> <OutfitSearchPanel
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> filters={filters}
<div> onFiltersChange={handleFiltersChange}
<label className="block text-sm font-medium text-gray-700 mb-1"> onSearch={generateRecommendations}
/>
</label>
<select
value={filters.occasion}
onChange={(e) => setFilters(prev => ({ ...prev, occasion: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value=""></option>
<option value="工作"></option>
<option value="休闲"></option>
<option value="正式"></option>
<option value="运动"></option>
<option value="约会"></option>
<option value="聚会"></option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
value={filters.season}
onChange={(e) => setFilters(prev => ({ ...prev, season: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value=""></option>
<option value="春季"></option>
<option value="夏季"></option>
<option value="秋季"></option>
<option value="冬季"></option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
value={filters.style}
onChange={(e) => setFilters(prev => ({ ...prev, style: e.target.value }))}
placeholder="输入风格关键词"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
value={filters.minScore}
onChange={(e) => setFilters(prev => ({ ...prev, minScore: parseFloat(e.target.value) }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value={0.5}>50</option>
<option value={0.6}>60</option>
<option value={0.7}>70</option>
<option value={0.8}>80</option>
<option value={0.9}>90</option>
</select>
</div>
</div>
<div className="flex justify-end">
<button
onClick={generateRecommendations}
className="px-4 py-2 bg-primary-600 text-white text-sm font-medium rounded-md hover:bg-primary-700"
>
</button>
</div>
</div>
)} )}
{/* 推荐列表 */} {/* 推荐列表 */}
@@ -356,7 +314,7 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
{/* 搭配预览 */} {/* 搭配预览 */}
<div className="aspect-square bg-gray-100 relative p-4"> <div className="aspect-square bg-gray-100 relative p-4">
<div className="grid grid-cols-2 gap-2 h-full"> <div className="grid grid-cols-2 gap-2 h-full">
{recommendation.items.slice(0, 4).map((item, index) => ( {recommendation.items.slice(0, 4).map((item) => (
<div <div
key={item.id} key={item.id}
className="bg-white rounded-lg border border-gray-200 overflow-hidden" className="bg-white rounded-lg border border-gray-200 overflow-hidden"

View File

@@ -3,9 +3,8 @@ import {
MagnifyingGlassIcon, MagnifyingGlassIcon,
FunnelIcon, FunnelIcon,
XMarkIcon, XMarkIcon,
AdjustmentsHorizontalIcon
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import {CustomSelect} from '../CustomSelect'; import {CustomMultiSelect, CustomSelect} from '../CustomSelect';
interface SearchFilters { interface SearchFilters {
categories?: string[]; categories?: string[];
@@ -185,12 +184,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
</label> </label>
<CustomSelect <CustomMultiSelect
options={categoryOptions} options={categoryOptions}
value={filters.categories || []} value={filters.categories || []}
onChange={(value) => handleFilterChange('categories', value)} onChange={(value) => handleFilterChange('categories', value)}
placeholder="选择类别" placeholder="选择类别"
multiple
/> />
</div> </div>
@@ -199,12 +197,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
</label> </label>
<CustomSelect <CustomMultiSelect
options={styleOptions} options={styleOptions}
value={filters.styles || []} value={filters.styles || []}
onChange={(value) => handleFilterChange('styles', value)} onChange={(value) => handleFilterChange('styles', value)}
placeholder="选择风格" placeholder="选择风格"
multiple
/> />
</div> </div>
@@ -213,12 +210,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
</label> </label>
<CustomSelect <CustomMultiSelect
options={occasionOptions} options={occasionOptions}
value={filters.occasions || []} value={filters.occasions || []}
onChange={(value) => handleFilterChange('occasions', value)} onChange={(value) => handleFilterChange('occasions', value)}
placeholder="选择场合" placeholder="选择场合"
multiple
/> />
</div> </div>
@@ -227,12 +223,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
</label> </label>
<CustomSelect <CustomMultiSelect
options={seasonOptions} options={seasonOptions}
value={filters.seasons || []} value={filters.seasons || []}
onChange={(value) => handleFilterChange('seasons', value)} onChange={(value) => handleFilterChange('seasons', value)}
placeholder="选择季节" placeholder="选择季节"
multiple
/> />
</div> </div>
@@ -241,12 +236,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
</label> </label>
<CustomSelect <CustomMultiSelect
options={colorOptions} options={colorOptions}
value={filters.colors || []} value={filters.colors || []}
onChange={(value) => handleFilterChange('colors', value)} onChange={(value) => handleFilterChange('colors', value)}
placeholder="选择颜色" placeholder="选择颜色"
multiple
/> />
</div> </div>
@@ -255,12 +249,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
</label> </label>
<CustomSelect <CustomMultiSelect
options={matchingTypeOptions} options={matchingTypeOptions}
value={filters.matchingTypes || []} value={filters.matchingTypes || []}
onChange={(value) => handleFilterChange('matchingTypes', value)} onChange={(value) => handleFilterChange('matchingTypes', value)}
placeholder="选择搭配类型" placeholder="选择搭配类型"
multiple
/> />
</div> </div>
</div> </div>

View File

@@ -0,0 +1 @@
export const PROJECT_ID = "gen-lang-client-0413414134"

View File

@@ -17,9 +17,9 @@ import OutfitItemList from '../components/outfit/OutfitItemList';
import OutfitItemForm from '../components/outfit/OutfitItemForm'; import OutfitItemForm from '../components/outfit/OutfitItemForm';
import OutfitMatchingRecommendation from '../components/outfit/OutfitMatchingRecommendation'; import OutfitMatchingRecommendation from '../components/outfit/OutfitMatchingRecommendation';
import { useNotifications } from '../components/NotificationSystem'; import { useNotifications } from '../components/NotificationSystem';
import { PROJECT_ID } from '../components/outfit/const';
interface OutfitMatchProps { interface OutfitMatchProps {
projectId?: string;
} }
// 定义OutfitItem接口 // 定义OutfitItem接口
@@ -43,7 +43,9 @@ interface OutfitItem {
updated_at: string; updated_at: string;
} }
const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => { const OutfitMatch: React.FC<OutfitMatchProps> = ({ }) => {
// 使用传入的projectId或默认的PROJECT_ID
const currentProjectId = PROJECT_ID;
const [activeTab, setActiveTab] = useState<'upload' | 'analysis' | 'items' | 'matching'>('upload'); const [activeTab, setActiveTab] = useState<'upload' | 'analysis' | 'items' | 'matching'>('upload');
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [showItemForm, setShowItemForm] = useState(false); const [showItemForm, setShowItemForm] = useState(false);
@@ -52,15 +54,6 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
// 处理图像上传 // 处理图像上传
const handleImageUpload = async (files: File[]) => { const handleImageUpload = async (files: File[]) => {
if (!projectId) {
addNotification({
type: 'error',
title: '错误',
message: '项目ID不存在无法上传图片'
});
return;
}
setIsUploading(true); setIsUploading(true);
try { try {
@@ -72,7 +65,7 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
// 保存文件到服务器 // 保存文件到服务器
const savedPath = await invoke('save_outfit_image', { const savedPath = await invoke('save_outfit_image', {
projectId: projectId, projectId: currentProjectId,
fileName: file.name, fileName: file.name,
fileData: Array.from(uint8Array) fileData: Array.from(uint8Array)
}); });
@@ -81,7 +74,7 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
// 创建分析记录 // 创建分析记录
const analysisRequest = { const analysisRequest = {
project_id: projectId, project_id: currentProjectId,
image_path: savedPath, image_path: savedPath,
image_name: file.name image_name: file.name
}; };
@@ -160,11 +153,6 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
<p className="text-primary-100 mt-1">AI智能分析服装搭配</p> <p className="text-primary-100 mt-1">AI智能分析服装搭配</p>
</div> </div>
</div> </div>
{projectId && (
<div className="inline-flex items-center px-3 py-1 rounded-full bg-white bg-opacity-20 backdrop-blur-sm">
<span className="text-sm font-medium">: {projectId}</span>
</div>
)}
</div> </div>
{/* 装饰性背景元素 */} {/* 装饰性背景元素 */}
@@ -186,8 +174,8 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
key={key} key={key}
onClick={() => setActiveTab(key as any)} onClick={() => setActiveTab(key as any)}
className={`group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm ${activeTab === key className={`group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm ${activeTab === key
? 'border-primary-500 text-primary-600' ? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`} }`}
> >
<Icon className="h-5 w-5 mr-2" /> <Icon className="h-5 w-5 mr-2" />
@@ -233,16 +221,13 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
</p> </p>
</div> </div>
{projectId && ( <OutfitAnalysisResult
<OutfitAnalysisResult onCreateItems={(analysisId) => {
projectId={projectId} console.log('创建服装单品:', analysisId);
onCreateItems={(analysisId) => { // 切换到服装单品标签页
console.log('创建服装单品:', analysisId); setActiveTab('items');
// 切换到服装单品标签页 }}
setActiveTab('items'); />
}}
/>
)}
</div> </div>
)} )}
@@ -258,13 +243,11 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
</p> </p>
</div> </div>
{projectId && ( <OutfitItemList
<OutfitItemList projectId={currentProjectId}
projectId={projectId} onCreateItem={handleCreateItem}
onCreateItem={handleCreateItem} onEditItem={handleEditItem}
onEditItem={handleEditItem} />
/>
)}
</div> </div>
)} )}
@@ -280,19 +263,17 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
</p> </p>
</div> </div>
{projectId && ( <OutfitMatchingRecommendation
<OutfitMatchingRecommendation projectId={currentProjectId}
projectId={projectId} onSaveMatching={(recommendation) => {
onSaveMatching={(recommendation) => { console.log('保存搭配推荐:', recommendation);
console.log('保存搭配推荐:', recommendation); addNotification({
addNotification({ type: 'success',
type: 'success', title: '搭配已保存',
title: '搭配已保存', message: '您可以在我的搭配中查看保存的搭配'
message: '您可以在我的搭配中查看保存的搭配' });
}); }}
}} />
/>
)}
</div> </div>
)} )}
</div> </div>
@@ -392,15 +373,13 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
</div> </div>
{/* 服装单品表单 */} {/* 服装单品表单 */}
{projectId && ( <OutfitItemForm
<OutfitItemForm projectId={currentProjectId}
projectId={projectId} item={editingItem || undefined}
item={editingItem || undefined} isOpen={showItemForm}
isOpen={showItemForm} onClose={handleFormClose}
onClose={handleFormClose} onSave={handleFormSave}
onSave={handleFormSave} />
/>
)}
</div> </div>
); );
}; };

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, FolderOpen, Upload, FileVideo, FileAudio, FileImage, HardDrive, Brain, Loader2, Link, Layers, Calendar, MapPin, Users, CheckCircle, Filter, Shuffle, Download, Shirt } from 'lucide-react'; import { ArrowLeft, FolderOpen, Upload, FileVideo, FileAudio, FileImage, HardDrive, Brain, Loader2, Link, Layers, Calendar, MapPin, Users, CheckCircle, Filter, Shuffle, Download } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useProjectStore } from '../store/projectStore'; import { useProjectStore } from '../store/projectStore';
import { useMaterialStore } from '../store/materialStore'; import { useMaterialStore } from '../store/materialStore';
@@ -42,7 +42,6 @@ import { TemplateMatchingResultManager } from '../components/TemplateMatchingRes
import { useNotifications } from '../components/NotificationSystem'; import { useNotifications } from '../components/NotificationSystem';
import { ProjectMaterialUsageOverviewComponent } from '../components/ProjectMaterialUsageOverview'; import { ProjectMaterialUsageOverviewComponent } from '../components/ProjectMaterialUsageOverview';
import { useMaterialUsage } from '../hooks/useMaterialUsage'; import { useMaterialUsage } from '../hooks/useMaterialUsage';
import OutfitMatch from './OutfitMatch';
// 格式化时间 // 格式化时间
const formatTime = (dateString: string) => { const formatTime = (dateString: string) => {
@@ -146,8 +145,8 @@ export const ProjectDetails: React.FC = () => {
const [materialClassificationFilter, setMaterialClassificationFilter] = useState<string>('全部'); const [materialClassificationFilter, setMaterialClassificationFilter] = useState<string>('全部');
const [materialModelFilter, setMaterialModelFilter] = useState<string>('全部'); const [materialModelFilter, setMaterialModelFilter] = useState<string>('全部');
const [materialUsageFilter, setMaterialUsageFilter] = useState<string>('全部'); const [materialUsageFilter, setMaterialUsageFilter] = useState<string>('全部');
const [materialClassificationRecords, setMaterialClassificationRecords] = useState<{[materialId: string]: any[]}>({}); const [materialClassificationRecords, setMaterialClassificationRecords] = useState<{ [materialId: string]: any[] }>({});
const [modelsMap, setModelsMap] = useState<{[modelId: string]: any}>({}); const [modelsMap, setModelsMap] = useState<{ [modelId: string]: any }>({});
// 用于跟踪分类统计是否已加载的ref // 用于跟踪分类统计是否已加载的ref
const classificationStatsLoadedRef = useRef<string | null>(null); const classificationStatsLoadedRef = useRef<string | null>(null);
@@ -176,7 +175,7 @@ export const ProjectDetails: React.FC = () => {
const loadAllModels = useCallback(async () => { const loadAllModels = useCallback(async () => {
try { try {
const models = await invoke('get_all_models') as any[]; const models = await invoke('get_all_models') as any[];
const modelMap: {[modelId: string]: any} = {}; const modelMap: { [modelId: string]: any } = {};
models.forEach(model => { models.forEach(model => {
modelMap[model.id] = model; modelMap[model.id] = model;
}); });
@@ -194,7 +193,7 @@ export const ProjectDetails: React.FC = () => {
const materialsToProcess = materialList || materials; const materialsToProcess = materialList || materials;
// 获取每个素材的分类记录 // 获取每个素材的分类记录
const classificationRecords: {[materialId: string]: any[]} = {}; const classificationRecords: { [materialId: string]: any[] } = {};
for (const material of materialsToProcess) { for (const material of materialsToProcess) {
try { try {
const records = await invoke('get_material_classification_records', { materialId: material.id }) as any[]; const records = await invoke('get_material_classification_records', { materialId: material.id }) as any[];
@@ -440,7 +439,7 @@ export const ProjectDetails: React.FC = () => {
}; };
const result = await MaterialMatchingService.executeMatching(request); const result = await MaterialMatchingService.executeMatching(request);
console.log({result}) console.log({ result })
setMatchingResult(result); setMatchingResult(result);
} catch (error) { } catch (error) {
console.error('素材匹配失败:', error); console.error('素材匹配失败:', error);
@@ -477,7 +476,7 @@ export const ProjectDetails: React.FC = () => {
matchingDurationMs: 0 // 使用默认值 matchingDurationMs: 0 // 使用默认值
}); });
console.log({savedResult}) console.log({ savedResult })
// 创建素材使用记录 // 创建素材使用记录
if (savedResult && typeof savedResult === 'object' && 'id' in savedResult) { if (savedResult && typeof savedResult === 'object' && 'id' in savedResult) {
try { try {
@@ -688,7 +687,7 @@ export const ProjectDetails: React.FC = () => {
const options = [{ label: '全部', value: '全部', count: materials.length }]; const options = [{ label: '全部', value: '全部', count: materials.length }];
// 统计分类信息 // 统计分类信息
const categoryCount: {[category: string]: number} = {}; const categoryCount: { [category: string]: number } = {};
// 遍历所有素材的分类记录 // 遍历所有素材的分类记录
Object.values(materialClassificationRecords).forEach(records => { Object.values(materialClassificationRecords).forEach(records => {
@@ -715,7 +714,7 @@ export const ProjectDetails: React.FC = () => {
const options = [{ label: '全部', value: '全部', count: materials.length }]; const options = [{ label: '全部', value: '全部', count: materials.length }];
// 统计模特信息 // 统计模特信息
const modelCounts: {[key: string]: number} = {}; const modelCounts: { [key: string]: number } = {};
materials.forEach(material => { materials.forEach(material => {
if (material.model_id) { if (material.model_id) {
const modelKey = material.model_id; const modelKey = material.model_id;
@@ -918,11 +917,10 @@ export const ProjectDetails: React.FC = () => {
<nav className="flex space-x-1 px-4 md:px-6 overflow-x-auto" aria-label="Tabs"> <nav className="flex space-x-1 px-4 md:px-6 overflow-x-auto" aria-label="Tabs">
<button <button
onClick={() => setActiveTab('overview')} onClick={() => setActiveTab('overview')}
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${ className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'overview'
activeTab === 'overview'
? 'text-primary-600 border-b-2 border-primary-500' ? 'text-primary-600 border-b-2 border-primary-500'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Brain className="w-4 h-4" /> <Brain className="w-4 h-4" />
@@ -932,11 +930,10 @@ export const ProjectDetails: React.FC = () => {
</button> </button>
<button <button
onClick={() => setActiveTab('materials')} onClick={() => setActiveTab('materials')}
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${ className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'materials'
activeTab === 'materials'
? 'text-primary-600 border-b-2 border-primary-500' ? 'text-primary-600 border-b-2 border-primary-500'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<FolderOpen className="w-4 h-4" /> <FolderOpen className="w-4 h-4" />
@@ -946,11 +943,10 @@ export const ProjectDetails: React.FC = () => {
</button> </button>
<button <button
onClick={() => setActiveTab('segments')} onClick={() => setActiveTab('segments')}
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${ className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'segments'
activeTab === 'segments'
? 'text-primary-600 border-b-2 border-primary-500' ? 'text-primary-600 border-b-2 border-primary-500'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Layers className="w-4 h-4" /> <Layers className="w-4 h-4" />
@@ -960,11 +956,10 @@ export const ProjectDetails: React.FC = () => {
</button> </button>
<button <button
onClick={() => setActiveTab('templates')} onClick={() => setActiveTab('templates')}
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${ className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'templates'
activeTab === 'templates'
? 'text-primary-600 border-b-2 border-primary-500' ? 'text-primary-600 border-b-2 border-primary-500'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Link className="w-4 h-4" /> <Link className="w-4 h-4" />
@@ -974,11 +969,10 @@ export const ProjectDetails: React.FC = () => {
</button> </button>
<button <button
onClick={() => setActiveTab('matching-results')} onClick={() => setActiveTab('matching-results')}
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${ className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'matching-results'
activeTab === 'matching-results'
? 'text-primary-600 border-b-2 border-primary-500' ? 'text-primary-600 border-b-2 border-primary-500'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Brain className="w-4 h-4" /> <Brain className="w-4 h-4" />
@@ -988,11 +982,10 @@ export const ProjectDetails: React.FC = () => {
</button> </button>
<button <button
onClick={() => setActiveTab('export-records')} onClick={() => setActiveTab('export-records')}
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${ className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'export-records'
activeTab === 'export-records'
? 'text-primary-600 border-b-2 border-primary-500' ? 'text-primary-600 border-b-2 border-primary-500'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
@@ -1002,11 +995,10 @@ export const ProjectDetails: React.FC = () => {
</button> </button>
<button <button
onClick={() => setActiveTab('usage-stats')} onClick={() => setActiveTab('usage-stats')}
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${ className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'usage-stats'
activeTab === 'usage-stats'
? 'text-primary-600 border-b-2 border-primary-500' ? 'text-primary-600 border-b-2 border-primary-500'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
@@ -1018,11 +1010,10 @@ export const ProjectDetails: React.FC = () => {
</button> </button>
<button <button
onClick={() => setActiveTab('ai-logs')} onClick={() => setActiveTab('ai-logs')}
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${ className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'ai-logs'
activeTab === 'ai-logs'
? 'text-primary-600 border-b-2 border-primary-500' ? 'text-primary-600 border-b-2 border-primary-500'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`} }`}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<HardDrive className="w-4 h-4" /> <HardDrive className="w-4 h-4" />
@@ -1030,20 +1021,6 @@ export const ProjectDetails: React.FC = () => {
<span className="sm:hidden">AI日志</span> <span className="sm:hidden">AI日志</span>
</div> </div>
</button> </button>
<button
onClick={() => setActiveTab('outfit-match')}
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
activeTab === 'outfit-match'
? 'text-primary-600 border-b-2 border-primary-500'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`}
>
<div className="flex items-center space-x-2">
<Shirt className="w-4 h-4" />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</div>
</button>
</nav> </nav>
</div> </div>
@@ -1159,7 +1136,7 @@ export const ProjectDetails: React.FC = () => {
</div> </div>
)} )}
</div> </div>
)} )}
@@ -1526,13 +1503,6 @@ export const ProjectDetails: React.FC = () => {
<AiAnalysisLogViewer projectId={project.id} /> <AiAnalysisLogViewer projectId={project.id} />
</div> </div>
)} )}
{/* 服装搭配选项卡 */}
{activeTab === 'outfit-match' && project && (
<div className="h-full">
<OutfitMatch projectId={project.id} />
</div>
)}
</div> </div>
</div> </div>
</div> </div>