"use client" import * as React from "react" import { useVirtualizer } from "@tanstack/react-virtual" import { Skeleton } from "@/components/ui/skeleton" import { ScrollArea } from "@/components/ui/scroll-area" import { RankRowItem } from "./rank-row-item" import type { LeaderboardEntry } from "@/lib/services/leaderboard" const leaderboardTableColumns = "grid-cols-[72px_minmax(0,1fr)_132px] sm:grid-cols-[88px_minmax(0,1fr)_152px]" const leaderboardRowHeight = 60 interface LeaderboardTableProps { items: LeaderboardEntry[] loading?: boolean currentUserId?: number currentUserEntry?: LeaderboardEntry currentUserRank?: number onLoadMore?: () => void hasMore?: boolean startRank?: number } export const LeaderboardTable = React.memo(function LeaderboardTable({ items, loading, currentUserId, currentUserEntry, currentUserRank, onLoadMore, hasMore, startRank = 1, }: LeaderboardTableProps) { const parentRef = React.useRef(null) const maxBalance = React.useMemo(() => { if (items.length === 0) return 1 return Math.max(...items.map(item => parseFloat(item.available_balance))) || 1 }, [items]) const computePercentage = React.useCallback( (balance: number) => { const denominator = Math.log(maxBalance + 1) if (denominator <= 0) return 0 const safeBalance = Math.max(balance, 0) return (Math.log(safeBalance + 1) / denominator) * 100 }, [maxBalance] ) const rankedItems = React.useMemo( () => items .map((entry, index) => ({ entry, rank: startRank + index, percentage: computePercentage(parseFloat(entry.available_balance)), })) .filter(({ entry }) => entry.user_id !== currentUserEntry?.user_id), [items, startRank, currentUserEntry?.user_id, computePercentage] ) const virtualizer = useVirtualizer({ count: rankedItems.length, getScrollElement: () => parentRef.current, estimateSize: () => leaderboardRowHeight, overscan: 6, useFlushSync: false, }) const virtualItems = virtualizer.getVirtualItems() React.useEffect(() => { const lastItem = virtualItems[virtualItems.length - 1] if (!lastItem) return if (lastItem.index >= rankedItems.length - 1 && hasMore && !loading && onLoadMore) { onLoadMore() } }, [virtualItems, rankedItems.length, hasMore, loading, onLoadMore]) const currentUserPercentage = currentUserEntry ? computePercentage(parseFloat(currentUserEntry.available_balance)) : 0 if (loading && items.length === 0) { return (
排名
用户
可用积分
{Array.from({ length: 8 }).map((_, i) => (
))}
) } if (items.length === 0) { return
暂无排行数据
} return (
{/* 列头 */}
排名
用户
可用积分
{/* 当前用户置顶行 */} {currentUserEntry && currentUserRank && (
)} {/* 榜单主体 — 从 muted 过渡到页面背景色 */}
{virtualItems.map((virtualRow) => { const { entry, rank, percentage } = rankedItems[virtualRow.index] return (
) })}
{loading && items.length > 0 && (
正在加载更多...
)}
) })