"use client"; import { useEffect, useLayoutEffect, useRef, useState } from "react"; import { usePathname } from "next/navigation"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import BackToTop from "@/components/BackToTop"; import MobileTabBar from "@/components/MobileTabBar"; import type { User } from "@/lib/api"; import { isWideShellPath } from "@/lib/siteUrl"; import { libraryPath, userPath } from "@/lib/urlStyle"; import { useSiteBrand } from "@/components/SiteBrandProvider"; function pathIsAdmin(pathname: string | null) { if (!pathname) return false; return pathname === "/admin" || pathname.startsWith("/admin/"); } function pathIsChat(pathname: string | null) { if (!pathname) return false; return pathname === "/chat" || pathname.startsWith("/chat/"); } function pathIsComposeStudio(pathname: string | null) { if (!pathname) return false; return pathname === "/compose" || /^\/post\/[^/]+\/edit\/?$/.test(pathname); } /** * 公开站 chrome 闸门:/admin 不渲染 Header/Footer/1440 main。 * 首屏用 middleware 的 x-pathname 初值保证 hydration 一致; * 挂载后同步跟 usePathname(渲染期判定,避免 useEffect 晚一拍闪框)。 */ export default function SiteChrome({ initialIsAdmin, user, unread, chatUnread, theme, children, }: { initialIsAdmin: boolean; user: User | null; unread: number; chatUnread: number; theme: "light" | "dark"; children: React.ReactNode; }) { const pathname = usePathname(); const [mounted, setMounted] = useState(false); const isFirstPath = useRef(true); useEffect(() => { setMounted(true); }, []); // Next.js App Router:新页顶若仍在视口内会跳过滚顶。sticky Header 把 // 「微滚后的页顶」判成已可见,软导航就会停在资料卡上沿。父级 layoutEffect // 晚于 Next.js 子级滚动处理,且不影响 F5 / 带 hash 的定位。 useLayoutEffect(() => { if (isFirstPath.current) { isFirstPath.current = false; return; } if (pathIsAdmin(pathname)) return; if (window.location.hash) return; window.scrollTo({ top: 0, left: 0, behavior: "auto" }); }, [pathname]); // 首屏跟 SSR;之后在渲染期直接读 pathname,软导航无 Header 闪动 const isAdmin = mounted ? pathIsAdmin(pathname) : initialIsAdmin; useLayoutEffect(() => { document.documentElement.dataset.j13Chrome = isAdmin ? "admin" : "site"; }, [isAdmin]); const brand = useSiteBrand(); if (isAdmin) { return <>{children}; } // 聊天 / 发帖工作台锁视口,窗口回顶无意义 const showBackToTop = !pathIsChat(pathname) && !pathIsComposeStudio(pathname); // 底栏只在已启用 tab 的根路径显示(个人页子 tab 走 ?tab= 查询参数,不影响 pathname); // 发帖无「首页」概念不产生可见路径,发帖工作台 / 帖子编辑页继续排除 const enabledTabs = new Set(brand.mobile_tabs); const meHref = user ? userPath(brand.url_style, user.id) : ""; const tabBarRoots: string[] = []; if (enabledTabs.has("home")) tabBarRoots.push("/"); if (enabledTabs.has("library")) tabBarRoots.push(libraryPath(brand.url_style)); if (enabledTabs.has("messages")) tabBarRoots.push("/chat"); if (enabledTabs.has("me") && meHref) tabBarRoots.push(meHref); const showTabBar = !!pathname && !pathIsComposeStudio(pathname) && tabBarRoots.includes(pathname); // 书库页正文区放宽(Header/Footer 保持 1440,避免顶栏菜单位置随页面跳动) const wideShell = isWideShellPath(pathname); return ( <>
{children}