import React, { useRef, useEffect, useState, useCallback } from 'react'; import { motion } from 'framer-motion'; interface VirtualListProps { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; itemHeight: number; containerHeight: number; overscan?: number; className?: string; onEndReached?: () => void; endReachedThreshold?: number; } export function VirtualList({ items, renderItem, itemHeight, containerHeight, overscan = 5, className = '', onEndReached, endReachedThreshold = 200, }: VirtualListProps) { const containerRef = useRef(null); const [scrollTop, setScrollTop] = useState(0); const [isScrolling, setIsScrolling] = useState(false); const scrollTimeoutRef = useRef | null>(null); const totalHeight = items.length * itemHeight; const visibleCount = Math.ceil(containerHeight / itemHeight); const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan); const endIndex = Math.min( items.length, Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan ); const visibleItems = items.slice(startIndex, endIndex); const offsetY = startIndex * itemHeight; const handleScroll = useCallback( (e: React.UIEvent) => { const newScrollTop = e.currentTarget.scrollTop; setScrollTop(newScrollTop); setIsScrolling(true); // Clear previous timeout if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } // Set new timeout to detect scroll end scrollTimeoutRef.current = setTimeout(() => { setIsScrolling(false); }, 150); // Check if end is reached if (onEndReached) { const scrollHeight = e.currentTarget.scrollHeight; const clientHeight = e.currentTarget.clientHeight; const scrollBottom = scrollHeight - newScrollTop - clientHeight; if (scrollBottom < endReachedThreshold) { onEndReached(); } } }, [onEndReached, endReachedThreshold] ); useEffect(() => { return () => { if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } }; }, []); return (
{visibleItems.map((item, index) => { const actualIndex = startIndex + index; return ( {renderItem(item, actualIndex)} ); })}
); } // 简化版虚拟列表(用于卡片布局) interface VirtualGridProps { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; itemHeight: number; containerHeight: number; columns: number; gap?: number; className?: string; } export function VirtualGrid({ items, renderItem, itemHeight, containerHeight, columns, gap = 16, className = '', }: VirtualGridProps) { const containerRef = useRef(null); const [scrollTop, setScrollTop] = useState(0); const rowHeight = itemHeight + gap; const totalRows = Math.ceil(items.length / columns); const totalHeight = totalRows * rowHeight; const visibleCount = Math.ceil(containerHeight / rowHeight); const startRow = Math.max(0, Math.floor(scrollTop / rowHeight) - 2); const endRow = Math.min( totalRows, Math.ceil((scrollTop + containerHeight) / rowHeight) + 2 ); const startIndex = startRow * columns; const endIndex = Math.min(items.length, endRow * columns); const visibleItems = items.slice(startIndex, endIndex); const offsetY = startRow * rowHeight; const handleScroll = useCallback((e: React.UIEvent) => { setScrollTop(e.currentTarget.scrollTop); }, []); return (
{visibleItems.map((item, index) => { const actualIndex = startIndex + index; return (
{renderItem(item, actualIndex)}
); })}
); } // 懒加载图片组件 interface LazyImageProps { src: string; alt: string; className?: string; placeholder?: React.ReactNode; onLoad?: () => void; onError?: () => void; } export function LazyImage({ src, alt, className = '', placeholder, onLoad, onError, }: LazyImageProps) { const [isLoaded, setIsLoaded] = useState(false); const [isInView, setIsInView] = useState(false); const imgRef = useRef(null); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsInView(true); observer.disconnect(); } }, { rootMargin: '50px' } ); if (imgRef.current) { observer.observe(imgRef.current); } return () => observer.disconnect(); }, []); const handleLoad = () => { setIsLoaded(true); onLoad?.(); }; const handleError = () => { onError?.(); }; return (
{!isLoaded && placeholder} {isInView && ( {alt} )}
); } // 懒加载组件包装器 interface LazyComponentProps { children: React.ReactNode; fallback?: React.ReactNode; threshold?: number; } export function LazyComponent({ children, fallback = null, threshold = 0, }: LazyComponentProps) { const [isVisible, setIsVisible] = useState(false); const ref = useRef(null); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } }, { threshold } ); if (ref.current) { observer.observe(ref.current); } return () => observer.disconnect(); }, [threshold]); return (
{isVisible ? children : fallback}
); }