import type { Metadata } from "next"; import Link from "next/link"; import { cookies } from "next/headers"; import { Trophy, Crown, Medal, FileText, MessageSquare, ThumbsUp, BadgeCheck, CalendarCheck, PenSquare, TrendingUp, TrendingDown, } from "lucide-react"; import { fetchLeaderboard, fetchMyRank, type LeaderboardEntry, type LeaderboardPeriod, type LeaderboardMetric, type LeaderboardMe, type PublicSettings, } from "@/lib/api"; import { getMeCached, getPublicSettingsCached } from "@/lib/serverData"; import { authCookieHeader } from "@/lib/cookies"; import { requestCanonical } from "@/lib/canonicalMeta"; import Avatar from "@/components/Avatar"; import LevelBadge from "@/components/LevelBadge"; interface PageProps { searchParams: Promise<{ period?: string; metric?: string }>; } const PERIODS: { key: LeaderboardPeriod; label: string }[] = [ { key: "week", label: "本周" }, { key: "month", label: "本月" }, { key: "all", label: "总榜" }, ]; const METRICS: { key: LeaderboardMetric; label: string }[] = [ { key: "all", label: "全部" }, { key: "post", label: "发帖最多" }, { key: "comment", label: "回复最多" }, { key: "like", label: "获赞最多" }, { key: "answer", label: "最佳答案" }, ]; function normPeriod(v?: string): LeaderboardPeriod { return v === "week" || v === "month" || v === "all" ? v : "all"; } function normMetric(v?: string): LeaderboardMetric { return v === "post" || v === "comment" || v === "like" || v === "answer" || v === "all" ? v : "all"; } /** 榜单链接:默认参数省略,保持 URL 干净 */ function lbHref(period: LeaderboardPeriod, metric: LeaderboardMetric): string { const q = new URLSearchParams(); if (period !== "all") q.set("period", period); if (metric !== "all") q.set("metric", metric); const s = q.toString(); return s ? `/leaderboard?${s}` : "/leaderboard"; } /** 右侧分数标签:总榜-全部=累计积分,周/月-全部=贡献值,细分=对应指标名 */ function scoreLabel(metric: LeaderboardMetric, period: LeaderboardPeriod): string { if (metric === "all") return period === "all" ? "累计积分" : "贡献值"; return METRICS.find((m) => m.key === metric)?.label.replace("最多", "") ?? "贡献值"; } export async function generateMetadata({ searchParams }: PageProps): Promise { await searchParams; const settings = await getPublicSettingsCached(); return { title: "排行榜", description: settings.site_description, ...(await requestCanonical("/leaderboard")), }; } /** 金银铜固定金属色(装饰用,不随明暗主题翻转) */ const METAL: Record<1 | 2 | 3, { ring: string; fg: string; soft: string }> = { 1: { ring: "linear-gradient(135deg, #ffe9a8, #f0c964 45%, #c9973f)", fg: "#a97f1e", soft: "rgba(212, 160, 23, 0.14)", }, 2: { ring: "linear-gradient(135deg, #f1f4f8, #c3cbd6 45%, #96a0ad)", fg: "#6b7686", soft: "rgba(125, 135, 148, 0.16)", }, 3: { ring: "linear-gradient(135deg, #f3cdae, #d69a68 45%, #b0763f)", fg: "#8f5f2e", soft: "rgba(176, 118, 63, 0.15)", }, }; function RankMedal({ place }: { place: 1 | 2 | 3 }) { const m = METAL[place]; return ( {place === 1 ? : } ); } /** 领奖台卡片:渐变金属环头像 + 名字/等级 + 分数大数字 */ function PodiumCard({ e, place, period, metric }: { e: LeaderboardEntry; place: 1 | 2 | 3; period: LeaderboardPeriod; metric: LeaderboardMetric; }) { const m = METAL[place]; const first = place === 1; return ( {/* 两层均用 flex 包裹(inline 行盒 leading 会把环撑成竖椭圆):金属环 + 白色内垫 + 头像贴合 */}

{e.nickname || e.username}

@{e.username}

{e.score.toLocaleString()}

{scoreLabel(metric, period)}

{e.post_count} · {e.comment_count}

); } /** 4 名起的列表行 */ function RankRow({ e, period, metric }: { e: LeaderboardEntry; period: LeaderboardPeriod; metric: LeaderboardMetric; }) { return ( {e.rank} {e.nickname || e.username} @{e.username} {e.post_count} {e.comment_count} {e.like_count} {e.answer_count > 0 && ( {e.answer_count} )} {e.score.toLocaleString()} {scoreLabel(metric, period)} ); } /** 右栏:我的排名(登录用户带本周/本月名次变化) */ function MyRankPanel({ me, authed, period, metric }: { me: LeaderboardMe | null; authed: boolean; period: LeaderboardPeriod; metric: LeaderboardMetric; }) { const periodLabel = period === "month" ? "本月" : "本周"; const label = scoreLabel(metric, period); return (

我的排名

{me && me.rank > 0 && period !== "all" && ( me.has_change ? ( me.change > 0 ? ( {periodLabel}上升 {me.change} 名 ) : me.change < 0 ? ( {periodLabel}下降 {-me.change} 名 ) : ( {periodLabel}持平 ) ) : ( {periodLabel}新上榜 ) )}
{!authed ? (

登录后查看我的排名

参与发帖、回帖与签到即可上榜

立即登录
) : me && me.rank > 0 ? (

# {me.rank}

{label} {me.score.toLocaleString()}

{period === "all" ? "总榜" : periodLabel} · 共 {me.total} 人上榜

) : (

暂未上榜

{period === "all" ? "签到或发帖积累积分即可上榜" : `${periodLabel}内参与互动即可上榜`}

写新帖
)}
); } /** 右栏:贡献值规则(数据来自公开设置的积分规则 8 键) */ function RulesPanel({ s }: { s: PublicSettings }) { const rows: { icon: React.ReactNode; label: string; value: string; note?: string }[] = [ { icon: , label: "每日签到", value: `+${s.points_checkin_base}`, note: `连续每 ${s.points_streak_every_days} 天额外 +${s.points_streak_bonus}`, }, { icon: , label: "发表主题", value: `+${s.points_post_reward}`, note: `每日上限 ${s.points_post_daily_cap} 篇`, }, { icon: , label: "发表回复", value: `+${s.points_reply_reward}`, note: `每日上限 ${s.points_reply_daily_cap} 条`, }, { icon: , label: "帖子被推荐", value: `+${s.points_recommend_reward}`, note: "每帖仅计一次", }, { icon: , label: "回答被采纳", value: "悬赏另计", note: "悬赏积分由提问者设置", }, ]; return (

贡献值规则

    {rows.map((r) => (
  • {r.icon} {r.label} {r.note && {r.note}} {r.value}
  • ))}

累计积分即贡献值,决定等级与总榜排名;消费仅扣余额,不减少累计值。

); } export default async function LeaderboardPage({ searchParams }: PageProps) { const sp = await searchParams; const period = normPeriod(sp.period); const metric = normMetric(sp.metric); const cookie = authCookieHeader(await cookies()); const settings = await getPublicSettingsCached(); const [meUser] = await Promise.all([getMeCached(cookie || undefined)]); const authed = !!meUser; // 接口失败降级为空榜,不让页面 500 let entries: LeaderboardEntry[] = []; try { const res = await fetchLeaderboard(period, metric, cookie || undefined); entries = res.entries ?? []; } catch { /* 降级空榜 */ } const me = authed ? await fetchMyRank(period, metric, cookie || undefined) : null; const podium = entries.slice(0, 3); const rest = entries.slice(3); const totalScore = entries.reduce((acc, e) => acc + e.score, 0); return (
{/* 左栏:贡献排行榜 */}
{/* Hero:渐变洗底 + 光斑装饰 */}

贡献排行榜

每一分积累,都是社区前行的足迹

{entries.length > 0 && (

{entries.length}

上榜用户

{totalScore.toLocaleString()}

{period === "all" && metric === "all" ? "累计贡献积分" : "贡献合计"}

{podium[0]?.nickname || podium[0]?.username || "—"}

当前榜首

)}
{/* 周期 Tab(本周/本月/总榜)+ 维度细分(全部/发帖/回复/获赞/最佳答案) */}
{PERIODS.map((p) => ( {p.label} ))}
{METRICS.map((m) => { const active = metric === m.key; return ( {m.label} ); })}
{entries.length === 0 ? (

榜单虚位以待

{period === "all" ? "签到、发帖、回帖都能积累积分上榜" : "这个周期还没有动态,快来抢占先机"}

写新帖
) : ( <> {/* 领奖台:DOM 顺序 2-1-3,冠军居中放大 */}
{podium[1] && } {podium[0] && } {podium[2] && }
{rest.length > 0 && (
{rest.map((e) => ( ))}
)} )}
{/* 右栏:我的排名 + 贡献值规则 */}
); }