"use client" import { useState, useEffect, useCallback, useRef } from "react" import Image from "next/image" import { Card } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Calendar, MapPin, Grid, List, Eye, Heart, RefreshCw } from "lucide-react" import { useDrag } from "@use-gesture/react" import { useSpring, animated } from "react-spring" interface Photo { id: number src: string title: string description: string category: string tags: string[] date: string exif: { camera: string lens: string settings: string location: string } } interface PhotoGalleryProps { photos: Photo[] onPhotoClick: (photo: Photo) => void onRefresh?: () => Promise } type ViewMode = 'grid' | 'masonry' | 'list' export function PhotoGallery({ photos, onPhotoClick, onRefresh }: PhotoGalleryProps) { const [loadedImages, setLoadedImages] = useState>(new Set()) const [isHydrated, setIsHydrated] = useState(false) const [viewMode, setViewMode] = useState('grid') const [page, setPage] = useState(1) const [displayedPhotos, setDisplayedPhotos] = useState([]) const [isLoading, setIsLoading] = useState(false) const [hasMore, setHasMore] = useState(true) const [isRefreshing, setIsRefreshing] = useState(false) const loaderRef = useRef(null) const galleryRef = useRef(null) // 下拉刷新动画 const [{ y }, api] = useSpring(() => ({ y: 0 })) // 下拉刷新手势 const bindRefresh = useDrag( ({ down, movement: [, my], velocity: [, vy], direction: [, yDir] }) => { // 只在页面顶部且向下拖拽时触发 const isAtTop = window.scrollY === 0 const isPullingDown = yDir > 0 && my > 0 if (!isAtTop || !isPullingDown) return const refreshThreshold = 60 const shouldRefresh = my > refreshThreshold || vy > 0.3 if (shouldRefresh && !down && !isRefreshing && onRefresh) { handleRefresh() } api.start({ y: down ? Math.min(my, 80) : 0, immediate: down, config: { tension: 300, friction: 30 } }) }, { axis: 'y', bounds: { top: 0 }, rubberband: true, filterTaps: true, } ) const handleRefresh = async () => { if (!onRefresh || isRefreshing) return setIsRefreshing(true) try { await onRefresh() setPage(1) setHasMore(true) } catch (error) { console.error('刷新失败:', error) } finally { setIsRefreshing(false) } } const photosPerPage = 12 useEffect(() => { setIsHydrated(true) }, []) // 初始化显示的照片 useEffect(() => { if (photos.length > 0) { setDisplayedPhotos(photos.slice(0, photosPerPage)) setHasMore(photos.length > photosPerPage) setPage(1) } }, [photos]) // 加载更多照片 const loadMorePhotos = useCallback(() => { if (isLoading || !hasMore) return setIsLoading(true) // 模拟API加载延迟 setTimeout(() => { const nextPage = page + 1 const startIndex = (nextPage - 1) * photosPerPage const endIndex = startIndex + photosPerPage const newPhotos = photos.slice(startIndex, endIndex) if (newPhotos.length > 0) { setDisplayedPhotos(prev => [...prev, ...newPhotos]) setPage(nextPage) setHasMore(endIndex < photos.length) } else { setHasMore(false) } setIsLoading(false) }, 500) }, [page, photos, isLoading, hasMore, photosPerPage]) // 滚动检测 useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting && hasMore && !isLoading) { loadMorePhotos() } }, { threshold: 0.1 } ) if (loaderRef.current) { observer.observe(loaderRef.current) } return () => observer.disconnect() }, [hasMore, isLoading, loadMorePhotos]) const handleImageLoad = (photoId: number) => { setLoadedImages((prev) => new Set(prev).add(photoId)) } const getGridClasses = () => { switch (viewMode) { case 'grid': return 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6' case 'masonry': return 'columns-1 md:columns-2 lg:columns-3 xl:columns-4 gap-6 space-y-6' case 'list': return 'space-y-6' default: return 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6' } } const renderPhotoCard = (photo: Photo) => { const isListView = viewMode === 'list' return ( onPhotoClick(photo)} >
{isHydrated && !loadedImages.has(photo.id) && (
)} {photo.title} handleImageLoad(photo.id)} />
{/* Hover overlay */}
点击查看
{/* Card content */}

{photo.title}

{isListView && (

{photo.description}

)}
{photo.tags.slice(0, isListView ? 4 : 2).map((tag) => ( {tag} ))}
{photo.date}
{photo.exif.location}
{photo.exif.camera} • {photo.exif.settings}
) } return ( `translateY(${py}px)`) }} className="space-y-6 touch-pan-x" > {/* 下拉刷新指示器 */} {onRefresh && ( `translateY(${Math.max(0, py - 60)}px)`) }} className="fixed top-16 left-1/2 transform -translate-x-1/2 z-40 bg-white/90 backdrop-blur-sm rounded-full p-3 shadow-lg border md:hidden" > )} {/* 视图模式切换 */}
{onRefresh && ( )}
显示 {displayedPhotos.length} / {photos.length} 张照片
{/* 照片网格 */}
{displayedPhotos.map(renderPhotoCard)}
{/* 加载更多 */} {hasMore && (
{isLoading ? (
正在加载更多...
) : ( )}
)} {/* 无更多照片 */} {!hasMore && displayedPhotos.length > photosPerPage && (
已显示全部 {photos.length} 张照片
)}
) }