feat: 完成CustomMultiSelect多选组件开发并集成到顶部导航栏
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -32,3 +34,259 @@ export const CustomSelect: React.FC<{
|
|||||||
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
|||||||
163
apps/desktop/src/components/outfit/MultiSelectExample.tsx
Normal file
163
apps/desktop/src/components/outfit/MultiSelectExample.tsx
Normal 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) => 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[]) => 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;
|
||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
|
||||||
PhotoIcon,
|
PhotoIcon,
|
||||||
TagIcon,
|
TagIcon,
|
||||||
SparklesIcon
|
SparklesIcon
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -2,16 +2,15 @@ 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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
1
apps/desktop/src/components/outfit/const.ts
Normal file
1
apps/desktop/src/components/outfit/const.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const PROJECT_ID = "gen-lang-client-0413414134"
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user