feat: 实现相似度检索工具
- 基于现有search_similar_outfits功能开发独立的相似度检索小工具 - 遵循promptx/tauri-desktop-app-expert开发规范 - 实现完整的前后端架构: * Rust后端命令接口 (similarity_search_commands.rs) * TypeScript类型定义 (similaritySearch.ts) * Zustand状态管理 (similaritySearchStore.ts) * React组件 (SimilaritySearchTool, SimilaritySearchPanel, SimilaritySearchResults, SimilaritySearchCard) * 服务层 (similaritySearchService.ts) 功能特性: - 智能搜索建议和自动完成 - 可调节的相关性阈值 (LOWEST/LOW/MEDIUM/HIGH) - 快速搜索标签 - 响应式网格布局结果展示 - 优雅的加载状态和错误处理 - 遵循UI/UX设计标准的美观界面 技术实现: - 复用现有outfit search API和数据模型 - 简化的搜索配置,专注核心功能 - 完整的TypeScript类型安全 - 现代化的React Hooks和状态管理 - TailwindCSS响应式设计 - 平滑的动画和交互效果 集成: - 添加到快捷工具列表 (/tools/similarity-search) - 配置React Router路由 - 注册Tauri命令处理器
This commit is contained in:
186
apps/desktop/src/components/similarity/SimilaritySearchCard.tsx
Normal file
186
apps/desktop/src/components/similarity/SimilaritySearchCard.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
ExternalLink,
|
||||
Tag,
|
||||
Image as ImageIcon,
|
||||
TrendingUp
|
||||
} from 'lucide-react';
|
||||
import { SimilaritySearchCardProps } from '../../types/similaritySearch';
|
||||
import SimilaritySearchService from '../../services/similaritySearchService';
|
||||
|
||||
/**
|
||||
* 相似度检索结果卡片组件
|
||||
* 遵循设计系统规范,提供统一的结果展示界面
|
||||
*/
|
||||
export const SimilaritySearchCard: React.FC<SimilaritySearchCardProps> = ({
|
||||
result,
|
||||
onSelect,
|
||||
showScore = true,
|
||||
compact = false,
|
||||
}) => {
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
// 处理卡片点击
|
||||
const handleCardClick = useCallback(() => {
|
||||
if (onSelect) {
|
||||
onSelect(result);
|
||||
}
|
||||
}, [result, onSelect]);
|
||||
|
||||
// 处理外部链接点击
|
||||
const handleExternalClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (result.image_url) {
|
||||
window.open(result.image_url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}, [result.image_url]);
|
||||
|
||||
// 处理图片加载
|
||||
const handleImageLoad = useCallback(() => {
|
||||
setImageLoaded(true);
|
||||
}, []);
|
||||
|
||||
// 处理图片错误
|
||||
const handleImageError = useCallback(() => {
|
||||
setImageError(true);
|
||||
setImageLoaded(true);
|
||||
}, []);
|
||||
|
||||
// 获取相关性评分样式
|
||||
const scoreColor = SimilaritySearchService.getRelevanceScoreColor(result.relevance_score);
|
||||
const formattedScore = SimilaritySearchService.formatRelevanceScore(result.relevance_score);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
card card-interactive group cursor-pointer animate-fade-in-up
|
||||
relative overflow-hidden bg-gradient-to-br from-white to-gray-50/50
|
||||
hover:from-white hover:to-primary-50/30 transition-all duration-500
|
||||
hover:shadow-lg hover:shadow-primary-500/10 hover:-translate-y-1
|
||||
border border-gray-200 hover:border-primary-300
|
||||
${compact ? 'p-4' : 'p-6'}
|
||||
`}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
{/* 装饰性背景 */}
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-br from-primary-100 to-primary-200 rounded-full -translate-y-12 translate-x-12 opacity-40 group-hover:opacity-60 transition-all duration-500 group-hover:scale-110"></div>
|
||||
|
||||
{/* 相关性评分 */}
|
||||
{showScore && (
|
||||
<div className="absolute top-3 right-3 z-10">
|
||||
<div className={`px-2 py-1 rounded-full text-xs font-medium bg-white shadow-sm border ${scoreColor}`}>
|
||||
<div className="flex items-center gap-1">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
{formattedScore}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
{/* 图片区域 */}
|
||||
<div className={`relative ${compact ? 'h-32' : 'h-40'} bg-gray-100 rounded-lg overflow-hidden mb-4`}>
|
||||
{result.image_url && !imageError ? (
|
||||
<>
|
||||
<img
|
||||
src={result.image_url}
|
||||
alt={result.style_description}
|
||||
className={`w-full h-full object-cover transition-all duration-300 ${
|
||||
imageLoaded ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
||||
<ImageIcon className="w-8 h-8" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 外部链接按钮 */}
|
||||
{result.image_url && (
|
||||
<button
|
||||
onClick={handleExternalClick}
|
||||
className="absolute top-2 left-2 p-1.5 bg-white/90 hover:bg-white rounded-lg shadow-sm transition-all duration-200 opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 text-gray-600" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="space-y-3">
|
||||
{/* 标题和描述 */}
|
||||
<div>
|
||||
<h3 className={`font-semibold text-high-emphasis line-clamp-2 ${
|
||||
compact ? 'text-sm' : 'text-base'
|
||||
}`}>
|
||||
{result.style_description || '未知风格'}
|
||||
</h3>
|
||||
{result.id && (
|
||||
<p className="text-xs text-medium-emphasis mt-1">
|
||||
ID: {result.id}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 环境标签 */}
|
||||
{result.environment_tags && result.environment_tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{result.environment_tags.slice(0, compact ? 2 : 3).map((tag: string, index: number) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded-full"
|
||||
>
|
||||
<Tag className="w-2.5 h-2.5" />
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{result.environment_tags.length > (compact ? 2 : 3) && (
|
||||
<span className="inline-flex items-center px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">
|
||||
+{result.environment_tags.length - (compact ? 2 : 3)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 产品信息 */}
|
||||
{result.products && result.products.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-medium-emphasis font-medium">产品信息</div>
|
||||
<div className="space-y-1">
|
||||
{result.products.slice(0, compact ? 1 : 2).map((product: any, index: number) => (
|
||||
<div key={index} className="text-xs text-gray-600">
|
||||
<span className="font-medium">{product.category}</span>
|
||||
{product.description && (
|
||||
<span className="ml-2 text-gray-500 line-clamp-1">
|
||||
{product.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{result.products.length > (compact ? 1 : 2) && (
|
||||
<div className="text-xs text-gray-500">
|
||||
还有 {result.products.length - (compact ? 1 : 2)} 个产品...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 悬停效果 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-transparent via-transparent to-primary-50/20 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimilaritySearchCard;
|
||||
Reference in New Issue
Block a user