/** * # 组件 网站顶部导航 Header(对齐原站,Tailwind + shadcn/ui 实现) * 结构:深灰 topbar(口号 + 社交/登录)+ 白底主导航(logo + NavigationMenu + * 搜索图标 + CONTACT US + 邮件/语言图标)。移动端汉堡菜单(纯 React state)。 * * 桌面导航用 shadcn/ui NavigationMenu(Radix)实现,巨幕面板逐列复刻原站 * Max Mega Menu 布局与样式(对原站 megamenu 内联 CSS + 无头浏览器 computed style * 的逐项实测结论,2026-10): * - 顶级条:高 68px、16px/400/#222,hover/展开文字变橙,实心三角指示器 * - Product:4 列巨幕(SoM | 主板+SBC+图 | 灰底 IoT 网关宽列(双子栏+芯片logo) | 其它产品) * - Services:3 列(图 | ODM/OEM 服务 | 开发者支持),分组头带 #fad3b3 下边线 * - Solutions:4 列分组 + 首列「一站式硬件方案」橙色标题 * - Resources:原站为自定义 HTML 标签面板(博客/新闻/认证表格),本站以资源链接面板呈现 * - Success Stories:普通链接;Company:flyout 下拉列表 * - 面板 chrome:#fcfbfa 底 + 顶部 2px #f5b279 橙线 + 0 10px 30px 阴影,直角 * - 分组头三级样式:橙 18px(星标) / 橙 16px / 深色加粗+#e0e5f9 下边线;叶子 16px/400 * 切换动效为 shadcn/ui(tw-animate-css fade/zoom/slide),与原站视觉分离保留。 * 菜单数据来自 DB max_mega_menu_1(getMenuTree);面板列布局/样式以标题匹配的 * 配置常量描述(menu_items 表不存布局信息,widget 图片不在菜单表内,只能配置化)。 */ import { useState } from "react"; import { Link } from "@tanstack/react-router"; import { Search, Menu, X, ChevronRight, Mail, MessageCircleMore, Star } from "lucide-react"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, CaretDown, } from "~/components/ui/navigation-menu"; import { LangSwitcher } from "~/components/website/LangSwitcher"; import { useT } from "~/i18n"; import type { MenuNode } from "~/server/data"; /** # 类型 巨幕面板列配置(groups 引用 menu_items 表里的分组标题) */ type MegaCol = { /** # 成员变量 本列分组标题清单(与 DB 菜单标题一致) **/ groups?: string[]; /** # 成员变量 列顶标题(原站 Solutions 首列「一站式硬件方案」) **/ heading?: string; /** # 成员变量 列内双子栏分组(原站 IoT 网关灰底宽列:左「By Application/多协议」右「平台支持」) **/ subCols?: string[][]; /** # 成员变量 列底部 widget 图片(原站 media_image widget) **/ image?: { src: string; width: number; height: number; alt: string }; /** # 成员变量 列底部芯片厂商 logo 行(原站 6 个 media_image widget) **/ logos?: boolean; /** # 成员变量 灰底列(原站实测 #f4f5f8,仅 IoT 网关宽列使用) **/ grey?: boolean; }; /** # 成员变量 Product 巨幕 4 列布局(列宽比按原站实测 239:253:519:319) * 注意 menu_items 里 Product 的子项是「平铺的分组」(NXP Serials / By Application / * Other Products 的子分组均为 Product 直接子级),原站同列多个分组靠 CSS 呈现 * 层级;这里按原站视觉把它们编入同一列。灰底宽列含双子栏与芯片 logo 行。 **/ const PRODUCT_COLS: MegaCol[] = [ { groups: ["System on Modules >", "NXP Serials", "Rockchip Serials", "MediaTek Serials"] }, { groups: ["Embedded Mainboard", "Custom Industrial SBCs"], image: { src: "/uploads/2024/08/som-board.png", width: 211, height: 137, alt: "som board" } }, { grey: true, groups: ["IoT Gateway >"], subCols: [["By Application", "Multi Protocols Support"], ["Platform/Software Support"]], logos: true }, { groups: ["Other Products", "Remote Control >", "Far-Field Voice >", "BLE Beacon >", "Control Board/IoT Terminal Devices >", "Smart Sensors >"] }, ]; /** # 成员变量 Services 巨幕 3 列布局(列宽比按原站实测 230:460:230;分组头带浅橙下边线) **/ const SERVICES_COLS: MegaCol[] = [ { image: { src: "/uploads/2024/08/odm-oem-service-dusuniot.jpg", width: 234, height: 300, alt: "ODM/OEM Services" } }, { groups: ["ODM/OEM Services >"] }, { groups: ["Developer Support"] }, ]; /** # 成员变量 Solutions 巨幕 4 列布局(列宽比按原站实测 289:246:278:257,首列带「一站式硬件方案」标题) **/ const SOLUTIONS_COLS: MegaCol[] = [ { groups: ["Smart Control", "Building Automation"], heading: "One-Stop Hardware Solutions" }, { groups: ["Healthcare IoT", "Smart Home"] }, { groups: ["Security & Automation", "Smart Retail >"] }, { groups: ["Embedded Applications"] }, ]; /** # 成员变量 橙色星标分组头(原站 dashicons-star-filled + mega-has-icon) **/ const STAR_GROUPS = new Set(["System on Modules >", "Embedded Mainboard", "IoT Gateway >", "ODM/OEM Services >"]); /** # 成员变量 橙色 18px 无星分组头(原站对应列标题,实测 computed style) **/ const ORANGE_GROUPS = new Set(["Other Products", "Custom Industrial SBCs", "Developer Support", "Embedded Applications"]); /** # 成员变量 橙色 16px 小分组头(原站 Other Products 列内嵌套橙色组,实测 16px/700) **/ const ORANGE_SM_GROUPS = new Set(["Control Board/IoT Terminal Devices >", "Smart Sensors >"]); /** # 成员变量 深色加粗但无下边线的分组头(原站 Smart Retail >,实测无 border-bottom) **/ const DARK_NOLINE_GROUPS = new Set(["Smart Retail >"]); /** # 成员变量 Resources 面板链接(原站为自定义 HTML 标签面板;此处收录其全部资源入口,本地均已 200) **/ const RESOURCES_LINKS: { title: string; url: string; note: string }[] = [ { title: "Blog", url: "/blog/", note: "博客归档" }, { title: "News", url: "/news/", note: "新闻归档" }, { title: "Gateway Articles", url: "/gateway/", note: "网关技术文章归档" }, { title: "Certification", url: "/certification/", note: "认证页(原站 Resources 面板标签)" }, { title: "Hardware News", url: "/resources/hardware-news/", note: "原站 Resources 面板链接" }, { title: "Software Update & News", url: "/resources/software-updates/", note: "原站 Resources 面板链接" }, { title: "WiKi Resources", url: "https://wiki.dusuniot.com/", note: "原站 Services 面板同款外链" }, { title: "Videos", url: "https://www.youtube.com/channel/UCyb4PpqVgvKgC9KpkByZaaQ", note: "原站 Services 面板同款外链" }, ]; /** # 类型 Header 属性:菜单树由服务端 loader 传入 */ export function Header(props: { menu: MenuNode[] }) { const t = useT(); const [open, setOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); // 菜单为空时使用内置后备导航(数据导入未完成时优雅降级) const items: MenuNode[] = props.menu.length > 0 ? props.menu : [ { id: -1, title: "Home", url: "/", target: "", children: [] }, { id: -2, title: "Products", url: "/collection/", target: "", children: [] }, { id: -3, title: "Blog", url: "/blog/", target: "", children: [] }, { id: -4, title: "Contact Us", url: "/contact-us/", target: "", children: [] }, ]; // 原站顶栏不含「Search」菜单项(搜索是独立图标),过滤掉 const tops = items.filter((it) => it.title !== "Search"); return (
{/* topbar:口号 + 社交图标(原站实测 2026-11-04 用户指定:容器 1400 居中、 高 36px、图标 36×36 盒 gap 10、顺序 Linkedin/Youtube/Facebook、无登录项;<768 隐藏) */}
{t("topbar_oem")}
{/* 主导航(原站 2026-11-04 用户指定:导航容器 1400px 居中;桌面 68px、logo 后 80px 起菜单; 平板 768-959 为汉堡导航(顶栏 36 + 导航 48,logo 缩放,右侧 CONTACT US+邮件); 手机 <768 无顶栏、导航 48、汉堡在 logo 右 96px、右侧橙色气泡+邮件) */}
{/* 直接复用原站 header logo(Elementor site-logo 同一图片,实测渲染 171×42.75px) */} Dusun IoT
{searchOpen ? (
) : ( )}
{/* 汉堡:原站为 mega-toggle 块(手机位于 logo 右 96px、平板右聚拢),图标浅灰 */}
{/* 移动端菜单 */} {open ? (
{tops.map((it) => ( setOpen(false)} /> ))} setOpen(false)} className="block border-b border-line px-1 py-[9px] font-medium text-ink"> {t("contact_us")}
) : null}
); } /** # 组件 桌面主导航(shadcn/ui NavigationMenu) * 按原站菜单顺序逐项渲染,每个顶级项一个 NavigationMenu 根(视口形态不同: * 巨幕项用全宽视口,Company 用居中小视口,Success Stories 是纯链接)。 * 独立根互不干扰,指针移出根范围即收起;根间距用外层 gap 保持一致。 */ function DesktopNav({ tops }: { tops: MenuNode[] }) { return (
{tops.map((node) => { const linkHref = TOP_URL_FIX[node.title] ?? normalizeUrl(node.url) ?? undefined; // 巨幕面板项(全宽视口) if (node.title === "Product" || node.title === "Services" || node.title === "Solutions" || node.title === "Resources") { return ( {node.title === "Product" ? ( ) : node.title === "Services" ? ( ) : node.title === "Solutions" ? ( ) : ( )} ); } // Company:flyout 简单下拉列表(居中小视口) if (node.children.length > 0) { return (
    {node.children.map((c) => { const info = linkInfo(TOP_URL_FIX[c.title] ?? c.url); return info ? (
  • {c.title}
  • ) : (
  • {c.title}
  • ); })}
); } // 无面板的普通链接(Success Stories,样式与顶级触发项一致) return linkHref ? ( {node.title} ) : null; })}
); } /** # 组件 Resources 面板(原站为自定义 HTML 标签面板,此处收录其全部资源入口;链接均已本地 200) */ function ResourcesPanel() { return (
{RESOURCES_LINKS.map((r) => ( {r.title} ))}
); } /** # 成员变量 顶级/子级触发链接修正(DB 存「#」或旧路径,按原站现值修正;均已本地 200) **/ const TOP_URL_FIX: Record = { Product: "/shop/", // 原站 Product 触发链接 → /shop/ Solutions: "/solution/", // 原站 Solutions 触发链接 → /solution/ "Blog >": "/blog/", // Company/Services 里的 Blog 子项(DB 为 #) "News >": "/news/", // Company 里的 News 子项(DB 为 #) }; /** # 组件 顶级触发项:有链接时 asChild 渲染 (点击可跳转、样式落在 上 * 以免被全局 a{color:brand} 染橙),否则纯按钮。指示器为实心三角(原站 * dashicons arrow-down,展开不旋转、颜色随文字变橙)。 */ function TopTrigger({ title, href }: { title: string; href?: string }) { if (href) { return ( {title} ); } return {title}; } /** # 组件 巨幕面板:按列配置从菜单树取分组并渲染 * 参数 cols 列布局配置;source 菜单树(顶级项 children);layout 列宽 grid-template; * headLine 分组头是否带浅橙下边线(原站实测:Services 面板有 #fad3b3 线,Product/Solutions 无)。 * 样式按原站 megamenu CSS 实测:面板内边距 10px 0 20px 10px;每个分组条目块 * padding 15px(底 0);灰底列 #f4f5f8 通高。 */ function MegaPanel(props: { cols: MegaCol[]; source: MenuNode[]; layout: string; headLine?: boolean }) { return (
{props.cols.map((col, i) => { const groups = (col.groups ?? []).map((g) => props.source.find((n) => n.title === g)).filter((n): n is MenuNode => Boolean(n)); const subCols = (col.subCols ?? []).map((titles) => titles.map((g) => props.source.find((n) => n.title === g)).filter((n): n is MenuNode => Boolean(n))); const body = ( <> {col.heading ?
{col.heading}
: null} {groups.map((g) => ( ))} {subCols.length > 0 ? (
{subCols.map((gs, si) => (
{gs.map((g) => ( ))}
))}
) : null} {col.image ? (
{col.image.alt}
) : null} {col.logos ? (
{["rockchip", "nxp", "mtk", "espressif", "silicon", "nordic"].map((n) => ( {n} ))}
) : null} ); return col.grey ? (
{body}
) : (
{body}
); })}
); } /** # 组件 巨幕分组:分组头(橙 18px 星标 / 橙 16px / 深色加粗+#e0e5f9 下边线) * + 子项列表(子项再含子级时渲染二级分组)。条目块 padding 15px(底 0)按原站实测。 */ function MegaGroup({ node, headLine }: { node: MenuNode; headLine?: boolean }) { const head = linkInfo(node.url); const star = STAR_GROUPS.has(node.title); const orangeLg = star || ORANGE_GROUPS.has(node.title); const orangeSm = ORANGE_SM_GROUPS.has(node.title); const darkNoLine = DARK_NOLINE_GROUPS.has(node.title); let headCls: string; if (orangeLg) headCls = `text-[18px] font-bold text-brand${headLine ? " border-b border-group-line pb-2.5" : ""}`; else if (orangeSm) headCls = "text-[16px] font-bold text-brand"; else if (darkNoLine) headCls = "text-[16px] font-bold text-[#222]"; else headCls = "border-b border-subline pb-1 text-[16px] font-bold text-[#222]"; const headWrap = `flex items-center gap-1.5 whitespace-nowrap ${orangeLg ? "mb-3" : "mb-2.5"} ${headCls}`; return (
{head ? ( {star ? : null} {node.title} ) : (
{star ? : null} {node.title}
)}
); } /** # 组件 巨幕叶子链接:站内 / 外链新窗口 / 无链接的普通文本(原站 16px/400/#222) */ function MegaItemLink({ node }: { node: MenuNode }) { const info = linkInfo(node.url); const cls = "block leading-[1.7] text-[16px] text-[#222] hover:text-brand"; if (info) { return ( {node.title} ); } return {node.title}; } /** # 组件 顶部社交图标(lucide 新版已移除品牌图标,改为内联 SVG) */ function YoutubeGlyph() { return ( ); } function FacebookGlyph() { return ( ); } function LinkedinGlyph() { return ( ); } /** # 组件 移动端导航项(子菜单平铺展示) */ function MobileNavItem({ node, onNavigate }: { node: MenuNode; onNavigate: () => void }) { const href = normalizeUrl(node.url); return (
{href ? ( {node.title} ) : ( {node.title} )} {node.children.map((c) => { const ch = normalizeUrl(c.url); return (
{ch ? ( {c.title} ) : ( {c.title} )}
); })}
); } /** # 函数 菜单 URL 归一化为站内路径(外部链接返回 null) */ function normalizeUrl(url: string): string | null { if (!url) return null; // 完整 URL:仅同域或相对路径可站内跳转 if (/^https?:\/\//i.test(url)) { try { const u = new URL(url); if (u.host === "dusuniot.com" || u.host === "www.dusuniot.com") return u.pathname; return null; } catch { return null; } } if (url.startsWith("#") || url.startsWith("mailto:") || url.startsWith("tel:")) return null; return url; } /** # 类型 菜单链接解析结果 */ /** # 函数 菜单 URL → 可渲染链接(站内路径 / 外链原样;「#」等返回 null) * 与 normalizeUrl 的差异:外链(WiKi/YouTube/LinkedIn 等)原样保留,由调用方 * 以新窗口锚点渲染,与原站 mega menu 行为一致。 */ function linkInfo(url: string): { href: string; external: boolean } | null { if (!url || url.startsWith("#") || url.startsWith("mailto:") || url.startsWith("tel:")) return null; if (/^https?:\/\//i.test(url)) { try { const u = new URL(url); if (u.host === "dusuniot.com" || u.host === "www.dusuniot.com") return { href: u.pathname, external: false }; return { href: url, external: true }; } catch { return null; } } return { href: url, external: false }; }