Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a122ffa27 | |||
| 85cf284848 | |||
| 9772ba88ae |
BIN
public/logo-v4.png
Normal file
BIN
public/logo-v4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@ -14,7 +14,7 @@ const Body = z.object({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/auth/send-otp
|
* POST /api/auth/send-otp
|
||||||
* 发送短信验证码 · 单手机号 60s 限频 / 单 IP 5 分钟 5 次
|
* 发送短信验证码 · 单手机号 60s 限频 / 单 IP 5 分钟 100 次
|
||||||
*/
|
*/
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@ -31,7 +31,7 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
const ip = await getClientIp();
|
const ip = await getClientIp();
|
||||||
if (ip) {
|
if (ip) {
|
||||||
const ipRl = await rateLimit(`otp:ip:${ip}`, 300, 5);
|
const ipRl = await rateLimit(`otp:ip:${ip}`, 300, 100);
|
||||||
if (!ipRl.allowed) return ERR.RATE_LIMITED();
|
if (!ipRl.allowed) return ERR.RATE_LIMITED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,10 +16,10 @@ export async function generateMetadata({
|
|||||||
}: ArtistPageProps): Promise<Metadata> {
|
}: ArtistPageProps): Promise<Metadata> {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const artist = getArtist(id);
|
const artist = getArtist(id);
|
||||||
if (!artist) return { title: "艺人不存在 · CYBER STAR" };
|
if (!artist) return { title: "艺人不存在 · 银河初星计划 C . S . G" };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${artist.name} · ${artist.enName} · CYBER STAR`,
|
title: `${artist.name} · ${artist.enName} · 银河初星计划 C . S . G`,
|
||||||
description: artist.bio.slice(0, 120),
|
description: artist.bio.slice(0, 120),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 81 KiB |
@ -33,15 +33,27 @@ const inter = Inter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "CYBER ✦ STAR · 虚拟偶像 Top12 出道企划",
|
title: "银河初星计划 C . S . G",
|
||||||
description:
|
description:
|
||||||
"36 位虚拟偶像候选人,由你投票决出最终出道 Top12。Cyber Star · Virtual Idol Debut Project.",
|
"36 位虚拟偶像候选人,由你投票决出最终出道 Top12。银河初星计划 C . S . G。",
|
||||||
keywords: ["虚拟偶像", "出道", "投票", "Top12", "Cyber Star", "Virtual Idol"],
|
keywords: [
|
||||||
|
"银河初星计划",
|
||||||
|
"C . S . G",
|
||||||
|
"虚拟偶像",
|
||||||
|
"出道",
|
||||||
|
"投票",
|
||||||
|
"Top12",
|
||||||
|
"Virtual Idol",
|
||||||
|
],
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "CYBER ✦ STAR",
|
title: "银河初星计划 C . S . G",
|
||||||
description: "虚拟偶像 Top12 出道企划",
|
description: "虚拟偶像 Top12 出道企划",
|
||||||
type: "website",
|
type: "website",
|
||||||
},
|
},
|
||||||
|
icons: {
|
||||||
|
icon: "/favicon.ico?v=4",
|
||||||
|
shortcut: "/favicon.ico?v=4",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
@ -93,8 +93,8 @@ export default function LoginForm() {
|
|||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex flex-col items-center mb-8">
|
<div className="flex flex-col items-center mb-8">
|
||||||
<img
|
<img
|
||||||
src="/logo-v3.png"
|
src="/logo-v4.png?v=4"
|
||||||
alt="CYBER STAR"
|
alt="银河初星计划 C . S . G"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
className="block select-none h-20 sm:h-24 w-auto"
|
className="block select-none h-20 sm:h-24 w-auto"
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Suspense } from "react";
|
|||||||
import LoginForm from "./LoginForm";
|
import LoginForm from "./LoginForm";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "登录 · CYBER STAR",
|
title: "登录 · 银河初星计划 C . S . G",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { auth } from "@/lib/auth";
|
|||||||
import MeContent from "./MeContent";
|
import MeContent from "./MeContent";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "个人中心 · CYBER STAR",
|
title: "个人中心 · 银河初星计划 C . S . G",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function MePage() {
|
export default async function MePage() {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export default function Footer() {
|
|||||||
<footer className="border-t border-white/[0.06] bg-deep mt-16">
|
<footer className="border-t border-white/[0.06] bg-deep mt-16">
|
||||||
<div className="max-w-[1500px] mx-auto px-6 sm:px-8 h-16 flex items-center justify-center text-center">
|
<div className="max-w-[1500px] mx-auto px-6 sm:px-8 h-16 flex items-center justify-center text-center">
|
||||||
<p className="text-[11px] text-white/35 tracking-[0.05em]">
|
<p className="text-[11px] text-white/35 tracking-[0.05em]">
|
||||||
© {year} CYBER STAR · All Rights Reserved
|
© {year} 银河初星计划 C . S . G · All Rights Reserved
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -90,8 +90,8 @@ export default function HeroBanner({
|
|||||||
<div className="absolute inset-0 max-w-[1500px] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="absolute inset-0 max-w-[1500px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{/* Eyebrow 左上 · 紧贴导航下方 */}
|
{/* Eyebrow 左上 · 紧贴导航下方 */}
|
||||||
<div className="absolute top-[6.5rem] sm:top-[7.5rem] left-4 sm:left-6 lg:left-8 z-10">
|
<div className="absolute top-[6.5rem] sm:top-[7.5rem] left-4 sm:left-6 lg:left-8 z-10">
|
||||||
<p className="font-label text-[10px] sm:text-xs tracking-[0.4em] uppercase text-purple-200/90">
|
<p className="font-label text-[10px] sm:text-xs tracking-[0.28em] uppercase text-purple-200/90">
|
||||||
Top 12 · Cyber Star Debut Survival
|
银河初星计划 C . S . G
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ interface LogoProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 高度由 size 控制,宽度按 logo.png 实际比例(约 5.41:1,单行 CYBER STAR + 星环)自适应
|
// 高度由 size 控制,宽度按 logo-v4.png 实际比例自适应
|
||||||
const HEIGHT_PX: Record<LogoSize, number> = {
|
const HEIGHT_PX: Record<LogoSize, number> = {
|
||||||
sm: 24,
|
sm: 24,
|
||||||
md: 44,
|
md: 44,
|
||||||
@ -25,11 +25,11 @@ export default function Logo({
|
|||||||
|
|
||||||
// 用原生 <img> 绕开 Next/Image 的格式转换 —— 某些环境下 sharp 把透明 PNG
|
// 用原生 <img> 绕开 Next/Image 的格式转换 —— 某些环境下 sharp 把透明 PNG
|
||||||
// 转 webp/avif 时会铺白底,导致 logo 在深色 nav 上出现白色矩形。
|
// 转 webp/avif 时会铺白底,导致 logo 在深色 nav 上出现白色矩形。
|
||||||
// ?v=2 缓存破坏:logo 改版时 +1,浏览器立刻拉新版而不读老缓存。
|
// ?v=4 缓存破坏:logo 改版时 +1,浏览器立刻拉新版而不读老缓存。
|
||||||
const inner = (
|
const inner = (
|
||||||
<img
|
<img
|
||||||
src="/logo.png?v=3"
|
src="/logo-v4.png?v=4"
|
||||||
alt="CYBER STAR"
|
alt="银河初星计划 C . S . G"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
style={{
|
style={{
|
||||||
@ -46,7 +46,7 @@ export default function Logo({
|
|||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="inline-flex items-center hover:opacity-90 transition-opacity"
|
className="inline-flex items-center hover:opacity-90 transition-opacity"
|
||||||
aria-label="CYBER STAR · 首页"
|
aria-label="银河初星计划 C . S . G · 首页"
|
||||||
style={{ background: "transparent" }}
|
style={{ background: "transparent" }}
|
||||||
>
|
>
|
||||||
{inner}
|
{inner}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import Logo from "./Logo";
|
||||||
import NavLinks from "./NavLinks";
|
import NavLinks from "./NavLinks";
|
||||||
import SearchTrigger from "./SearchTrigger";
|
import SearchTrigger from "./SearchTrigger";
|
||||||
import AuthMenu from "./auth/AuthMenu";
|
import AuthMenu from "./auth/AuthMenu";
|
||||||
@ -27,6 +28,7 @@ export default function Navigation() {
|
|||||||
// nav 关掉自己的玻璃,避免双重 backdrop-filter 在 y=80 处出现拼接线。
|
// nav 关掉自己的玻璃,避免双重 backdrop-filter 在 y=80 处出现拼接线。
|
||||||
const filterStuck = useUIStore((s) => s.filterStuck);
|
const filterStuck = useUIStore((s) => s.filterStuck);
|
||||||
const glassOff = isTransparent || filterStuck;
|
const glassOff = isTransparent || filterStuck;
|
||||||
|
const showLogo = pathname !== "/";
|
||||||
|
|
||||||
// 维护一个站内导航计数器(per-tab),供 FloatingBackButton 判断 router.back() 是否安全
|
// 维护一个站内导航计数器(per-tab),供 FloatingBackButton 判断 router.back() 是否安全
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -81,7 +83,16 @@ export default function Navigation() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<nav className="relative max-w-[1500px] mx-auto h-20 px-4 sm:px-6 lg:px-8 flex items-center gap-4 sm:gap-8">
|
<nav className="relative max-w-[1500px] mx-auto h-20 px-4 sm:px-6 lg:px-8 flex items-center gap-4 sm:gap-8">
|
||||||
{/* 左侧:首页 / 排行榜 / 我的(logo 已移除,所有屏宽都显示在第一行) */}
|
{showLogo && (
|
||||||
|
<div className="hidden sm:flex shrink-0 items-center">
|
||||||
|
<Logo
|
||||||
|
size="md"
|
||||||
|
className="drop-shadow-[0_0_14px_rgba(139,92,246,0.55)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 左侧:首页 / 排行榜 / 我的 */}
|
||||||
<NavLinks />
|
<NavLinks />
|
||||||
|
|
||||||
{/* 右侧:搜索 + 今日余票 + 登录/注册 (或 头像+下拉) */}
|
{/* 右侧:搜索 + 今日余票 + 登录/注册 (或 头像+下拉) */}
|
||||||
|
|||||||
@ -173,8 +173,8 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
|||||||
|
|
||||||
<div className="flex flex-col items-center mb-6">
|
<div className="flex flex-col items-center mb-6">
|
||||||
<img
|
<img
|
||||||
src="/logo-v3.png"
|
src="/logo-v4.png?v=4"
|
||||||
alt="CYBER STAR"
|
alt="银河初星计划 C . S . G"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
className="block select-none h-16 sm:h-20 w-auto"
|
className="block select-none h-16 sm:h-20 w-auto"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession, signOut } from "next-auth/react";
|
||||||
import { useVoteStore } from "@/lib/store";
|
import { useVoteStore } from "@/lib/store";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,6 +11,10 @@ import { useVoteStore } from "@/lib/store";
|
|||||||
* - status === "unauthenticated" → 清本地(避免上一个用户的票残留给下一个登录者)
|
* - status === "unauthenticated" → 清本地(避免上一个用户的票残留给下一个登录者)
|
||||||
* - 切换用户(uid 变化) → 重新拉一次
|
* - 切换用户(uid 变化) → 重新拉一次
|
||||||
*
|
*
|
||||||
|
* 僵尸 session 兜底:NextAuth 用 JWT 策略,cookie 不会因 DB user 被删而失效。
|
||||||
|
* 当 /api/me 返回 401(签名失效) 或 NOT_FOUND(DB 里 user 已不存在) 时,
|
||||||
|
* 自动 signOut() 清 cookie —— 避免页面"假登录"假象(显示已登录但拉不到数据)。
|
||||||
|
*
|
||||||
* localStorage 仅作为本设备的缓存加速首屏渲染,服务端永远是唯一真相源。
|
* localStorage 仅作为本设备的缓存加速首屏渲染,服务端永远是唯一真相源。
|
||||||
*/
|
*/
|
||||||
export function useSyncMe() {
|
export function useSyncMe() {
|
||||||
@ -45,12 +49,23 @@ export function useSyncMe() {
|
|||||||
credentials: "include",
|
credentials: "include",
|
||||||
signal: ctrl.signal,
|
signal: ctrl.signal,
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then(async (r) => {
|
||||||
.then((res) => {
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (res?.ok && Array.isArray(res.data?.votedArtists)) {
|
const res = await r.json().catch(() => null);
|
||||||
|
|
||||||
|
if (r.ok && res?.ok && Array.isArray(res.data?.votedArtists)) {
|
||||||
hydrateFromServer(res.data.votedArtists as string[]);
|
hydrateFromServer(res.data.votedArtists as string[]);
|
||||||
lastSyncedUidRef.current = uid;
|
lastSyncedUidRef.current = uid;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 僵尸 session:JWT 还有效,但 DB 里 user 已不存在(或鉴权失效)。
|
||||||
|
// 直接登出清 cookie,UI 状态切换为未登录,避免"显示已登录但拉不到数据"。
|
||||||
|
const code: string | undefined = res?.error?.code;
|
||||||
|
if (r.status === 401 || code === "UNAUTHORIZED" || code === "NOT_FOUND") {
|
||||||
|
signOut({ redirect: false });
|
||||||
|
reset();
|
||||||
|
lastSyncedUidRef.current = null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user