/**
* Data-Driven Header Navigation
* http://www.github.com/zhaohuijian
* based on Tailwind UI v3.4.17 and Heroicons v2.2.0
*/
"use client";
import { useState } from "react";
import {
Dialog,
DialogPanel,
Disclosure,
DisclosureButton,
DisclosurePanel,
Popover,
PopoverButton,
PopoverGroup,
PopoverPanel,
} from "@headlessui/react";
import {
ArrowPathIcon,
Bars3Icon,
ChartPieIcon,
CursorArrowRaysIcon,
FingerPrintIcon,
SquaresPlusIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import {
ChevronDownIcon,
PhoneIcon,
PlayCircleIcon,
} from "@heroicons/react/20/solid";
/**
* 数据驱动的导航结构示例
*/
const navigation = {
logo: {
href: "https://github.com/zhaohuijian",
srOnly: "Huijian's Blog",
src: "https://2.gravatar.com/avatar/2883808b7665c2a281d37dca1e69adf1c14ed0fd5394260e25c1332d1fd365ff",
},
navItems: [
{
title: "Product",
/*
* 子菜单项(subItems)
* 用于 Popover / Disclosure 下拉时列出的链接
*/
subItems: [
{
name: "Analytics",
description: "Get a better understanding of your traffic",
href: "#",
icon: ChartPieIcon,
},
{
name: "Engagement",
description: "Speak directly to your customers",
href: "#",
icon: CursorArrowRaysIcon,
},
{
name: "Security",
description: "Your customers’ data will be safe and secure",
href: "#",
icon: FingerPrintIcon,
},
{
name: "Integrations",
description: "Connect with third-party tools",
href: "#",
icon: SquaresPlusIcon,
},
{
name: "Automations",
description: "Build strategic funnels that will convert",
href: "#",
icon: ArrowPathIcon,
},
],
/**
* callsToAction:
* 用于子菜单底部那两格
*/
callsToAction: [
{ name: "Watch demo", href: "#", icon: PlayCircleIcon },
{ name: "Contact sales", href: "#", icon: PhoneIcon },
],
},
{
title: "Features",
/*
* 子菜单项(subItems)
* 用于 Popover / Disclosure 下拉时列出的链接
*/
subItems: [
{
name: "Analytics",
description: "Get a better understanding of your traffic",
href: "#",
},
{
name: "Engagement",
description: "Speak directly to your customers",
href: "#",
},
{
name: "Security",
description: "Your customers’ data will be safe and secure",
href: "#",
},
{
name: "Integrations",
description: "Connect with third-party tools",
href: "#",
},
{
name: "Automations",
description: "Build strategic funnels that will convert",
href: "#",
},
],
},
{
title: "Marketplace",
href: "#",
},
{
title: "Company",
/*
* 子菜单项(subItems)
* 用于 Popover / Disclosure 下拉时列出的链接
*/
subItems: [
{
name: "Analytics",
href: "#",
},
{
name: "Engagement",
href: "#",
},
{
name: "Security",
href: "#",
},
{
name: "Integrations",
href: "#",
},
{
name: "Automations",
href: "#",
},
],
},
],
/**
* 登录链接
*/
loginLink: {
href: "#",
label: "Log in",
},
};
export default function Example() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<div className="bg-white">
<header className="absolute inset-x-0 top-0 z-5">
<nav
className="mx-auto flex max-w-7xl items-center justify-between p-6 lg:px-8"
aria-label="Global"
>
{/* Logo 区域 */}
<div className="flex lg:flex-1 lg:items-center lg:gap-x-8">
<a href={navigation.logo.href} className="-m-1.5 p-1.5">
<span className="sr-only">{navigation.logo.srOnly}</span>
<img
className="h-8 w-auto rounded-sm"
src={navigation.logo.src}
alt=""
/>
</a>
</div>
{/* 移动端菜单按钮 */}
<div className="flex lg:hidden">
<button
type="button"
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700"
onClick={() => setMobileMenuOpen(true)}
>
<span className="sr-only">Open main menu</span>
<Bars3Icon className="size-6" aria-hidden="true" />
</button>
</div>
{/* 桌面端的菜单 */}
<PopoverGroup className="hidden lg:flex lg:gap-x-12">
{navigation.navItems.map((item) => {
// 如果有 subItems,渲染一个 Popover,里面包含子菜单
if (item.subItems && item.subItems.length > 0) {
return (
<Popover className="relative" key={item.title}>
<PopoverButton className="flex items-center gap-x-1 text-sm font-semibold text-gray-900">
{item.title}
<ChevronDownIcon
className="size-5 flex-none text-gray-400"
aria-hidden="true"
/>
</PopoverButton>
{/* PopoverPanel,显示子菜单 */}
<PopoverPanel
transition
className="absolute top-full -left-8 z-10 mt-3 w-max max-w-md overflow-hidden rounded-3xl bg-white shadow-lg ring-1 ring-gray-900/5 transition
data-closed:translate-y-1 data-closed:opacity-0 data-enter:duration-200 data-enter:ease-out data-leave:duration-150 data-leave:ease-in"
/* 也可以配合 Transition 组件做过渡 */
>
<div className="p-4">
{item.subItems.map((sub) => (
<div
key={sub.name}
className="group relative flex items-center gap-x-6 rounded-lg p-4 text-sm hover:bg-gray-50"
>
{sub.icon && (
<div className="flex size-11 flex-none items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white">
<sub.icon
className="size-6 text-gray-600 group-hover:text-indigo-600"
aria-hidden="true"
/>
</div>
)}
<div className="flex-auto">
<a
href={sub.href}
className="block font-semibold text-gray-900"
>
{sub.name}
<span className="absolute inset-0" />
</a>
{sub.description && (
<p className="mt-1 text-gray-600">
{sub.description}
</p>
)}
</div>
</div>
))}
</div>
{/* callsToAction 区域 */}
{item.callsToAction && item.callsToAction.length > 0 && (
<div className="grid grid-cols-2 divide-x divide-gray-900/5 bg-gray-50">
{item.callsToAction.map((cta) => (
<a
key={cta.name}
href={cta.href}
className="flex items-center justify-center gap-x-2.5 p-3 text-sm font-semibold text-gray-900 hover:bg-gray-100"
>
<cta.icon
className="size-5 flex-none text-gray-400"
aria-hidden="true"
/>
{cta.name}
</a>
))}
</div>
)}
</PopoverPanel>
</Popover>
);
}
// 如果没有 subItems,则只是普通的链接
return (
<a
key={item.title}
href={item.href}
className="text-sm font-semibold text-gray-900"
>
{item.title}
</a>
);
})}
</PopoverGroup>
{/* 桌面端:右侧的登录按钮 */}
<div className="hidden lg:ml-8 lg:flex lg:items-center lg:border-l lg:border-slate-900/15 lg:pl-8">
<a href="/login">Sign in</a>
<a
className="inline-flex justify-center rounded-lg text-sm font-semibold py-2.5 px-4 bg-slate-900 text-white hover:bg-slate-700 -my-2.5 ml-8"
href="/all-access"
>
<span>
Get all-access <span aria-hidden="true">→</span>
</span>
</a>
</div>
</nav>
{/* 移动端侧边栏菜单 */}
<Dialog
as="div"
className="lg:hidden"
open={mobileMenuOpen}
onClose={setMobileMenuOpen}
>
{/* 背景遮罩 */}
<div className="fixed inset-0 z-10 bg-slate-900/25 backdrop-blur-sm transition-opacity" />
<DialogPanel className="fixed inset-y-0 right-0 z-10 w-full overflow-y-auto bg-white px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10">
{/* 顶部Logo 和 关闭按钮 */}
<div className="flex items-center justify-between">
<a href={navigation.logo.href} className="-m-1.5 p-1.5">
<span className="sr-only">{navigation.logo.srOnly}</span>
<img className="h-8 w-auto" src={navigation.logo.src} alt="" />
</a>
<button
type="button"
className="-m-2.5 rounded-md p-2.5 text-gray-700"
onClick={() => setMobileMenuOpen(false)}
>
<span className="sr-only">Close menu</span>
<XMarkIcon className="size-6" aria-hidden="true" />
</button>
</div>
{/* 移动端菜单本体 */}
<div className="mt-6 flow-root">
<div className="-my-6 divide-y divide-gray-500/10">
{/* 顶层导航区 */}
<div className="space-y-2 py-6">
{navigation.navItems.map((item) => {
// 如果有 subItems,则在移动端使用 Disclosure
if (item.subItems && item.subItems.length > 0) {
return (
<Disclosure as="div" className="-mx-3" key={item.title}>
<DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 pr-3.5 pl-3 text-base font-semibold text-gray-900 hover:bg-gray-50">
{item.title}
<ChevronDownIcon
className="size-5 flex-none group-data-open:rotate-180"
aria-hidden="true"
/>
</DisclosureButton>
<DisclosurePanel className="mt-2 space-y-2">
{[
...item.subItems,
...(item.callsToAction || []),
].map((sub) => (
<DisclosureButton
key={sub.name}
as="a"
href={sub.href}
className="block rounded-lg py-2 pr-3 pl-6 text-sm font-semibold text-gray-900 hover:bg-gray-50"
>
{sub.name}
</DisclosureButton>
))}
</DisclosurePanel>
</Disclosure>
);
}
// 否则普通的菜单项
return (
<a
key={item.title}
href={item.href}
className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold text-gray-900 hover:bg-gray-50"
>
{item.title}
</a>
);
})}
</div>
{/* 登录按钮区 */}
<div className="py-6">
<div className="-my-2 space-y-4">
<a
className="block w-full py-2 font-semibold"
href="/login"
>
Sign in
</a>
<a
className="inline-flex justify-center rounded-lg text-sm font-semibold py-3 px-4 bg-slate-900 text-white hover:bg-slate-700 w-full"
href="/all-access"
>
<span>
Get all-access <span aria-hidden="true">→</span>
</span>
</a>
</div>
</div>
</div>
</div>
</DialogPanel>
</Dialog>
</header>
</div>
);
}