UI-UX/src/components/cards/ArtistPortrait.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

67 lines
1.8 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.

import Image from "next/image";
import type { Artist } from "@/types/artist";
import { cn } from "@/lib/cn";
interface ArtistPortraitProps {
artist: Artist;
className?: string;
/** 圆角覆盖 */
rounded?: string;
}
/**
* 艺人立绘容器。
* 有真实图时显示 Image否则渲染统一品牌紫渐变占位带英文首字母
*/
export default function ArtistPortrait({
artist,
className,
rounded = "rounded-lg",
}: ArtistPortraitProps) {
const initial =
artist.enName?.charAt(0).toUpperCase() ||
artist.name?.charAt(0) ||
"?";
if (artist.portrait) {
return (
<div
className={cn("relative overflow-hidden bg-deep", rounded, className)}
>
<Image
src={artist.portrait}
alt={`${artist.name} · ${artist.enName}`}
fill
sizes="(max-width: 768px) 50vw, 360px"
// 氛围图大多为全身竖向构图,用 object-cover + object-top
// 让头部 + 上半身保留在画面里,下半身(腿/脚)自然超出底边被剪裁
className="object-cover object-top"
/>
</div>
);
}
// 无立绘 fallback品牌紫渐变 + 字母占位
return (
<div
className={cn(
"relative overflow-hidden flex items-center justify-center",
"bg-[linear-gradient(155deg,rgba(139,92,246,0.20)_0%,#1a1638_60%,#0d0a24_100%)]",
rounded,
className,
)}
>
<div
aria-hidden
className="absolute inset-0 bg-[radial-gradient(circle_at_50%_30%,rgba(139,92,246,0.35)_0%,transparent_55%)]"
/>
<span
className="font-logo text-5xl text-white/85 glow-text-purple tracking-wider relative z-10"
aria-hidden
>
{initial}
</span>
</div>
);
}