fix: 修复相似度检索工具事件冒泡问题和Rust编译错误

- 修复SimilaritySearchCard中外部链接按钮点击时触发卡片选择的事件冒泡问题
- 增强外部链接按钮事件处理,添加preventDefault和stopPropagation
- 改进卡片点击事件处理,检查点击目标避免按钮触发卡片选择
- 修复outfit_search_commands.rs中的lifetime错误,重构描述字段解析逻辑
- 移除相关性阈值过滤逻辑,返回所有搜索结果
- 优化相似度检索工具UI布局和详情模态框功能
- 添加详细的搜索结果展示和外部链接处理功能
This commit is contained in:
imeepos 2025-07-24 16:45:57 +08:00
parent f859bb5322
commit 90aa401059
5 changed files with 213 additions and 66 deletions

View File

@ -594,14 +594,7 @@ fn convert_vertex_response_to_search_results(
// 应用相关性阈值过滤
let threshold = request.config.relevance_threshold.to_value();
if search_result.relevance_score >= threshold {
results.push(search_result);
} else {
eprintln!(
"结果被过滤: 评分 {:.2} < 阈值 {:.2}",
search_result.relevance_score, threshold
);
}
results.push(search_result);
} else {
eprintln!("解析搜索结果失败");
}
@ -752,11 +745,26 @@ fn parse_vertex_product_info(value: &serde_json::Value) -> Result<ProductInfo, a
.unwrap_or("服装")
.to_string();
let description = value
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("时尚单品")
.to_string();
// 尝试从多个字段获取描述信息
let description = if let Some(desc) = value.get("description").and_then(|v| v.as_str()) {
desc.to_string()
} else if let Some(styles_array) = value.get("design_styles").and_then(|v| v.as_array()) {
if !styles_array.is_empty() {
let styles: Vec<String> = styles_array.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect();
if !styles.is_empty() {
styles.join("")
} else {
"时尚单品".to_string()
}
} else {
"时尚单品".to_string()
}
} else {
"时尚单品".to_string()
};
let color_pattern = value
.get("color_pattern")

View File

@ -15,6 +15,7 @@ import SimilaritySearchService from '../../services/similaritySearchService';
export const SimilaritySearchCard: React.FC<SimilaritySearchCardProps> = ({
result,
onSelect,
onExternalLinkClick,
showScore = true,
compact = false,
}) => {
@ -22,7 +23,13 @@ export const SimilaritySearchCard: React.FC<SimilaritySearchCardProps> = ({
const [imageError, setImageError] = useState(false);
// 处理卡片点击
const handleCardClick = useCallback(() => {
const handleCardClick = useCallback((e: React.MouseEvent) => {
// 如果点击的是按钮或其子元素,不触发卡片选择
const target = e.target as HTMLElement;
if (target.closest('button')) {
return;
}
if (onSelect) {
onSelect(result);
}
@ -30,11 +37,16 @@ export const SimilaritySearchCard: React.FC<SimilaritySearchCardProps> = ({
// 处理外部链接点击
const handleExternalClick = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (result.image_url) {
window.open(result.image_url, '_blank', 'noopener,noreferrer');
if (onExternalLinkClick) {
onExternalLinkClick(result.image_url);
} else {
window.open(result.image_url, '_blank', 'noopener,noreferrer');
}
}
}, [result.image_url]);
}, [result.image_url, onExternalLinkClick]);
// 处理图片加载
const handleImageLoad = useCallback(() => {

View File

@ -26,6 +26,7 @@ export const SimilaritySearchResults: React.FC<SimilaritySearchResultsProps> = (
onPageChange,
onPageSizeChange,
onResultSelect,
onExternalLinkClick,
}) => {
// 计算分页信息
const totalPages = SimilaritySearchService.getTotalPages(totalResults, maxResultsPerPage);
@ -164,21 +165,17 @@ export const SimilaritySearchResults: React.FC<SimilaritySearchResultsProps> = (
</p>
</div>
</div>
<div className="flex items-center gap-2 text-xs text-green-600 bg-white px-3 py-1.5 rounded-full border border-green-200">
<Clock className="w-3 h-3" />
<span></span>
</div>
</div>
</div>
{/* 结果网格 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 lg:gap-6">
{results.map((result, index) => (
<SimilaritySearchCard
key={result.id || index}
result={result}
onSelect={handleResultSelect}
onExternalLinkClick={onExternalLinkClick}
showScore={true}
compact={true}
/>
@ -197,7 +194,7 @@ export const SimilaritySearchResults: React.FC<SimilaritySearchResultsProps> = (
{onPageSizeChange && (
<div className="flex items-center gap-2 text-sm">
<span className="text-medium-emphasis">:</span>
<span className="text-medium-emphasis"></span>
<select
value={maxResultsPerPage}
onChange={(e) => handlePageSizeChange(Number(e.target.value))}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useCallback } from 'react';
import React, { useEffect, useCallback, useState } from 'react';
import {
Search,
Sparkles,
@ -6,7 +6,11 @@ import {
TrendingUp,
Zap,
ArrowLeft,
Loader2
Loader2,
X,
ExternalLink,
Copy,
Download
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import {
@ -26,6 +30,10 @@ import { CustomSelect } from '../../components/CustomSelect';
const SimilaritySearchTool: React.FC = () => {
const navigate = useNavigate();
// 本地状态
const [selectedResult, setSelectedResult] = useState<any>(null);
const [showDetailModal, setShowDetailModal] = useState(false);
// 状态管理
const {
query,
@ -106,6 +114,25 @@ const SimilaritySearchTool: React.FC = () => {
changePageSize(pageSize);
}, [changePageSize]);
// 处理结果选择
const handleResultSelect = useCallback((result: any) => {
console.log('Selected result:', result);
setSelectedResult(result);
setShowDetailModal(true);
}, []);
// 处理外部链接打开
const handleOpenExternal = useCallback((url: string) => {
// 直接使用 window.open 打开外部链接
window.open(url, '_blank', 'noopener,noreferrer');
}, []);
// 关闭详细信息模态框
const handleCloseDetailModal = useCallback(() => {
setShowDetailModal(false);
setSelectedResult(null);
}, []);
// 监听相关性阈值变化,立即重新搜索
useEffect(() => {
const request: SimilaritySearchRequest = {
@ -126,12 +153,12 @@ const SimilaritySearchTool: React.FC = () => {
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-50">
{/* 页面头部 */}
<div className="bg-white border-b border-gray-100 shadow-sm">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6">
<div className="mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={handleBackToTools}
className="flex items-center gap-2 px-3 py-2 text-gray-600 hover:text-primary-600 hover:bg-gray-50 rounded-lg transition-all duration-200"
className="flex items-center gap-2 py-2 text-gray-600 hover:text-primary-600 hover:bg-gray-50 rounded-lg transition-all duration-200"
>
<ArrowLeft className="w-4 h-4" />
<span className="text-sm font-medium"></span>
@ -151,40 +178,6 @@ const SimilaritySearchTool: React.FC = () => {
</div>
<div className="flex items-center gap-3">
{/* 相关性阈值选择器 */}
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-600">:</span>
<div className="flex items-center gap-2">
<CustomSelect
value={selectedThreshold}
onChange={setThreshold}
options={
configState.config?.available_thresholds.map(threshold => ({
value: threshold.value,
label: threshold.label,
description: threshold.description
})) || [
{ value: "LOWEST", label: "最低 (0.3)", description: "显示更多相关结果" },
{ value: "LOW", label: "较低 (0.5)", description: "包含较多相关结果" },
{ value: "MEDIUM", label: "中等 (0.7)", description: "平衡相关性和数量" },
{ value: "HIGH", label: "较高 (0.9)", description: "只显示高度相关结果" }
]
}
placeholder="选择相关性阈值"
className="min-w-[160px]"
disabled={searchState.isSearching}
/>
{searchState.isSearching && (
<div className="flex items-center gap-1 text-primary-600">
<Loader2 className="w-4 h-4 animate-spin" />
<span className="text-xs">...</span>
</div>
)}
</div>
</div>
<div className="h-6 w-px bg-gray-200"></div>
<button
onClick={handleReset}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all duration-200"
@ -211,9 +204,9 @@ const SimilaritySearchTool: React.FC = () => {
)}
{/* 主要内容 */}
<div className="container mx-auto py-6 lg:py-8">
<div className="mx-auto py-6 lg:py-8">
{/* 搜索面板和结果 */}
<div className="grid grid-cols-1 lg:grid-cols-[400px_1fr] gap-6 lg:gap-8">
<div className="grid grid-cols-1 lg:grid-cols-[300px_1fr] gap-6 lg:gap-8">
{/* 搜索面板 */}
<div className="space-y-6 order-2 lg:order-1">
<SimilaritySearchPanel
@ -240,9 +233,8 @@ const SimilaritySearchTool: React.FC = () => {
isLoading={searchState.isSearching}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
onResultSelect={(result: any) => {
console.log('Selected result:', result);
}}
onResultSelect={handleResultSelect}
onExternalLinkClick={handleOpenExternal}
/>
) : searchState.error ? (
<div className="card h-full flex flex-col items-center justify-center text-center p-6 sm:p-12 animate-fade-in bg-gradient-to-br from-red-50 to-pink-50 border border-red-200">
@ -279,6 +271,142 @@ const SimilaritySearchTool: React.FC = () => {
</div>
</div>
</div>
{/* 详细信息模态框 */}
{showDetailModal && selectedResult && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden">
{/* 模态框头部 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h3 className="text-xl font-semibold text-gray-900"></h3>
<button
onClick={handleCloseDetailModal}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
{/* 模态框内容 */}
<div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
<div className="space-y-6">
{/* 图片展示 */}
<div className="relative">
<img
src={selectedResult.image_url}
alt={selectedResult.style_description}
className="w-full h-64 object-cover rounded-lg"
/>
<div className="absolute top-3 right-3">
<div className="px-3 py-1 bg-white/90 backdrop-blur-sm rounded-full text-sm font-medium">
: {(selectedResult.relevance_score * 100).toFixed(1)}%
</div>
</div>
</div>
{/* 基本信息 */}
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2"></h4>
<p className="text-gray-900 bg-gray-50 p-3 rounded-lg">
{selectedResult.style_description || '暂无描述'}
</p>
</div>
{/* 环境标签 */}
{selectedResult.environment_tags && selectedResult.environment_tags.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2"></h4>
<div className="flex flex-wrap gap-2">
{selectedResult.environment_tags.map((tag: string, index: number) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{tag}
</span>
))}
</div>
</div>
)}
{/* 产品信息 */}
{selectedResult.products && selectedResult.products.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2"></h4>
<div className="space-y-3">
{selectedResult.products.map((product: any, index: number) => (
<div key={index} className="bg-gray-50 p-4 rounded-lg border border-gray-200">
{/* 产品类别 */}
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-semibold text-gray-900">{product.category}</span>
{product.color_pattern?.rgb_hex && (
<div
className="w-4 h-4 rounded-full border border-gray-300"
style={{ backgroundColor: product.color_pattern.rgb_hex }}
title={`颜色: ${product.color_pattern.rgb_hex}`}
/>
)}
</div>
{/* 产品描述 */}
{product.description && product.description !== "时尚单品" && (
<div className="text-sm text-gray-700 mb-2">{product.description}</div>
)}
{/* 设计风格 */}
{product.design_styles && product.design_styles.length > 0 && (
<div className="flex flex-wrap gap-1">
{product.design_styles.map((style: string, styleIndex: number) => (
<span
key={styleIndex}
className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"
>
{style}
</span>
))}
</div>
)}
{/* 颜色信息详细 */}
{product.color_pattern && (
<div className="mt-2 text-xs text-gray-500">
<div className="grid grid-cols-3 gap-2">
<span>: {(product.color_pattern.hue * 360).toFixed(0)}°</span>
<span>: {(product.color_pattern.saturation * 100).toFixed(0)}%</span>
<span>: {(product.color_pattern.value * 100).toFixed(0)}%</span>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
{/* 操作按钮 */}
<div className="flex gap-3 pt-4 border-t border-gray-200">
<button
onClick={() => handleOpenExternal(selectedResult.image_url)}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
<ExternalLink className="w-4 h-4" />
</button>
<button
onClick={() => navigator.clipboard.writeText(selectedResult.image_url)}
className="flex items-center justify-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
>
<Copy className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};

View File

@ -88,11 +88,13 @@ export interface SimilaritySearchResultsProps {
onPageChange: (page: number) => void;
onPageSizeChange?: (pageSize: number) => void;
onResultSelect?: (result: SearchResult) => void;
onExternalLinkClick?: (url: string) => void;
}
export interface SimilaritySearchCardProps {
result: SearchResult;
onSelect?: (result: SearchResult) => void;
onExternalLinkClick?: (url: string) => void;
showScore?: boolean;
compact?: boolean;
}