feat(frontend+backend): 实现SSR登录态优化与字体自托管
1. 后端调整refresh cookie路径与注释,完善认证cookie处理逻辑 2. 前端全局layout新增next/font自托管字体,消除FOUT闪动 3. 实现SSR直出登录态与未读通知数,避免页面加载闪动 4. 新增中间件处理access token静默轮转,保障SSR登录态 5. 优化登录、改密流程,添加页面刷新同步登录态 6. 重构通知铃铛组件支持初始未读数传入
This commit is contained in:
@@ -36,12 +36,14 @@
|
||||
--shadow: 0 2px 8px rgba(87, 66, 36, 0.05), 0 10px 28px rgba(87, 66, 36, 0.05);
|
||||
--shadow-lg: 0 6px 16px rgba(87, 66, 36, 0.07), 0 20px 48px rgba(87, 66, 36, 0.1);
|
||||
|
||||
--font-display: "Plus Jakarta Sans", "Noto Sans SC", -apple-system, "PingFang SC",
|
||||
"Microsoft YaHei", system-ui, sans-serif;
|
||||
--font-sans: "Plus Jakarta Sans", "Noto Sans SC", -apple-system, "PingFang SC",
|
||||
"Microsoft YaHei", system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "Noto Sans SC", ui-monospace, "Cascadia Code",
|
||||
Consolas, monospace;
|
||||
/* next/font 构建期自托管,变量由 layout.tsx 注入;
|
||||
本地系统字体仅作网络字体分片就绪前的回退(PingFang/雅黑与 Noto 观感接近) */
|
||||
--font-display: var(--font-jakarta), var(--font-noto-sc), -apple-system,
|
||||
"PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||
--font-sans: var(--font-jakarta), var(--font-noto-sc), -apple-system,
|
||||
"PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||
--font-mono: var(--font-jetbrains), var(--font-noto-sc), ui-monospace,
|
||||
"Cascadia Code", Consolas, monospace;
|
||||
|
||||
--r: 18px;
|
||||
--r-sm: 12px;
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
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 { fetchMe, fetchUnreadCount, type User } from "@/lib/api";
|
||||
|
||||
// 构建期下载并自托管字体:同源加载 + 自动 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: {
|
||||
@@ -27,21 +51,34 @@ export default async function RootLayout({
|
||||
// 浅色优先的设计系统,仅在显式选择暗色时挂 .dark
|
||||
const theme = cookieStore.get("j13-theme")?.value === "dark" ? "dark" : "light";
|
||||
|
||||
// SSR 直出登录态(access 过期时 middleware 已先行轮转,见 middleware.ts),
|
||||
// 保证 F5 初始 HTML 中的右上角就是正确的用户区,而非先闪"登录/注册"
|
||||
const cookieHeader = cookieStore.toString();
|
||||
let user: User | null = null;
|
||||
let unread = 0;
|
||||
if (cookieStore.get("j13_refresh")?.value) {
|
||||
try {
|
||||
const [me, count] = await Promise.all([
|
||||
fetchMe(cookieHeader),
|
||||
fetchUnreadCount(cookieHeader),
|
||||
]);
|
||||
user = me.user;
|
||||
unread = count;
|
||||
} catch {
|
||||
// 后端不可用时降级为游客视图,不阻塞页面渲染
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="zh-CN" className={theme === "dark" ? "dark" : ""}>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&family=Noto+Sans+SC:wght@400;500;700;900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<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 />
|
||||
<Header initialUser={user} initialUnread={unread} initialTheme={theme} />
|
||||
<main
|
||||
id="main"
|
||||
tabIndex={-1}
|
||||
|
||||
@@ -23,7 +23,9 @@ export default function LoginPage() {
|
||||
const res = await apiLogin(username, password);
|
||||
// 登录成功后后端通过 Set-Cookie 写入 HttpOnly JWT,前端无需存储 token
|
||||
if (res.user) {
|
||||
// root layout 在客户端导航中持久化,push 后再 refresh 让其重新取登录态
|
||||
router.push(redirect);
|
||||
router.refresh();
|
||||
} else {
|
||||
setError(res.error || "登录失败");
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ export default function SettingsPage() {
|
||||
const res = await apiChangePassword(oldPassword, newPassword);
|
||||
if (res.message) {
|
||||
setSuccess(true);
|
||||
// 后端已使全部 token 失效并清 cookie,刷新 layout 让 Header 回到游客态
|
||||
router.refresh();
|
||||
setTimeout(() => router.push("/login"), 1500);
|
||||
} else {
|
||||
setError(res.error || "修改失败");
|
||||
|
||||
Reference in New Issue
Block a user