数据准确性 - 票数初始为 0,不再用 Math.sqrt 公式造假票 - 排序 tiebreaker 统一为 votes desc + no asc,确保稳定 - store.rank() 与 sortArtists() 行为对齐 Top12 / 出道位 - Top12Bar 仅收纳真正有票的人(votes > 0),0 票时显示"出道位尚未产生"空态 - ArtistCard / RankingRow / SearchModal / MyFanSupport / RankCard 的 inTop12 高亮全部加 votes > 0 守卫 - ArtistFilters 新增"实时排名 / 编号顺序"分段切换 + 首页 sortKey 状态 领奖台 (Top3Podium) - 1 人有票即可显示领奖台(此前要求 >= 3 人才显示) - 缺位的 #2/#3 用"虚位以待"占位卡片填充,与正式卡片同 3:4 比例对齐 - 全员 0 票时三张全部显示虚位以待 - 卡片间距拉大到 gap-8 sm:gap-12 排行榜页 (/ranking) - API 票数与本地乐观投票取 max() 合并,避免 API 落后覆盖本地新票 - 合并后重新排序与赋 rank - 仅 >= 12 人有票才显示出道线分隔与复活位标记 - 复活位 gapToDebut 计算修正 跨日额度 - selectRemaining 按 quotaDate 判断是否跨日,跨日自动回满额(此前会卡在昨日剩余值) 搜索 (SearchModal) - 改为订阅 store 拿活的票数,投票后立即反映 - 默认"热门 Top12"只在真正有票时显示,否则降级为"推荐艺人 · 编号顺序" - 票数显示统一走 formatVotes(0 票不再显示 0.0w) 人物详情 - 36 人真实数据接入,移除全部静态数据(slogan/birthday/cv/themeColor) - 接入人物小传 docx 数据(年龄/身高/性格/口头禅/技能/赛道/座右铭/长简介) - 视频区与版心同宽 + 首帧自动作为封面 + 整区点击播放/暂停 + 可拖拽进度条 - 表演图片改为三张氛围图竖向 3:4,左对齐 - 移除分享按钮,投票按钮全宽 个人页 (/me) - 移除等级/邀请好友/签到/编辑资料等静态数据 - 退出登录按钮在移动端 icon-only 显示(此前 sm:hidden 导致移动端无法登出) - 我的应援 list 基于真实 myVotesByArtist 派生,凯之类的投票真正同步过去 导航 - 余票徽章未登录态显示 0/0,已登录显示 N/10 - 登录/注册按钮样式与登录后头像胶囊保持一致(紫色实心) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { ChevronRight, Trophy } from "lucide-react";
|
||
import type { Artist } from "@/types/artist";
|
||
import { cn } from "@/lib/cn";
|
||
import ArtistPortrait from "./cards/ArtistPortrait";
|
||
|
||
interface Top12BarProps {
|
||
artists: Artist[];
|
||
/** 是否显示头部标题 */
|
||
showHeader?: boolean;
|
||
}
|
||
|
||
function formatVotes(v: number): string {
|
||
if (v >= 10_000) return `${(v / 10_000).toFixed(1)}W 票`;
|
||
return `${v.toLocaleString()} 票`;
|
||
}
|
||
|
||
export default function Top12Bar({ artists, showHeader = true }: Top12BarProps) {
|
||
// Top12 出道位 只看「真正有票」的人 —— 0 票时不靠编号兜底占位
|
||
const top12 = artists.filter((a) => a.votes > 0).slice(0, 12);
|
||
return (
|
||
<div className="w-full">
|
||
{showHeader && (
|
||
<div className="flex items-end justify-between mb-3 px-1">
|
||
<h2 className="font-display text-base sm:text-lg tracking-[0.2em] text-white inline-flex items-center gap-2">
|
||
<Trophy size={16} className="text-purple-300" />
|
||
实时 Top12 出道位
|
||
</h2>
|
||
<Link
|
||
href="/ranking"
|
||
className="font-label text-[11px] tracking-widest text-purple-300 hover:text-purple-200 uppercase inline-flex items-center gap-0.5"
|
||
>
|
||
查看完整榜单
|
||
<ChevronRight size={12} />
|
||
</Link>
|
||
</div>
|
||
)}
|
||
|
||
{top12.length === 0 ? (
|
||
<Top12Empty />
|
||
) : (
|
||
// 12 张胶囊卡片 · grid 等分铺满,无滚动 · 无外边框无背景
|
||
<div
|
||
className={cn(
|
||
"grid grid-cols-6 sm:grid-cols-12 gap-2 sm:gap-3 py-2",
|
||
)}
|
||
>
|
||
{top12.map((artist) => (
|
||
<Top12Card key={artist.id} artist={artist} />
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Top12Empty() {
|
||
return (
|
||
<div className="py-10 sm:py-12 px-6 text-center border border-dashed border-white/10 rounded-2xl bg-white/[0.02]">
|
||
<p className="font-label text-[11px] tracking-widest uppercase text-purple-300 mb-2">
|
||
Awaiting Votes
|
||
</p>
|
||
<p className="text-sm text-white/70">出道位尚未产生 · 等你为 ta 投下第一票</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Top12Card({ artist }: { artist: Artist }) {
|
||
const inTop3 = artist.rank <= 3;
|
||
|
||
return (
|
||
<Link
|
||
href={`/artist/${artist.id}`}
|
||
className="group block text-center"
|
||
aria-label={`${artist.name} · 当前第 ${artist.rank} 名`}
|
||
>
|
||
{/* 外层 relative,徽章可以完整显示在外面,不会被卡片圆角裁切 */}
|
||
<div className="relative">
|
||
{/* 卡片本体:2:3 矩形,上下两端 = 半圆(rounded-full 在 2:3 矩形上 = 胶囊形) */}
|
||
<div
|
||
className={cn(
|
||
"relative w-full aspect-[2/3] rounded-full overflow-hidden border",
|
||
"border-white/15",
|
||
"transition-transform group-hover:-translate-y-1",
|
||
// 仅前 3 名保留紫色辉光
|
||
inTop3 && "border-purple-500 shadow-[0_0_16px_rgba(139,92,246,0.55)]",
|
||
)}
|
||
>
|
||
<ArtistPortrait
|
||
artist={artist}
|
||
rounded="rounded-none"
|
||
className="absolute inset-0 w-full h-full"
|
||
/>
|
||
</div>
|
||
|
||
{/* 排名徽章 · 完整悬浮在卡片左上角(外层定位,不被 overflow 裁切) */}
|
||
<span
|
||
className={cn(
|
||
"absolute -top-1 -left-1 z-10",
|
||
"w-6 h-6 sm:w-7 sm:h-7 rounded-full",
|
||
"bg-purple-500 text-white font-display text-[11px] sm:text-xs",
|
||
"flex items-center justify-center tabular-nums",
|
||
"border-2 border-deepest",
|
||
inTop3 && "shadow-[0_0_10px_rgba(139,92,246,0.8)]",
|
||
)}
|
||
>
|
||
{artist.rank}
|
||
</span>
|
||
</div>
|
||
|
||
{/* 卡片下方 · 名字 + 票数 */}
|
||
<div className="mt-2 px-0.5">
|
||
<div className="text-[11px] sm:text-xs text-white truncate font-medium">
|
||
{artist.name}
|
||
</div>
|
||
<div className="text-[10px] sm:text-[11px] text-purple-300 tabular-nums mt-0.5">
|
||
{formatVotes(artist.votes)}
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
);
|
||
}
|