此提交完成了全站管理员后台核心功能开发: 1. 新增AdminUserService与管理接口,支持用户列表查询、角色变更与封禁解封 2. 重构认证链路,统一账号封禁错误码与处理逻辑 3. 新增全局强制下线机制,账号被封禁时自动弹出告知弹窗并清理登录态 4. 重构管理员后台路由与布局,统一权限校验逻辑 5. 优化通知轮询逻辑,页面切回前台时立即刷新未读计数 6. 合并原公告、外观管理页到统一后台面板
111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
import type { Metadata } from "next";
|
||
import { cookies } from "next/headers";
|
||
import { Plus_Jakarta_Sans, Noto_Sans_SC, JetBrains_Mono } from "next/font/google";
|
||
import "./globals.css";
|
||
import Header from "@/components/Header";
|
||
import Footer from "@/components/Footer";
|
||
import Toaster from "@/components/Toaster";
|
||
import AccountGuard from "@/components/AccountGuard";
|
||
import type { User } from "@/lib/api";
|
||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||
import { TOKEN_COOKIE, REFRESH_COOKIE, authCookieHeader } from "@/lib/cookies";
|
||
import { accentStyleCSS } from "@/lib/theme";
|
||
|
||
// 构建期下载并自托管字体:同源加载 + 自动 preload + fallback 度量校正,
|
||
// 消除 F5 时 Google CDN 字体到达后整页"由粗变细"的 FOUT 闪动
|
||
const jakarta = Plus_Jakarta_Sans({
|
||
subsets: ["latin"],
|
||
weight: ["400", "500", "600", "700", "800"],
|
||
variable: "--font-jakarta",
|
||
display: "swap",
|
||
});
|
||
|
||
const notoSC = Noto_Sans_SC({
|
||
weight: ["400", "500", "700", "900"],
|
||
variable: "--font-noto-sc",
|
||
display: "swap",
|
||
});
|
||
|
||
const jetbrainsMono = JetBrains_Mono({
|
||
subsets: ["latin"],
|
||
weight: ["400", "500", "700"],
|
||
variable: "--font-jetbrains",
|
||
display: "swap",
|
||
});
|
||
|
||
export const metadata: Metadata = {
|
||
title: {
|
||
default: "姜十三论坛 · J13 FORUM",
|
||
template: "%s · 姜十三论坛",
|
||
},
|
||
description: "姜十三论坛 - 技术分享与讨论社区",
|
||
openGraph: {
|
||
title: "姜十三论坛 · J13 FORUM",
|
||
description: "姜十三论坛 - 技术分享与讨论社区",
|
||
type: "website",
|
||
},
|
||
};
|
||
|
||
export default async function RootLayout({
|
||
children,
|
||
}: {
|
||
children: React.ReactNode;
|
||
}) {
|
||
const cookieStore = await cookies();
|
||
// 浅色优先的设计系统,仅在显式选择暗色时挂 .dark
|
||
const theme = cookieStore.get("j13-theme")?.value === "dark" ? "dark" : "light";
|
||
|
||
// SSR 直出登录态(access 过期时 middleware 已先行轮转,见 middleware.ts),
|
||
// 保证 F5 初始 HTML 中的右上角就是正确的用户区,而非先闪"登录/注册"。
|
||
// 旧版 refresh cookie 可能是 Path=/api/auth(页面请求不带),因此只要
|
||
// token 或 refresh 任一存在就向后端确认一次,避免半登录态(列表点赞态与
|
||
// Header 游客态不一致),并兼容 access 15 分钟内的自然窗口。
|
||
// 站点主题色(公开设置):派生浅/暗两套 accent CSS 变量后随首屏 HTML 内联,
|
||
// 覆盖 globals.css 默认令牌——无主题闪动(FOUC),后端不可用时降级为默认靛蓝
|
||
const { accent } = await getPublicSettingsCached();
|
||
const accentCSS = accentStyleCSS(accent || "");
|
||
|
||
let user: User | null = null;
|
||
let unread = 0;
|
||
const hasAuthCookie =
|
||
!!cookieStore.get(TOKEN_COOKIE)?.value || !!cookieStore.get(REFRESH_COOKIE)?.value;
|
||
if (hasAuthCookie) {
|
||
// 用户信息与未读数合并为一次请求;只转发最小化的 access cookie
|
||
const me = await getMeCached(authCookieHeader(cookieStore) || undefined);
|
||
user = me.user;
|
||
unread = me.unread_count;
|
||
}
|
||
|
||
return (
|
||
<html
|
||
lang="zh-CN"
|
||
className={`${theme === "dark" ? "dark" : ""} ${jakarta.variable} ${notoSC.variable} ${jetbrainsMono.variable}`}
|
||
>
|
||
<body className="min-h-screen flex flex-col">
|
||
{accentCSS ? (
|
||
// id 与 lib/theme 的运行时换肤共用:后台实时预览时直接复用/替换此节点
|
||
<style
|
||
id="j13-accent-vars"
|
||
data-accent-source="ssr"
|
||
dangerouslySetInnerHTML={{ __html: accentCSS }}
|
||
/>
|
||
) : null}
|
||
<a href="#main" className="skip-link">
|
||
跳到主要内容
|
||
</a>
|
||
<Header initialUser={user} initialUnread={unread} initialTheme={theme} />
|
||
<main
|
||
id="main"
|
||
tabIndex={-1}
|
||
className="flex-1 w-full max-w-[1440px] mx-auto px-4 sm:px-6 py-7 sm:py-9 outline-none"
|
||
>
|
||
{children}
|
||
</main>
|
||
<Footer />
|
||
<Toaster />
|
||
<AccountGuard />
|
||
</body>
|
||
</html>
|
||
);
|
||
}
|