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