194 lines
7.1 KiB
TypeScript
194 lines
7.1 KiB
TypeScript
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,
|
|
onExternalLinkClick,
|
|
showScore = true,
|
|
compact = false,
|
|
}) => {
|
|
const [imageLoaded, setImageLoaded] = useState(false);
|
|
const [imageError, setImageError] = useState(false);
|
|
|
|
// 处理卡片点击
|
|
const handleCardClick = useCallback((e: React.MouseEvent) => {
|
|
// 如果点击的是按钮或其子元素,不触发卡片选择
|
|
const target = e.target as HTMLElement;
|
|
if (target.closest('button')) {
|
|
return;
|
|
}
|
|
|
|
if (onSelect) {
|
|
onSelect(result);
|
|
}
|
|
}, [result, onSelect]);
|
|
|
|
// 处理外部链接点击
|
|
const handleExternalClick = useCallback((e: React.MouseEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (result.image_url) {
|
|
if (onExternalLinkClick) {
|
|
onExternalLinkClick(result.image_url);
|
|
} else {
|
|
window.open(result.image_url, '_blank', 'noopener,noreferrer');
|
|
}
|
|
}
|
|
}, [result.image_url, onExternalLinkClick]);
|
|
|
|
// 处理图片加载
|
|
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>
|
|
</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;
|