UI-UX/src/components/Top12Bar.tsx
iye 71a2672ff6 fix(data,ranking,ui): real dynamic ranking + data sync hardening
数据准确性
- 票数初始为 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>
2026-05-13 13:56:42 +08:00

125 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}