import React, { ReactNode } from 'react'; import { useInfiniteScroll } from '@/hooks/useInfiniteScroll'; import { cn } from '@/lib/utils'; interface InfiniteScrollListProps { /** * 数据项数组 */ items: T[]; /** * 渲染每个数据项的函数 */ renderItem: (item: T, index: number) => ReactNode; /** * 获取每个数据项的唯一key */ getItemKey: (item: T, index: number) => string | number; /** * 是否有更多数据可以加载 */ hasNextPage: boolean; /** * 是否正在加载 */ isLoading: boolean; /** * 加载下一页的函数 */ fetchNextPage: () => void; /** * 列表容器的className */ className?: string; /** * 网格列数(支持响应式) */ columns?: { default: number; sm?: number; md?: number; lg?: number; xl?: number; } | number; /** * 网格间距 */ gap?: number; /** * 加载状态的骨架屏组件 */ LoadingSkeleton?: React.ComponentType; /** * 加载更多时显示的组件 */ LoadingMore?: React.ComponentType; /** * 空状态组件 */ EmptyComponent?: React.ComponentType; /** * 错误状态组件 */ ErrorComponent?: React.ComponentType<{ onRetry: () => void }>; /** * 是否有错误 */ hasError?: boolean; /** * 重试函数 */ onRetry?: () => void; /** * 触发加载的阈值(px) */ threshold?: number; /** * 是否启用无限滚动 */ enabled?: boolean; } /** * 通用的无限滚动列表组件 * 支持网格布局、骨架屏、错误状态等功能 */ export function InfiniteScrollList({ items, renderItem, getItemKey, hasNextPage, isLoading, fetchNextPage, className, columns = 4, gap = 4, LoadingSkeleton, LoadingMore, EmptyComponent, ErrorComponent, hasError = false, onRetry, threshold = 200, enabled = true, }: InfiniteScrollListProps) { const { loadMoreRef, isFetching } = useInfiniteScroll({ hasNextPage, isLoading, fetchNextPage, threshold, enabled, }); // 生成网格列数的CSS类名映射 const getGridColsClass = () => { if (typeof columns === 'number') { const gridClassMap: Record = { 1: 'grid-cols-1', 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4', 5: 'grid-cols-5', 6: 'grid-cols-6', }; return gridClassMap[columns] || 'grid-cols-4'; } const classes = []; const colsClassMap: Record = { 1: 'grid-cols-1', 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4', 5: 'grid-cols-5', 6: 'grid-cols-6', }; classes.push(colsClassMap[columns.default] || 'grid-cols-4'); if (columns.sm) classes.push(`sm:${colsClassMap[columns.sm]}`); if (columns.md) classes.push(`md:${colsClassMap[columns.md]}`); if (columns.lg) classes.push(`lg:${colsClassMap[columns.lg]}`); if (columns.xl) classes.push(`xl:${colsClassMap[columns.xl]}`); return classes.join(' '); }; // 生成间距类名 const getGapClass = () => { const gapClassMap: Record = { 1: 'gap-1', 2: 'gap-2', 3: 'gap-3', 4: 'gap-4', 5: 'gap-5', 6: 'gap-6', 8: 'gap-8', }; return gapClassMap[gap] || 'gap-4'; }; // 错误状态 if (hasError && ErrorComponent && onRetry) { return ; } // 首次加载状态 if (isLoading && items.length === 0) { if (LoadingSkeleton) { return (
{Array.from({ length: 8 }).map((_, index) => ( ))}
); } return (
{Array.from({ length: 8 }).map((_, index) => (
))}
); } // 空状态 if (items.length === 0 && EmptyComponent) { return ; } return (
{/* 主要内容 */}
{items.map((item, index) => ( {renderItem(item, index)} ))}
{/* 加载更多触发器 */} {hasNextPage && (
{LoadingMore ? ( ) : (
{(isFetching || isLoading) && (
)} {isFetching || isLoading ? 'Loading...' : ''}
)}
)}
); } // 默认的加载骨架屏组件 export const DefaultAlbumSkeleton = () => (
{/* 模拟标签 */}
{/* 模拟点赞按钮 */}
); // 默认的加载更多组件 export const DefaultLoadingMore = () => (
Loading...
);