/** * # 组件 网站公共布局 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 => { 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 属性的 全部补 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 ( <>
{/* ds-site 前台标识:全局 img 占位图/懒加载样式按此作用域(不影响后台) */}
{props.children}