Files
jiang13-bbs/frontend/app/layout.tsx
freefire e2e5ba077c feat: 添加用户个人资料编辑功能,优化页面布局与导航
1. 后端新增用户资料更新接口,支持修改昵称、头像、邮箱和签名
2. 前端新增资料编辑表单页面,适配响应式布局
3. 优化帖子详情页布局与移动端适配逻辑
4. 重构首页侧边栏导航,使用卡片式板块展示
5. 移除全局板块缓存,调整Header导航移除板块菜单
6. 补充用户签名展示功能,完善用户信息返回字段
2026-09-14 16:10:38 +08:00

95 lines
3.2 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 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 type { User } from "@/lib/api";
import { getMeCached } from "@/lib/serverData";
import { TOKEN_COOKIE, REFRESH_COOKIE, authCookieHeader } from "@/lib/cookies";
// 构建期下载并自托管字体:同源加载 + 自动 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 分钟内的自然窗口。
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">
<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 />
</body>
</html>
);
}