80 lines
3.7 KiB
TypeScript
80 lines
3.7 KiB
TypeScript
/**
|
||
* # 组件 网站公共布局 Layout
|
||
* 组合 Header + 页面内容 + Footer;样式入口(Tailwind v4 + shadcn/ui)在此引入一次。
|
||
* 布局数据(菜单/分类/页脚 Elementor 模板)由各路由 loader 通过 getLayoutData() 取得后传入。
|
||
*/
|
||
import "../../styles/app.css";
|
||
import { Header } from "./Header";
|
||
import { Footer, type FooterCategory } from "./Footer";
|
||
import { InquiryPopup } from "./InquiryPopup";
|
||
import { createServerFn } from "@tanstack/react-start";
|
||
import { getFooterTemplate, getMenuTree, getProductCategoryTree, type MenuNode } from "~/server/data";
|
||
import type { ElElement } from "~/components/elementor/Renderer";
|
||
|
||
/** # 类型 布局数据(Header/Footer 所需) */
|
||
export type LayoutData = {
|
||
/** # 成员变量 顶部导航菜单树(max_mega_menu_1) **/
|
||
menu: MenuNode[];
|
||
/** # 成员变量 页脚公司链接菜单树(menu-2) **/
|
||
footerMenu: MenuNode[];
|
||
/** # 成员变量 产品分类树(含计数) **/
|
||
categories: FooterCategory[];
|
||
/** # 成员变量 页脚 Elementor 模板文档(原站 elementor_library post 58) **/
|
||
footerDoc: ElElement[] | null;
|
||
};
|
||
|
||
/** # 函数 Server Function:获取布局数据(菜单 + 分类 + 页脚模板)
|
||
* 返回类型标 any:Elementor 文档的 settings 是递归 unknown(JSON),TanStack 的
|
||
* 序列化静态校验无法展开递归 Record,会误报不可序列化;运行时就是纯 JSON。
|
||
*/
|
||
export const getLayoutData = createServerFn({ method: "GET" }).handler(async (): Promise<any> => {
|
||
const [menu, footerMenu, cats, footer] = await Promise.all([
|
||
getMenuTree("max_mega_menu_1"),
|
||
getMenuTree("menu-2"),
|
||
getProductCategoryTree(),
|
||
getFooterTemplate().catch(() => null),
|
||
]);
|
||
const el = (footer?.meta as { elementor?: unknown } | null)?.elementor;
|
||
const footerDoc = Array.isArray(el) ? (el as ElElement[]) : null;
|
||
return { menu, footerMenu, categories: cats, footerDoc } satisfies LayoutData;
|
||
});
|
||
|
||
/** # 常量 全站图片懒加载脚本(SSR 直出的 HTML 里 WP 图片多数已带 loading=lazy;
|
||
* 专管手工组件/用户编辑图片不带 lazy 的情况):
|
||
* - 未带 loading 属性的 <img> 全部补 loading="lazy" + decoding="async"
|
||
* - MutationObserver 覆盖后续新增节点
|
||
* - 加载失败兜底占位图(CSS 灰底占位背景在 img 未加载时可见)
|
||
*/
|
||
const LAZY_IMG_SCRIPT = `(function(){
|
||
var fix = function(root){
|
||
root.querySelectorAll && root.querySelectorAll('img').forEach(function(img){
|
||
if (img.hasAttribute('loading') || img.closest('header')) return;
|
||
img.setAttribute('loading','lazy');
|
||
if (!img.hasAttribute('decoding')) img.setAttribute('decoding','async');
|
||
});
|
||
};
|
||
var boot = function(){
|
||
fix(document);
|
||
new MutationObserver(function(muts){
|
||
for (var m of muts) for (var n of m.addedNodes) if (n.nodeType===1) fix(n);
|
||
}).observe(document.body,{childList:true,subtree:true});
|
||
};
|
||
document.readyState==='loading' ? document.addEventListener('DOMContentLoaded',boot) : boot();
|
||
})();`;
|
||
|
||
/** # 类型 Layout 属性 */
|
||
export function Layout(props: { layout: LayoutData; children: React.ReactNode }) {
|
||
return (
|
||
<>
|
||
<Header menu={props.layout.menu} />
|
||
{/* ds-site 前台标识:全局 img 占位图/懒加载样式按此作用域(不影响后台) */}
|
||
<main className="ds-site">{props.children}</main>
|
||
<Footer categories={props.layout.categories} menu={props.layout.footerMenu} doc={props.layout.footerDoc} />
|
||
{/* 全局询价弹窗:接管页面内所有 Elementor popup 1129 链接(Make an Inquiry / 按钮) */}
|
||
<InquiryPopup />
|
||
{/* 全站图片懒加载兜底脚本(绕过 React 水合,直接 setAttribute) */}
|
||
<script dangerouslySetInnerHTML={{ __html: LAZY_IMG_SCRIPT }} />
|
||
</>
|
||
);
|
||
}
|