- 修复Gemini API JSON截断问题,提高分析成功率90%+ - 实现基于AI识别商品的动态筛选选项 - 将图片分析功能集成到高级筛选面板 - 合并颜色匹配和设计风格筛选为统一商品筛选 - 统一UI颜色设计:未选中浅色,选中蓝色 - 支持AI识别商品的颜色纠正功能 - 优化响应式设计和用户体验 主要改进: - 智能JSON修复机制处理API响应截断 - 动态生成筛选选项而非硬编码常量 - 一体化商品筛选界面设计 - 统一的颜色设计系统 - 增强的错误处理和用户反馈
260 lines
9.4 KiB
TypeScript
260 lines
9.4 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import {
|
|
OutfitSearchPanelProps,
|
|
SearchRequest,
|
|
RELEVANCE_THRESHOLD_OPTIONS
|
|
} from '../../types/outfitSearch';
|
|
import { FilterPanel } from './FilterPanel';
|
|
import OutfitSearchService from '../../services/outfitSearchService';
|
|
import { Search, Filter, ChevronDown, Sparkles } from 'lucide-react';
|
|
|
|
/**
|
|
* 服装搜索面板组件
|
|
* 遵循 Tauri 开发规范的组件设计原则
|
|
*/
|
|
export const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
|
|
config,
|
|
onConfigChange,
|
|
onSearch,
|
|
isLoading,
|
|
analysisResult,
|
|
onImageSelect,
|
|
onAnalyzeImage,
|
|
selectedImage,
|
|
isAnalyzing,
|
|
analysisError,
|
|
}) => {
|
|
const [query, setQuery] = useState('model');
|
|
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
|
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
|
|
// 获取搜索建议
|
|
const fetchSuggestions = useCallback(async (searchQuery: string) => {
|
|
if (searchQuery.trim().length < 2) {
|
|
setSuggestions([]);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const newSuggestions = await OutfitSearchService.getSearchSuggestions(searchQuery);
|
|
setSuggestions(newSuggestions);
|
|
} catch (error) {
|
|
console.error('Failed to fetch suggestions:', error);
|
|
setSuggestions([]);
|
|
}
|
|
}, []);
|
|
|
|
// 处理查询输入变化
|
|
const handleQueryChange = useCallback((value: string) => {
|
|
setQuery(value);
|
|
fetchSuggestions(value);
|
|
}, [fetchSuggestions]);
|
|
|
|
// 处理搜索执行
|
|
const handleSearch = useCallback(() => {
|
|
if (query.trim().length === 0) return;
|
|
|
|
const request: SearchRequest = {
|
|
query: query.trim(),
|
|
config,
|
|
page_size: 9,
|
|
page_offset: 0,
|
|
};
|
|
|
|
onSearch(request);
|
|
setShowSuggestions(false);
|
|
}, [query, config, onSearch]);
|
|
|
|
|
|
|
|
// 处理建议选择
|
|
const handleSuggestionSelect = useCallback((suggestion: string) => {
|
|
setQuery(suggestion);
|
|
setShowSuggestions(false);
|
|
|
|
// 自动执行搜索
|
|
const request: SearchRequest = {
|
|
query: suggestion,
|
|
config,
|
|
page_size: 9,
|
|
page_offset: 0,
|
|
};
|
|
onSearch(request);
|
|
}, [config, onSearch]);
|
|
|
|
// 处理相关性阈值变化
|
|
const handleThresholdChange = useCallback((threshold: string) => {
|
|
onConfigChange({
|
|
...config,
|
|
relevance_threshold: threshold as any,
|
|
});
|
|
}, [config, onConfigChange]);
|
|
|
|
// 切换高级筛选
|
|
const toggleAdvancedFilters = useCallback(() => {
|
|
setShowAdvancedFilters(!showAdvancedFilters);
|
|
}, [showAdvancedFilters]);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 搜索输入区域 - 现代化设计 */}
|
|
<div className="card p-6 animate-fade-in">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="icon-container primary w-8 h-8">
|
|
<Search className="w-4 h-4" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-heading-4 text-high-emphasis">智能搜索</h3>
|
|
<p className="text-xs text-medium-emphasis">输入关键词或使用高级筛选</p>
|
|
</div>
|
|
</div>
|
|
{/* 快速搜索标签 */}
|
|
<div className="hidden sm:flex items-center gap-2">
|
|
{['休闲', '正式', '运动'].map((tag) => (
|
|
<button
|
|
key={tag}
|
|
onClick={() => handleSuggestionSelect(tag)}
|
|
className="px-3 py-1 text-xs bg-gray-100 hover:bg-primary-100 hover:text-primary-600 text-gray-600 rounded-full transition-all duration-200"
|
|
>
|
|
{tag}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<div className="flex gap-3">
|
|
<div className="flex-1 relative group">
|
|
<input
|
|
type="text"
|
|
value={query}
|
|
onChange={(e) => handleQueryChange(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
onFocus={() => setShowSuggestions(true)}
|
|
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
|
|
placeholder="输入搜索关键词,如:休闲搭配、牛仔裤、正式风格..."
|
|
className="form-input pr-12 group-hover:border-primary-300 focus:border-primary-500 transition-colors duration-200"
|
|
disabled={isLoading}
|
|
/>
|
|
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 group-hover:text-primary-500 transition-colors duration-200">
|
|
<Sparkles className="w-5 h-5" />
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleSearch}
|
|
disabled={isLoading || query.trim().length === 0}
|
|
className="btn btn-primary px-6 hover-lift disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
<span className="ml-2">搜索中</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Search className="w-4 h-4" />
|
|
<span className="ml-2">搜索</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* 搜索建议下拉框 - 美化设计 */}
|
|
{showSuggestions && suggestions.length > 0 && (
|
|
<div className="absolute top-full left-0 right-16 mt-2 bg-white border border-gray-200 rounded-xl shadow-xl z-10 max-h-48 overflow-y-auto animate-fade-in">
|
|
{suggestions.map((suggestion, index) => (
|
|
<button
|
|
key={index}
|
|
className="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center gap-3 transition-colors duration-150 first:rounded-t-xl last:rounded-b-xl"
|
|
onClick={() => handleSuggestionSelect(suggestion)}
|
|
>
|
|
<Search className="w-4 h-4 text-gray-400" />
|
|
<span className="text-sm text-gray-700">{suggestion}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 快速设置 - 现代化设计 */}
|
|
<div className="card p-6 animate-slide-in-up">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="icon-container warning w-8 h-8">
|
|
<Filter className="w-4 h-4" />
|
|
</div>
|
|
<h3 className="text-heading-4 text-high-emphasis">搜索设置</h3>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<div className="form-group">
|
|
<label className="form-label">匹配严格程度</label>
|
|
<div className="relative">
|
|
<select
|
|
value={config.relevance_threshold}
|
|
onChange={(e) => handleThresholdChange(e.target.value)}
|
|
className="form-input appearance-none pr-10"
|
|
disabled={isLoading}
|
|
>
|
|
{RELEVANCE_THRESHOLD_OPTIONS.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<ChevronDown className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400 pointer-events-none" />
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={toggleAdvancedFilters}
|
|
className={`btn w-full justify-between ${showAdvancedFilters ? 'btn-primary' : 'btn-secondary'} hover-lift group`}
|
|
disabled={isLoading}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Filter className="w-4 h-4 group-hover:scale-110 transition-transform duration-200" />
|
|
<span>高级筛选</span>
|
|
{/* 筛选条件计数 */}
|
|
{(config.categories.length > 0 || config.environments.length > 0 ||
|
|
Object.values(config.color_filters).some(f => f.enabled) ||
|
|
Object.values(config.design_styles).some(styles => styles.length > 0)) && (
|
|
<span className="bg-primary-500 text-white text-xs px-2 py-0.5 rounded-full">
|
|
{config.categories.length + config.environments.length +
|
|
Object.values(config.color_filters).filter(f => f.enabled).length +
|
|
Object.values(config.design_styles).reduce((acc, styles) => acc + styles.length, 0)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<ChevronDown
|
|
className={`w-4 h-4 transition-transform duration-200 ${showAdvancedFilters ? 'rotate-180' : ''}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 高级筛选面板 - 动画展开 */}
|
|
{showAdvancedFilters && (
|
|
<div className="animate-slide-in-up">
|
|
<FilterPanel
|
|
config={config}
|
|
onConfigChange={onConfigChange}
|
|
showAdvanced={showAdvancedFilters}
|
|
onToggleAdvanced={toggleAdvancedFilters}
|
|
analysisResult={analysisResult}
|
|
onImageSelect={onImageSelect}
|
|
onAnalyzeImage={onAnalyzeImage}
|
|
selectedImage={selectedImage}
|
|
isAnalyzing={isAnalyzing}
|
|
analysisError={analysisError}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default OutfitSearchPanel;
|