fix: 修复相似度检索工具事件冒泡问题和Rust编译错误
- 修复SimilaritySearchCard中外部链接按钮点击时触发卡片选择的事件冒泡问题 - 增强外部链接按钮事件处理,添加preventDefault和stopPropagation - 改进卡片点击事件处理,检查点击目标避免按钮触发卡片选择 - 修复outfit_search_commands.rs中的lifetime错误,重构描述字段解析逻辑 - 移除相关性阈值过滤逻辑,返回所有搜索结果 - 优化相似度检索工具UI布局和详情模态框功能 - 添加详细的搜索结果展示和外部链接处理功能
This commit is contained in:
parent
f859bb5322
commit
90aa401059
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue