Files
jiang13-bbs/frontend/components/SiteChrome.tsx
freefire 1d862a0dd3 feat: 站点媒体库与移动端底部导航,服务测试补强
- 新增媒体库:media_library/media_thumbs 服务(WebP 缩略图)、管理端 media 页面
- 移动端底部导航 MobileTabBar 替换 MobilePostBar
- 旧数据导入增强与测试、路由与登录会话/板块侧边栏测试补强
- site-doc 组件精简(移除 Breadcrumb),文档新增迁移公告说明
2026-09-27 02:36:11 +08:00

120 lines
3.7 KiB
TypeScript
Raw Permalink 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.
"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 { 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);
}
/** 底部公共菜单避让:聊天 / 发帖工作台锁视口,不渲染底栏 */
function pathHasNoTabBar(pathname: string | null) {
return pathIsChat(pathname) || pathIsComposeStudio(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);
const showTabBar = showBackToTop;
return (
<>
<Header
initialUser={user}
initialUnread={unread}
initialChatUnread={chatUnread}
initialTheme={theme}
siteName={brand.site_name}
wordmark={brand.site_wordmark}
logoLight={brand.logo_light_url}
logoDark={brand.logo_dark_url}
brandMark={brand.brand_mark}
brandLogoSize={brand.brand_logo_size}
brandLogoFit={brand.brand_logo_fit}
/>
<main
id="main"
tabIndex={-1}
className="flex-1 w-full max-w-[1440px] mx-auto px-4 sm:px-6 pt-4 sm:pt-[var(--main-pad-y)] pb-[calc(4.75rem+env(safe-area-inset-bottom))] lg:pb-[var(--main-pad-y)] outline-none"
>
{children}
</main>
<Footer siteName={brand.site_name} links={brand.footer_links} />
{showTabBar ? (
<MobileTabBar user={user} initialChatUnread={chatUnread} />
) : null}
{showBackToTop ? <BackToTop /> : null}
</>
);
}