Files
jiang13-bbs/frontend/components/SiteChrome.tsx
freefire ff2ab286fb feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
2026-10-01 03:06:05 +08:00

125 lines
4.2 KiB
TypeScript
Raw 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 { 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);
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-[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}
</>
);
}