125 lines
4.3 KiB
TypeScript
125 lines
4.3 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import { Sparkles } from "lucide-react";
|
||
import Top3Podium from "@/components/ranking/Top3Podium";
|
||
import RankingRow from "@/components/ranking/RankingRow";
|
||
import DebutLineDivider from "@/components/ranking/DebutLineDivider";
|
||
import VoteModal from "@/components/VoteModal";
|
||
import Countdown from "@/components/ui/Countdown";
|
||
import { ARTISTS, getActivityEndTime, sortArtists } from "@/lib/mock-data";
|
||
import type { Artist } from "@/types/artist";
|
||
|
||
export default function RankingPage() {
|
||
const [voteTarget, setVoteTarget] = useState<Artist | null>(null);
|
||
|
||
const sorted = useMemo(() => sortArtists(ARTISTS, "votes"), []);
|
||
const endTime = useMemo(() => getActivityEndTime(), []);
|
||
|
||
const top3 = sorted.slice(0, 3);
|
||
const top4to12 = sorted.slice(3, 12);
|
||
const candidates = sorted.slice(12);
|
||
|
||
// 计算 #13 与第 12 名的差距,用于"救援投票"
|
||
const debutCutoff = sorted[11]?.votes ?? 0;
|
||
|
||
const handleVote = async (a: Artist, count: number) => {
|
||
await new Promise((r) => setTimeout(r, 400));
|
||
console.log(`Vote: ${a.name} × ${count}`);
|
||
setVoteTarget(null);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
|
||
{/* 头部 */}
|
||
<div className="text-center mb-10">
|
||
<p className="font-label text-[10px] sm:text-xs tracking-[0.4em] uppercase text-purple-300 mb-2">
|
||
Live Ranking · 2026
|
||
</p>
|
||
<h1 className="font-logo text-4xl sm:text-5xl tracking-[0.3em] uppercase glow-text-purple mb-3 inline-flex items-baseline">
|
||
Top
|
||
<span className="text-purple-300 mx-2 text-2xl sm:text-3xl">✦</span>
|
||
35
|
||
</h1>
|
||
<p className="text-white/55 text-sm mb-5">35 位候选人 · 实时排名</p>
|
||
<div className="flex justify-center">
|
||
<Countdown endTime={endTime} compact />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Top3 领奖台 */}
|
||
<Top3Podium top3={top3} />
|
||
|
||
{/* Top 4-12 标题 */}
|
||
<div className="flex items-center gap-2 mt-10 mb-4">
|
||
<Sparkles size={14} className="text-purple-300" />
|
||
<h2 className="font-display text-sm tracking-[0.25em] text-white uppercase">
|
||
出道位 · Top 4 ~ 12
|
||
</h2>
|
||
</div>
|
||
|
||
{/* 表头 */}
|
||
<div className="hidden sm:grid grid-cols-[64px_64px_1fr_100px_140px_110px] gap-4 px-3 py-2 text-[10px] tracking-widest uppercase text-white/40 font-label">
|
||
<span className="text-center">排名</span>
|
||
<span>头像</span>
|
||
<span>艺人</span>
|
||
<span className="text-right">票数</span>
|
||
<span className="text-right">距上一名</span>
|
||
<span className="text-center">操作</span>
|
||
</div>
|
||
|
||
{/* Top4-12 行 */}
|
||
<div className="space-y-2">
|
||
{top4to12.map((a, idx) => {
|
||
const prev = idx === 0 ? top3[2] : top4to12[idx - 1];
|
||
const gap = prev ? prev.votes - a.votes : undefined;
|
||
return (
|
||
<RankingRow
|
||
key={a.id}
|
||
artist={a}
|
||
gapAbove={gap}
|
||
onVote={setVoteTarget}
|
||
/>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* 出道线 */}
|
||
<DebutLineDivider />
|
||
|
||
{/* 候补区 */}
|
||
<div className="space-y-2">
|
||
{candidates.map((a, idx) => {
|
||
const prev = idx === 0 ? top4to12[top4to12.length - 1] : candidates[idx - 1];
|
||
const gap = prev ? prev.votes - a.votes : undefined;
|
||
const isRescue = idx === 0; // 第 13 位
|
||
const gapToDebut = isRescue ? debutCutoff - a.votes + 1 : undefined;
|
||
return (
|
||
<RankingRow
|
||
key={a.id}
|
||
artist={a}
|
||
gapAbove={gap}
|
||
gapToDebut={gapToDebut}
|
||
isRescue={isRescue}
|
||
onVote={setVoteTarget}
|
||
/>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* 底部提示 */}
|
||
<p className="text-xs text-white/35 text-center mt-10">
|
||
排名每分钟更新一次 · 投票后立即生效
|
||
</p>
|
||
</div>
|
||
|
||
<VoteModal
|
||
artist={voteTarget}
|
||
onClose={() => setVoteTarget(null)}
|
||
onConfirm={handleVote}
|
||
/>
|
||
</>
|
||
);
|
||
}
|