feat: 完成CustomMultiSelect多选组件开发并集成到顶部导航栏
This commit is contained in:
@@ -81,7 +81,7 @@ function App() {
|
||||
<Route path="/templates" element={<TemplateManagement />} />
|
||||
<Route path="/material-model-binding" element={<MaterialModelBinding />} />
|
||||
<Route path="/tools" element={<Tools />} />
|
||||
<Route path="/outfit-match/:projectId" element={<OutfitMatch />} />
|
||||
<Route path="/outfit-match" element={<OutfitMatch />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { XMarkIcon, CheckIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
// 自定义下拉选择组件
|
||||
// 单选下拉选择组件
|
||||
export const CustomSelect: React.FC<{
|
||||
value: string | number | null | undefined;
|
||||
onChange: (value: string) => void;
|
||||
@@ -32,3 +34,259 @@ export const CustomSelect: React.FC<{
|
||||
</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,
|
||||
DocumentDuplicateIcon,
|
||||
LinkIcon,
|
||||
WrenchScrewdriverIcon
|
||||
WrenchScrewdriverIcon,
|
||||
SparklesIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const Navigation: React.FC = () => {
|
||||
@@ -43,6 +44,12 @@ const Navigation: React.FC = () => {
|
||||
icon: CpuChipIcon,
|
||||
description: '管理AI视频分类规则'
|
||||
},
|
||||
{
|
||||
name: '服装搭配',
|
||||
href: '/outfit-match',
|
||||
icon: SparklesIcon,
|
||||
description: 'AI智能服装搭配推荐'
|
||||
},
|
||||
{
|
||||
name: '便捷工具',
|
||||
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';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useNotifications } from '../NotificationSystem';
|
||||
import { PROJECT_ID } from './const';
|
||||
|
||||
interface OutfitAnalysis {
|
||||
id: string;
|
||||
@@ -24,12 +25,10 @@ interface OutfitAnalysis {
|
||||
}
|
||||
|
||||
interface OutfitAnalysisResultProps {
|
||||
projectId: string;
|
||||
onCreateItems?: (analysisId: string) => void;
|
||||
}
|
||||
|
||||
const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({
|
||||
projectId,
|
||||
onCreateItems
|
||||
}) => {
|
||||
const [analyses, setAnalyses] = useState<OutfitAnalysis[]>([]);
|
||||
@@ -42,7 +41,7 @@ const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({
|
||||
try {
|
||||
setLoading(true);
|
||||
const options = {
|
||||
project_id: projectId,
|
||||
project_id: PROJECT_ID,
|
||||
status: null,
|
||||
limit: 50,
|
||||
offset: 0
|
||||
@@ -66,7 +65,7 @@ const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({
|
||||
const handleCreateItems = async (analysisId: string) => {
|
||||
try {
|
||||
await invoke('create_outfit_items_from_analysis', {
|
||||
projectId,
|
||||
projectId: PROJECT_ID,
|
||||
analysisId
|
||||
});
|
||||
|
||||
@@ -127,17 +126,15 @@ const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
loadAnalyses();
|
||||
|
||||
// 设置定时刷新,检查分析状态
|
||||
const interval = setInterval(() => {
|
||||
loadAnalyses();
|
||||
}, 5000); // 每5秒刷新一次
|
||||
|
||||
// 设置定时刷新,检查分析状态
|
||||
const interval = setInterval(() => {
|
||||
loadAnalyses();
|
||||
}, 5000); // 每5秒刷新一次
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [projectId]);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
@@ -100,7 +100,7 @@ const OutfitCard: React.FC<OutfitCardProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
const getCategoryIcon = (_category: string) => {
|
||||
// 这里可以根据类别返回不同的图标
|
||||
return '👕'; // 简化处理,实际应用中可以使用更具体的图标
|
||||
};
|
||||
@@ -171,7 +171,7 @@ const OutfitCard: React.FC<OutfitCardProps> = ({
|
||||
</h4>
|
||||
|
||||
<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 className="flex-shrink-0">
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
PhotoIcon,
|
||||
TagIcon,
|
||||
SparklesIcon
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
XMarkIcon,
|
||||
PhotoIcon,
|
||||
PlusIcon,
|
||||
XCircleIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useNotifications } from '../NotificationSystem';
|
||||
import { PROJECT_ID } from './const';
|
||||
|
||||
interface OutfitItem {
|
||||
id: string;
|
||||
@@ -50,7 +50,6 @@ interface OutfitItemFormProps {
|
||||
}
|
||||
|
||||
const OutfitItemForm: React.FC<OutfitItemFormProps> = ({
|
||||
projectId,
|
||||
item,
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -142,7 +141,7 @@ const OutfitItemForm: React.FC<OutfitItemFormProps> = ({
|
||||
|
||||
try {
|
||||
const requestData = {
|
||||
project_id: projectId,
|
||||
project_id: PROJECT_ID,
|
||||
analysis_id: item?.analysis_id || null,
|
||||
name: formData.name.trim(),
|
||||
category: formData.category.trim(),
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useNotifications } from '../NotificationSystem';
|
||||
import { PROJECT_ID } from './const';
|
||||
|
||||
interface OutfitItem {
|
||||
id: string;
|
||||
@@ -53,7 +54,7 @@ const OutfitItemList: React.FC<OutfitItemListProps> = ({
|
||||
try {
|
||||
setLoading(true);
|
||||
const options = {
|
||||
project_id: projectId,
|
||||
project_id: PROJECT_ID,
|
||||
category: selectedCategory || null,
|
||||
limit: 100,
|
||||
offset: 0
|
||||
|
||||
@@ -2,16 +2,15 @@ import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
SparklesIcon,
|
||||
HeartIcon,
|
||||
ShareIcon,
|
||||
BookmarkIcon,
|
||||
EyeIcon,
|
||||
PlusIcon,
|
||||
ArrowPathIcon,
|
||||
AdjustmentsHorizontalIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { HeartIcon as HeartSolidIcon, BookmarkIcon as BookmarkSolidIcon } from '@heroicons/react/24/solid';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useNotifications } from '../NotificationSystem';
|
||||
import OutfitSearchPanel from './OutfitSearchPanel';
|
||||
|
||||
interface OutfitItem {
|
||||
id: string;
|
||||
@@ -51,11 +50,29 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
|
||||
const [favorites, setFavorites] = useState<Set<string>>(new Set());
|
||||
const [savedItems, setSavedItems] = useState<Set<string>>(new Set());
|
||||
const [filters, setFilters] = useState({
|
||||
occasion: '',
|
||||
season: '',
|
||||
style: '',
|
||||
minScore: 0.7
|
||||
categories: [] as string[],
|
||||
styles: [] as string[],
|
||||
occasions: [] as string[],
|
||||
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 { addNotification } = useNotifications();
|
||||
|
||||
@@ -88,9 +105,9 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
|
||||
project_id: projectId,
|
||||
max_recommendations: 12,
|
||||
min_score_threshold: filters.minScore,
|
||||
occasion_filter: filters.occasion || null,
|
||||
season_filter: filters.season || null,
|
||||
style_filter: filters.style || null
|
||||
occasion_filter: filters.occasions.length > 0 ? filters.occasions[0] : null,
|
||||
season_filter: filters.seasons.length > 0 ? filters.seasons[0] : null,
|
||||
style_filter: filters.styles.length > 0 ? filters.styles.join(',') : null
|
||||
};
|
||||
|
||||
const result = await invoke('generate_outfit_recommendations', { request });
|
||||
@@ -185,9 +202,23 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
|
||||
|
||||
// 过滤推荐
|
||||
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.style && !rec.style_description.toLowerCase().includes(filters.style.toLowerCase())) return false;
|
||||
// 场合筛选
|
||||
if (filters.occasions.length > 0 && !filters.occasions.some(occasion => rec.occasion_tags.includes(occasion))) {
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -243,84 +274,11 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
|
||||
|
||||
{/* 筛选面板 */}
|
||||
{showFilters && (
|
||||
<div className="bg-gray-50 rounded-lg p-4 space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
场合
|
||||
</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>
|
||||
<OutfitSearchPanel
|
||||
filters={filters}
|
||||
onFiltersChange={handleFiltersChange}
|
||||
onSearch={generateRecommendations}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 推荐列表 */}
|
||||
@@ -356,7 +314,7 @@ const OutfitMatchingRecommendation: React.FC<OutfitMatchingRecommendationProps>
|
||||
{/* 搭配预览 */}
|
||||
<div className="aspect-square bg-gray-100 relative p-4">
|
||||
<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
|
||||
key={item.id}
|
||||
className="bg-white rounded-lg border border-gray-200 overflow-hidden"
|
||||
|
||||
@@ -3,9 +3,8 @@ import {
|
||||
MagnifyingGlassIcon,
|
||||
FunnelIcon,
|
||||
XMarkIcon,
|
||||
AdjustmentsHorizontalIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import {CustomSelect} from '../CustomSelect';
|
||||
import {CustomMultiSelect, CustomSelect} from '../CustomSelect';
|
||||
|
||||
interface SearchFilters {
|
||||
categories?: string[];
|
||||
@@ -185,12 +184,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
服装类别
|
||||
</label>
|
||||
<CustomSelect
|
||||
<CustomMultiSelect
|
||||
options={categoryOptions}
|
||||
value={filters.categories || []}
|
||||
onChange={(value) => handleFilterChange('categories', value)}
|
||||
placeholder="选择类别"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -199,12 +197,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
风格
|
||||
</label>
|
||||
<CustomSelect
|
||||
<CustomMultiSelect
|
||||
options={styleOptions}
|
||||
value={filters.styles || []}
|
||||
onChange={(value) => handleFilterChange('styles', value)}
|
||||
placeholder="选择风格"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -213,12 +210,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
适用场合
|
||||
</label>
|
||||
<CustomSelect
|
||||
<CustomMultiSelect
|
||||
options={occasionOptions}
|
||||
value={filters.occasions || []}
|
||||
onChange={(value) => handleFilterChange('occasions', value)}
|
||||
placeholder="选择场合"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -227,12 +223,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
适用季节
|
||||
</label>
|
||||
<CustomSelect
|
||||
<CustomMultiSelect
|
||||
options={seasonOptions}
|
||||
value={filters.seasons || []}
|
||||
onChange={(value) => handleFilterChange('seasons', value)}
|
||||
placeholder="选择季节"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -241,12 +236,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
主要颜色
|
||||
</label>
|
||||
<CustomSelect
|
||||
<CustomMultiSelect
|
||||
options={colorOptions}
|
||||
value={filters.colors || []}
|
||||
onChange={(value) => handleFilterChange('colors', value)}
|
||||
placeholder="选择颜色"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -255,12 +249,11 @@ const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
搭配类型
|
||||
</label>
|
||||
<CustomSelect
|
||||
<CustomMultiSelect
|
||||
options={matchingTypeOptions}
|
||||
value={filters.matchingTypes || []}
|
||||
onChange={(value) => handleFilterChange('matchingTypes', value)}
|
||||
placeholder="选择搭配类型"
|
||||
multiple
|
||||
/>
|
||||
</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 OutfitMatchingRecommendation from '../components/outfit/OutfitMatchingRecommendation';
|
||||
import { useNotifications } from '../components/NotificationSystem';
|
||||
import { PROJECT_ID } from '../components/outfit/const';
|
||||
|
||||
interface OutfitMatchProps {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
// 定义OutfitItem接口
|
||||
@@ -43,7 +43,9 @@ interface OutfitItem {
|
||||
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 [isUploading, setIsUploading] = useState(false);
|
||||
const [showItemForm, setShowItemForm] = useState(false);
|
||||
@@ -52,15 +54,6 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
|
||||
// 处理图像上传
|
||||
const handleImageUpload = async (files: File[]) => {
|
||||
if (!projectId) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '错误',
|
||||
message: '项目ID不存在,无法上传图片'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
@@ -72,7 +65,7 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
|
||||
// 保存文件到服务器
|
||||
const savedPath = await invoke('save_outfit_image', {
|
||||
projectId: projectId,
|
||||
projectId: currentProjectId,
|
||||
fileName: file.name,
|
||||
fileData: Array.from(uint8Array)
|
||||
});
|
||||
@@ -81,7 +74,7 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
|
||||
// 创建分析记录
|
||||
const analysisRequest = {
|
||||
project_id: projectId,
|
||||
project_id: currentProjectId,
|
||||
image_path: savedPath,
|
||||
image_name: file.name
|
||||
};
|
||||
@@ -160,11 +153,6 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
<p className="text-primary-100 mt-1">AI智能分析服装搭配,发现完美组合</p>
|
||||
</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>
|
||||
|
||||
{/* 装饰性背景元素 */}
|
||||
@@ -186,8 +174,8 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
key={key}
|
||||
onClick={() => setActiveTab(key as any)}
|
||||
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-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-5 w-5 mr-2" />
|
||||
@@ -233,16 +221,13 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{projectId && (
|
||||
<OutfitAnalysisResult
|
||||
projectId={projectId}
|
||||
onCreateItems={(analysisId) => {
|
||||
console.log('创建服装单品:', analysisId);
|
||||
// 切换到服装单品标签页
|
||||
setActiveTab('items');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<OutfitAnalysisResult
|
||||
onCreateItems={(analysisId) => {
|
||||
console.log('创建服装单品:', analysisId);
|
||||
// 切换到服装单品标签页
|
||||
setActiveTab('items');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -258,13 +243,11 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{projectId && (
|
||||
<OutfitItemList
|
||||
projectId={projectId}
|
||||
onCreateItem={handleCreateItem}
|
||||
onEditItem={handleEditItem}
|
||||
/>
|
||||
)}
|
||||
<OutfitItemList
|
||||
projectId={currentProjectId}
|
||||
onCreateItem={handleCreateItem}
|
||||
onEditItem={handleEditItem}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -280,19 +263,17 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{projectId && (
|
||||
<OutfitMatchingRecommendation
|
||||
projectId={projectId}
|
||||
onSaveMatching={(recommendation) => {
|
||||
console.log('保存搭配推荐:', recommendation);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: '搭配已保存',
|
||||
message: '您可以在我的搭配中查看保存的搭配'
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<OutfitMatchingRecommendation
|
||||
projectId={currentProjectId}
|
||||
onSaveMatching={(recommendation) => {
|
||||
console.log('保存搭配推荐:', recommendation);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: '搭配已保存',
|
||||
message: '您可以在我的搭配中查看保存的搭配'
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -392,15 +373,13 @@ const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
</div>
|
||||
|
||||
{/* 服装单品表单 */}
|
||||
{projectId && (
|
||||
<OutfitItemForm
|
||||
projectId={projectId}
|
||||
item={editingItem || undefined}
|
||||
isOpen={showItemForm}
|
||||
onClose={handleFormClose}
|
||||
onSave={handleFormSave}
|
||||
/>
|
||||
)}
|
||||
<OutfitItemForm
|
||||
projectId={currentProjectId}
|
||||
item={editingItem || undefined}
|
||||
isOpen={showItemForm}
|
||||
onClose={handleFormClose}
|
||||
onSave={handleFormSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
||||
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 { useProjectStore } from '../store/projectStore';
|
||||
import { useMaterialStore } from '../store/materialStore';
|
||||
@@ -42,7 +42,6 @@ import { TemplateMatchingResultManager } from '../components/TemplateMatchingRes
|
||||
import { useNotifications } from '../components/NotificationSystem';
|
||||
import { ProjectMaterialUsageOverviewComponent } from '../components/ProjectMaterialUsageOverview';
|
||||
import { useMaterialUsage } from '../hooks/useMaterialUsage';
|
||||
import OutfitMatch from './OutfitMatch';
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string) => {
|
||||
@@ -146,8 +145,8 @@ export const ProjectDetails: React.FC = () => {
|
||||
const [materialClassificationFilter, setMaterialClassificationFilter] = useState<string>('全部');
|
||||
const [materialModelFilter, setMaterialModelFilter] = useState<string>('全部');
|
||||
const [materialUsageFilter, setMaterialUsageFilter] = useState<string>('全部');
|
||||
const [materialClassificationRecords, setMaterialClassificationRecords] = useState<{[materialId: string]: any[]}>({});
|
||||
const [modelsMap, setModelsMap] = useState<{[modelId: string]: any}>({});
|
||||
const [materialClassificationRecords, setMaterialClassificationRecords] = useState<{ [materialId: string]: any[] }>({});
|
||||
const [modelsMap, setModelsMap] = useState<{ [modelId: string]: any }>({});
|
||||
|
||||
// 用于跟踪分类统计是否已加载的ref
|
||||
const classificationStatsLoadedRef = useRef<string | null>(null);
|
||||
@@ -176,7 +175,7 @@ export const ProjectDetails: React.FC = () => {
|
||||
const loadAllModels = useCallback(async () => {
|
||||
try {
|
||||
const models = await invoke('get_all_models') as any[];
|
||||
const modelMap: {[modelId: string]: any} = {};
|
||||
const modelMap: { [modelId: string]: any } = {};
|
||||
models.forEach(model => {
|
||||
modelMap[model.id] = model;
|
||||
});
|
||||
@@ -194,7 +193,7 @@ export const ProjectDetails: React.FC = () => {
|
||||
const materialsToProcess = materialList || materials;
|
||||
|
||||
// 获取每个素材的分类记录
|
||||
const classificationRecords: {[materialId: string]: any[]} = {};
|
||||
const classificationRecords: { [materialId: string]: any[] } = {};
|
||||
for (const material of materialsToProcess) {
|
||||
try {
|
||||
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);
|
||||
console.log({result})
|
||||
console.log({ result })
|
||||
setMatchingResult(result);
|
||||
} catch (error) {
|
||||
console.error('素材匹配失败:', error);
|
||||
@@ -477,7 +476,7 @@ export const ProjectDetails: React.FC = () => {
|
||||
matchingDurationMs: 0 // 使用默认值
|
||||
});
|
||||
|
||||
console.log({savedResult})
|
||||
console.log({ savedResult })
|
||||
// 创建素材使用记录
|
||||
if (savedResult && typeof savedResult === 'object' && 'id' in savedResult) {
|
||||
try {
|
||||
@@ -688,7 +687,7 @@ export const ProjectDetails: React.FC = () => {
|
||||
const options = [{ label: '全部', value: '全部', count: materials.length }];
|
||||
|
||||
// 统计分类信息
|
||||
const categoryCount: {[category: string]: number} = {};
|
||||
const categoryCount: { [category: string]: number } = {};
|
||||
|
||||
// 遍历所有素材的分类记录
|
||||
Object.values(materialClassificationRecords).forEach(records => {
|
||||
@@ -715,7 +714,7 @@ export const ProjectDetails: React.FC = () => {
|
||||
const options = [{ label: '全部', value: '全部', count: materials.length }];
|
||||
|
||||
// 统计模特信息
|
||||
const modelCounts: {[key: string]: number} = {};
|
||||
const modelCounts: { [key: string]: number } = {};
|
||||
materials.forEach(material => {
|
||||
if (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">
|
||||
<button
|
||||
onClick={() => setActiveTab('overview')}
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
|
||||
activeTab === 'overview'
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'overview'
|
||||
? '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">
|
||||
<Brain className="w-4 h-4" />
|
||||
@@ -932,11 +930,10 @@ export const ProjectDetails: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('materials')}
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
|
||||
activeTab === 'materials'
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'materials'
|
||||
? '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">
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
@@ -946,11 +943,10 @@ export const ProjectDetails: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('segments')}
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
|
||||
activeTab === 'segments'
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'segments'
|
||||
? '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">
|
||||
<Layers className="w-4 h-4" />
|
||||
@@ -960,11 +956,10 @@ export const ProjectDetails: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('templates')}
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
|
||||
activeTab === 'templates'
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'templates'
|
||||
? '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">
|
||||
<Link className="w-4 h-4" />
|
||||
@@ -974,11 +969,10 @@ export const ProjectDetails: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('matching-results')}
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
|
||||
activeTab === 'matching-results'
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'matching-results'
|
||||
? '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">
|
||||
<Brain className="w-4 h-4" />
|
||||
@@ -988,11 +982,10 @@ export const ProjectDetails: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('export-records')}
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
|
||||
activeTab === 'export-records'
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'export-records'
|
||||
? '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">
|
||||
<Download className="w-4 h-4" />
|
||||
@@ -1002,11 +995,10 @@ export const ProjectDetails: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('usage-stats')}
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
|
||||
activeTab === 'usage-stats'
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'usage-stats'
|
||||
? '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">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
@@ -1018,11 +1010,10 @@ export const ProjectDetails: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('ai-logs')}
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
|
||||
activeTab === 'ai-logs'
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${activeTab === 'ai-logs'
|
||||
? '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">
|
||||
<HardDrive className="w-4 h-4" />
|
||||
@@ -1030,20 +1021,6 @@ export const ProjectDetails: React.FC = () => {
|
||||
<span className="sm:hidden">AI日志</span>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -1526,13 +1503,6 @@ export const ProjectDetails: React.FC = () => {
|
||||
<AiAnalysisLogViewer projectId={project.id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 服装搭配选项卡 */}
|
||||
{activeTab === 'outfit-match' && project && (
|
||||
<div className="h-full">
|
||||
<OutfitMatch projectId={project.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user