Files
dusuniot/src/components/website/Layout.tsx
T

80 lines
3.7 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.
/**
* # 组件 网站公共布局 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 }} />
</>
);
}