'use client'; import { useState, useMemo, useCallback } from 'react'; import { ExternalLink, Image as ImageIcon } from 'lucide-react'; import { ImageLightbox } from '@/components/ui/ImageLightbox'; import { cn } from '@/lib/utils'; export interface SearchImageItem { title: string; imageUrl: string; width: number; height: number; score: string; position: number; sourceUrl?: string; } interface SearchImagesGridProps { images: SearchImageItem[]; className?: string; /** 最大显示图片数量,默认为 5 */ maxDisplay?: number; } /** * 搜索图片瀑布流展示组件 * 实现动态回填:当图片加载失败时,自动用备用图片替换 * 确保始终显示指定数量的有效图片 */ export function SearchImagesGrid({ images, className, maxDisplay = 5 }: SearchImagesGridProps) { const [lightboxOpen, setLightboxOpen] = useState(false); const [lightboxIndex, setLightboxIndex] = useState(0); // 已加载成功的图片索引 const [loadedImages, setLoadedImages] = useState>(new Set()); // 加载失败的图片索引 const [errorImages, setErrorImages] = useState>(new Set()); /** * 计算当前要显示的图片列表 * 策略:从所有图片中选择前 maxDisplay 张有效的图片 * 跳过已知失败的图片,自动用后续图片回填 */ const displayImages = useMemo(() => { const result: { image: SearchImageItem; originalIndex: number }[] = []; for (let i = 0; i < images.length && result.length < maxDisplay; i++) { // 跳过已知加载失败的图片 if (!errorImages.has(i)) { result.push({ image: images[i], originalIndex: i }); } } return result; }, [images, errorImages, maxDisplay]); // 有效图片的 URL 列表(用于 Lightbox) const validImageUrls = useMemo(() => { return displayImages .filter(({ originalIndex }) => loadedImages.has(originalIndex)) .map(({ image }) => image.imageUrl); }, [displayImages, loadedImages]); // 图片加载成功 const handleImageLoad = useCallback((index: number) => { setLoadedImages((prev) => new Set(prev).add(index)); }, []); // 图片加载失败 - 标记为错误,会触发重新计算 displayImages,自动回填 const handleImageError = useCallback((index: number) => { setErrorImages((prev) => new Set(prev).add(index)); }, []); // 打开灯箱 const openLightbox = useCallback((displayIndex: number) => { // 找到在有效加载图片中的索引 const loadedDisplayImages = displayImages.filter( ({ originalIndex }) => loadedImages.has(originalIndex) ); const clickedImage = displayImages[displayIndex]; const lightboxIdx = loadedDisplayImages.findIndex( ({ originalIndex }) => originalIndex === clickedImage?.originalIndex ); if (lightboxIdx >= 0) { setLightboxIndex(lightboxIdx); setLightboxOpen(true); } }, [displayImages, loadedImages]); // 获取源网站域名 const getSourceDomain = (url?: string) => { if (!url) return null; try { const urlObj = new URL(url); return urlObj.hostname.replace('www.', ''); } catch { return null; } }; if (!images || images.length === 0) return null; // 如果所有图片都加载失败,不显示任何内容 if (displayImages.length === 0) { return null; } // 统计已成功加载的图片数量 const successCount = displayImages.filter( ({ originalIndex }) => loadedImages.has(originalIndex) ).length; return ( <>
{/* 标题 */}
搜索到 {successCount > 0 ? successCount : displayImages.length} 张相关图片
{/* 瀑布流容器 */}
{displayImages.map(({ image, originalIndex }, displayIndex) => { const isLoaded = loadedImages.has(originalIndex); const sourceDomain = getSourceDomain(image.sourceUrl); return (
{/* 图片容器 */}
openLightbox(displayIndex)} > {/* 图片 */} {image.title handleImageLoad(originalIndex)} onError={() => handleImageError(originalIndex)} loading="lazy" /> {/* 加载占位 */} {!isLoaded && (
)} {/* 悬浮遮罩 */}
{/* 图片信息 */}
); })}
{/* 图片灯箱 - 只包含已成功加载的图片 */} setLightboxOpen(false)} /> ); }