Files
web/src/app/(home)/header.tsx
2026-03-14 15:25:23 +08:00

303 lines
8.3 KiB
TypeScript

'use client'
import {useMemo, useState, PointerEvent, ComponentProps, useSyncExternalStore, use, Suspense, MouseEvent} from 'react'
import Link from 'next/link'
import Image from 'next/image'
import {HeaderContext} from './_components/header/common'
import SolutionMenu from './_components/header/menu-solution'
import ProductMenu from './_components/header/menu-product'
import HelpMenu from './_components/header/menu-help'
import MobileMenu from './_components/header/menu-mobile'
import Wrap from '@/components/wrap'
import logo from '@/assets/logo.webp'
import {Button} from '@/components/ui/button'
import {useProfileStore} from '@/components/stores/profile'
import UserCenter from '@/components/composites/user-center'
import {MenuIcon} from 'lucide-react'
import down from '@/assets/header/down.svg'
import {merge} from '@/lib/utils'
export type HeaderProps = {}
export default function Header(props: HeaderProps) {
// ======================
// 滚动条状态
// ======================
const scroll = useSyncExternalStore((callback) => {
window.addEventListener('scroll', callback)
return () => {
window.removeEventListener('scroll', callback)
}
}, () => window.scrollY > 48, () => false)
// ======================
// 菜单状态
// ======================
const [menu, setMenu] = useState(false)
const [page, setPage] = useState(0)
const pages = useMemo(() => [
<ProductMenu key="product"/>,
<SolutionMenu key="solution"/>,
<HelpMenu key="help"/>,
<MobileMenu key="mobile"/>,
], [])
const toggleMobileMenu = () => {
if (menu) {
setMenu(false)
}
else {
setPage(3)
setMenu(true)
}
}
const enterMenu = (i: number) => {
return (e: PointerEvent) => {
setPage(i)
if (page !== i) {
setMenu(true)
}
else {
setMenu(!menu)
}
}
}
const leaveMenu = (e: PointerEvent) => {
setMenu(false)
}
const enterMenuContent = (e: PointerEvent) => {
setMenu(true)
}
const leaveMenuContent = (e: PointerEvent) => {
if (page != 3) {
setMenu(false)
}
}
const enterMenuMask = (e: PointerEvent) => {
if (e.pointerType !== 'mouse') {
setMenu(false)
}
}
// ======================
// 用户信息
// ======================
const profile = useProfileStore(store => store.profile)
// ======================
// render
// ======================
return (
<header
className={merge(
'fixed top-0 left-0 w-screen z-10 flex flex-col ',
menu ? 'h-screen' : 'pointer-events-none',
)}>
<HeaderContext.Provider value={{setMenu}}>
{/* 菜单栏 */}
<div className={merge(
`flex-none pointer-events-auto`,
`transition-[background,shadow] duration-200 ease-in-out`,
menu
? `bg-[#fffe] backdrop-blur-sm`
: scroll
? `bg-[#fffe] backdrop-blur-sm shadow-lg`
: `bg-transparent shadow-none`,
)}>
<Wrap className="h-20 max-md:h-16 flex justify-between">
<nav className="flex items-center justify-between lg:flex-row-reverse gap-4 lg:gap-8">
{/* 桌面端菜单 */}
<ul className="h-full items-stretch hidden lg:flex">
<LinkItem text="首页" href="/"/>
<MenuItem
text="产品订购"
active={menu && page === 0}
onPointerEnter={enterMenu(0)}
onPointerLeave={leaveMenu}
/>
<MenuItem
text="业务场景"
active={menu && page === 1}
onPointerEnter={enterMenu(1)}
onPointerLeave={leaveMenu}
/>
<MenuItem
text="帮助中心"
active={menu && page === 2}
onPointerEnter={enterMenu(2)}
onPointerLeave={leaveMenu}
/>
<LinkItem
text="业务定制"
href="/custom"/>
</ul>
{/* 移动端菜单 */}
<Button
theme="ghost"
className="lg:hidden size-10"
onClick={toggleMobileMenu}
>
<MenuIcon/>
</Button>
{/* logo */}
<Link href="/" className="flex items-center">
<Image src={logo} alt="logo" height={40} className="translate-y-0.5"/>
</Link>
</nav>
{/* 登录 */}
<Suspense>
<ProfileOrLogin/>
</Suspense>
</Wrap>
</div>
{/* 桌面端下拉菜单 */}
<div
className={merge(
`hidden lg:flex flex-auto overflow-auto lg:flex-none lg:basis-72 shadow-lg box-content`,
`bg-[#fffe] backdrop-blur-sm`,
`transition-[opacity,padding] transition-discrete duration-200 ease-in-out`,
menu
? `delay-[0s,0s] opacity-100 py-4`
: `delay-[0s,0s] opacity-0 py-0 pointer-events-none`,
)}
onPointerEnter={enterMenuContent}
onPointerLeave={leaveMenuContent}
>
{pages[page]}
</div>
{/* 移动端侧边栏菜单 */}
{menu && page === 3 && (
<div className="lg:hidden fixed inset-0 z-20 flex">
<div
className="flex-1 bg-black/40"
onPointerDown={enterMenuMask}
/>
<div className="w-72 max-w-[80vw] bg-white shadow-xl overflow-y-auto">
{pages[3]}
</div>
</div>
)}
{/* 遮罩层(桌面端) */}
<div className="flex-auto" onPointerEnter={enterMenuMask}/>
</HeaderContext.Provider>
</header>
)
}
function LinkItem(props: {
text: string
href: string
}) {
return (
<li className="group relative">
<Link
href={props.href}
className={[
`h-full px-4 flex items-center text-lg`,
`transition-colors duration-200 ease-in-out`,
`hover:text-blue-500`,
].join(' ')}
>
{props.text}
</Link>
<div className={[
`absolute bottom-0 w-full h-0.5 bg-transparent `,
`transition-colors duration-200`,
`group-hover:bg-blue-500`,
].join(' ')}>
</div>
</li>
)
}
function MenuItem(props: {
text: string
active: boolean
} & ComponentProps<'button'>) {
return (
<li className="group relative">
<button
onPointerEnter={props.onPointerEnter}
onPointerLeave={props.onPointerLeave}
className={[
`h-full px-4 flex gap-3 items-center cursor-pointer text-lg`,
`transition-colors duration-200 ease-in-out cursor-pointer`,
props.active
? `text-blue-500`
: ``,
].join(' ')}
>
<span>{props.text}</span>
<Image
src={down}
alt="drop_menu"
className={[
`transition-transform duration-200 ease-in-out`,
props.active
? `rotate-180`
: ``,
].join(' ')}
/>
</button>
<div
className={[
`absolute bottom-0 w-full h-0.5 pointer-events-none`,
`transition-colors duration-200`,
props.active
? `bg-blue-500`
: 'bg-transparent',
].join(' ')}/>
</li>
)
}
function ProfileOrLogin() {
const profile = use(useProfileStore(store => store.profile))
return (
<div className="flex items-center">
{profile == null
? (
<>
<Link
href="/login"
className="w-24 h-12 flex items-center justify-center lg:text-lg"
>
<span></span>
</Link>
<Link
href="/login"
className={[
`w-20 lg:w-24 h-10 lg:h-12 bg-linear-to-r rounded-sm flex items-center justify-center lg:text-lg text-white`,
`transition-colors duration-200 ease-in-out`,
`from-blue-500 to-cyan-400 hover:from-blue-500 hover:to-cyan-300`,
].join(' ')}
>
<span></span>
</Link>
</>
)
: (
<UserCenter profile={profile}/>
)
}
</div>
)
}