"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}
{showTabBar ? (
) : null}
{showBackToTop ? : null}
>
);
}