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:
2026-09-12 04:59:26 +08:00
parent 1eaf3903d4
commit 1bb565c687
9 changed files with 226 additions and 30 deletions

View File

@@ -17,19 +17,33 @@ import {
Bell,
Home,
} from "lucide-react";
import { apiMe, apiLogout, type User } from "@/lib/api";
import { apiLogout, type User } from "@/lib/api";
import NotificationBell from "./NotificationBell";
import Avatar from "./Avatar";
export default function Header() {
export default function Header({
initialUser,
initialUnread = 0,
initialTheme = "light",
}: {
initialUser: User | null;
initialUnread?: number;
initialTheme?: "light" | "dark";
}) {
const router = useRouter();
const pathname = usePathname();
const [menuOpen, setMenuOpen] = useState(false);
const [theme, setTheme] = useState<"light" | "dark">("light");
const [user, setUser] = useState<User | null>(null);
const [theme, setTheme] = useState<"light" | "dark">(initialTheme);
// 登录态以 SSR 直出为初始值,F5 无切换闪动;router.refresh() 后随 props 校正
const [user, setUser] = useState<User | null>(initialUser);
const [searchQuery, setSearchQuery] = useState("");
const searchInputRef = useRef<HTMLInputElement>(null);
// 登录/登出触发 router.refresh() 后,服务端 layout 重新取态并下发新 props
useEffect(() => {
setUser(initialUser);
}, [initialUser]);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
const q = searchQuery.trim();
@@ -45,9 +59,6 @@ export default function Header() {
document.documentElement.classList.toggle("dark", savedTheme === "dark");
setTheme(savedTheme);
setSearchQuery(new URLSearchParams(window.location.search).get("q")?.trim() || "");
apiMe()
.then((res) => setUser(res.user || null))
.catch(() => setUser(null));
}, [pathname]);
// "/" 快速聚焦搜索
@@ -173,7 +184,7 @@ export default function Header() {
<button onClick={toggleTheme} className={iconBtn} style={iconBtnStyle} aria-label="切换主题">
{theme === "dark" ? <Sun size={17} /> : <Moon size={17} />}
</button>
{user && <NotificationBell />}
{user && <NotificationBell initialUnread={initialUnread} />}
{user ? (
<>
<Link href="/compose" className="btn btn-primary btn-sm ml-1">
@@ -210,7 +221,7 @@ export default function Header() {
{/* 移动端按钮 */}
<div className="flex items-center gap-2 lg:hidden">
{user && <NotificationBell />}
{user && <NotificationBell initialUnread={initialUnread} />}
<button
onClick={() => setMenuOpen(!menuOpen)}
className="w-10 h-10 flex items-center justify-center rounded-full"

View File

@@ -12,10 +12,11 @@ import {
type NotificationItem,
} from "@/lib/api";
export default function NotificationBell() {
export default function NotificationBell({ initialUnread = 0 }: { initialUnread?: number }) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [unread, setUnread] = useState(0);
// SSR 直出未读数,红点不再挂载后才弹出;挂载后轮询自然校正
const [unread, setUnread] = useState(initialUnread);
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const [loading, setLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);