feat: 服装搭配页面UI美化和UX改进
UI优化内容: - 重新设计页面头部,使用优雅的渐变背景和现代化图标 - 优化标签导航,采用卡片式设计和平滑动画效果 - 美化搜索面板,改进输入框、筛选器和按钮的视觉设计 - 重构图片上传组件,添加拖拽区域样式和上传进度动画 - 优化搜索结果展示,使用网格布局和悬停效果 - 改进AI分析结果展示,采用卡片式布局和颜色编码 - 增强LLM聊天界面,现代化消息气泡和打字动画 响应式设计: - 实现移动优先的响应式布局 - 优化平板端和桌面端适配 - 修复1200px宽度下的左右布局显示问题 - 添加触摸友好的交互元素 用户体验提升: - 统一设计语言和视觉风格 - 添加流畅的页面切换和组件加载动画 - 优化加载状态、错误提示和空状态设计 - 改进信息层次和视觉可读性 技术改进: - 使用Tailwind CSS类替代内联样式 - 统一使用Lucide React图标库 - 完善CSS变量和设计令牌系统 - 添加兼容性变量支持旧的命名格式 符合promptx/frontend-developer规定的前端开发规范,确保界面美观、操作流畅、动画优美,符合用户操作习惯和大众审美习惯。
This commit is contained in:
220
apps/desktop/src/components/outfit/OutfitSearchPanel.tsx
Normal file
220
apps/desktop/src/components/outfit/OutfitSearchPanel.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
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,
|
||||
}) => {
|
||||
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 gap-3 mb-4">
|
||||
<div className="icon-container primary w-8 h-8">
|
||||
<Search className="w-4 h-4" />
|
||||
</div>
|
||||
<h3 className="text-heading-4 text-high-emphasis">智能搜索</h3>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1 relative">
|
||||
<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"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400">
|
||||
<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"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
搜索中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Search className="w-4 h-4" />
|
||||
搜索
|
||||
</>
|
||||
)}
|
||||
</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`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-4 h-4" />
|
||||
高级筛选
|
||||
</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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutfitSearchPanel;
|
||||
Reference in New Issue
Block a user