- nav: center links (首页/排行榜/我的), right-side AuthMenu + RemainingVotesBadge; image logo with responsive sizing - auth: replace /login route with global LoginModal triggered anywhere; "我的" intercepts unauth users with post-login redirect - home: full-screen Hero, redesigned Top12 (12 pill cards, top-3 glow), scroll-snap mandatory between Hero/Top12/candidates - home: candidates section with sticky filter that gains frosted-glass bg when stuck (matches nav) - filter: simplified tags (全部/舞蹈/声乐/rap/全能型); ArtistCard uniform purple vote button - ranking/me: remove Top12Bar; me header stacks 编辑资料/退出登录 vertically - typography: font-logo set to Orbitron; ✦ glyph in CYBER ✦ STAR preserved - layout: max-w-[1500px] unified across pages - docs: add design-spec.md + design-spec.html with full visual spec (lucide SVG, zero emoji policy) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
139 lines
4.2 KiB
TypeScript
139 lines
4.2 KiB
TypeScript
import type { Artist, ArtistTag } from "@/types/artist";
|
||
|
||
const STAGE_NAMES: Array<[string, string, string]> = [
|
||
["艺奈", "AURORA", "破晓极光"],
|
||
["路米", "LUMI", "暖光治愈"],
|
||
["星澪", "NEBULA", "星云吟唱"],
|
||
["凯", "KAI", "海岸少年"],
|
||
["回音", "ECHO", "声波女王"],
|
||
["薇尔", "VEIL", "薄雾低语"],
|
||
["艾莉雅", "ARIA", "咏叹之声"],
|
||
["怜", "REN", "莲华少女"],
|
||
["米拉", "MIRA", "镜面舞者"],
|
||
["诺娃", "NOVA", "超新星"],
|
||
["纪罗", "KIRO", "Rap 制造机"],
|
||
["瑞", "ZUI", "醉月夜"],
|
||
["阳", "SOL", "阳光少年"],
|
||
["凛", "LIN", "学院偶像"],
|
||
["律", "LYRA", "竖琴公主"],
|
||
["昕", "DAWN", "晨曦少女"],
|
||
["天", "SKY", "天空之翼"],
|
||
["语", "ARIE", "诗与远方"],
|
||
["翼", "WING", "飞翔之翼"],
|
||
["铃", "CHIME", "风铃声"],
|
||
["夜", "NYX", "暗夜女神"],
|
||
["晴", "SUNNY", "晴空万里"],
|
||
["月", "LUNA", "月光女神"],
|
||
["岚", "STORM", "暴风之子"],
|
||
["雷", "BOLT", "雷霆速度"],
|
||
["焰", "FLARE", "火焰之心"],
|
||
["雪", "FROST", "霜花少女"],
|
||
["林", "LEAF", "森林精灵"],
|
||
["渊", "ABYSS", "深渊之声"],
|
||
["瑶", "JADE", "翡翠少女"],
|
||
["晨", "AURIA", "金色晨光"],
|
||
["岩", "ROCK", "硬核摇滚"],
|
||
["翔", "SOAR", "翱翔天际"],
|
||
["茉", "MOLLY", "茉莉芬芳"],
|
||
["梓", "AZUR", "蓝调诗人"],
|
||
];
|
||
|
||
// 4 个新主标签均匀分布。每位艺人 1-2 个标签,便于筛选器命中。
|
||
const TAG_POOL: ArtistTag[][] = [
|
||
["vocal"],
|
||
["dance"],
|
||
["all-rounder"],
|
||
["rap"],
|
||
["vocal", "all-rounder"],
|
||
["dance", "all-rounder"],
|
||
["rap", "all-rounder"],
|
||
["vocal"],
|
||
["dance"],
|
||
["all-rounder"],
|
||
["rap"],
|
||
["vocal", "dance"],
|
||
];
|
||
|
||
const THEME_COLORS = [
|
||
"#8b5cf6",
|
||
"#ec4899",
|
||
"#06b6d4",
|
||
"#f59e0b",
|
||
"#10b981",
|
||
"#ef4444",
|
||
"#a78bfa",
|
||
"#f472b6",
|
||
"#38bdf8",
|
||
"#fbbf24",
|
||
"#34d399",
|
||
"#fb7185",
|
||
];
|
||
|
||
/** 生成确定性 35 位艺人 mock 数据 */
|
||
function buildArtists(): Artist[] {
|
||
return STAGE_NAMES.map(([name, enName, slogan], idx) => {
|
||
const no = String(idx + 1).padStart(3, "0");
|
||
const rank = idx + 1;
|
||
// 票数采用反比例衰减(确定性 · 避免 SSR/CSR hydration 不一致)
|
||
// 主曲线:125000/√rank · 用 idx 推导抖动量保持稳定分布
|
||
const jitter = ((idx * 1103) % 2999) - 1499;
|
||
const votes = Math.round(125000 / Math.sqrt(rank) + jitter);
|
||
|
||
return {
|
||
id: no,
|
||
no,
|
||
name,
|
||
enName,
|
||
slogan,
|
||
bio: `来自虚拟星域的偶像候选人 ${enName},从小热爱音乐与舞蹈。性格${
|
||
rank % 2 === 0 ? "温柔" : "活泼"
|
||
},擅长${
|
||
idx % 3 === 0 ? "抒情曲" : idx % 3 === 1 ? "舞台表演" : "Rap 创作"
|
||
}。曾获得多项虚拟偶像新人奖项,代表作品深受粉丝喜爱。立志成为 Top12 出道阵容的一员,用音乐传递梦想与力量。`,
|
||
portrait: "",
|
||
avatar: "",
|
||
gallery: ["", "", "", "", ""],
|
||
videoUrl: undefined,
|
||
videoPoster: "",
|
||
tags: TAG_POOL[idx % TAG_POOL.length]!,
|
||
birthday: `${String(((idx * 7) % 12) + 1).padStart(2, "0")}-${String(
|
||
((idx * 13) % 28) + 1
|
||
).padStart(2, "0")}`,
|
||
height: 158 + (idx % 12),
|
||
cv: idx < 12 ? `CV 配音 #${idx + 1}` : undefined,
|
||
themeColor: THEME_COLORS[idx % THEME_COLORS.length]!,
|
||
votes,
|
||
rank,
|
||
};
|
||
});
|
||
}
|
||
|
||
export const ARTISTS: Artist[] = buildArtists();
|
||
|
||
export const TOP_12 = ARTISTS.slice(0, 12);
|
||
export const CANDIDATES = ARTISTS.slice(12);
|
||
|
||
/** 按当前排序方式获取艺人列表 */
|
||
export type SortKey = "votes" | "no" | "recent";
|
||
export function sortArtists(list: Artist[], key: SortKey = "votes"): Artist[] {
|
||
const sorted = [...list];
|
||
if (key === "votes") sorted.sort((a, b) => b.votes - a.votes);
|
||
else if (key === "no") sorted.sort((a, b) => a.no.localeCompare(b.no));
|
||
return sorted;
|
||
}
|
||
|
||
/** 按 ID 获取艺人 */
|
||
export function getArtist(id: string): Artist | undefined {
|
||
return ARTISTS.find((a) => a.id === id);
|
||
}
|
||
|
||
/** 活动结束时间(mock:当前日期 + 12 天) */
|
||
export function getActivityEndTime(): Date {
|
||
const end = new Date();
|
||
end.setDate(end.getDate() + 12);
|
||
end.setHours(end.getHours() + 3);
|
||
end.setMinutes(end.getMinutes() + 24);
|
||
end.setSeconds(end.getSeconds() + 18);
|
||
return end;
|
||
}
|