2025-06-18 17:57:12 +08:00
|
|
|
'use client'
|
2025-12-18 17:10:20 +08:00
|
|
|
import {useMemo, useState, PointerEvent, ComponentProps, useSyncExternalStore, use, Suspense, MouseEvent} from 'react'
|
2025-06-22 10:58:41 +08:00
|
|
|
import Link from 'next/link'
|
2025-06-18 17:57:12 +08:00
|
|
|
import Image from 'next/image'
|
2025-11-18 19:16:24 +08:00
|
|
|
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'
|
2025-06-22 10:58:41 +08:00
|
|
|
import Wrap from '@/components/wrap'
|
2025-06-18 17:57:12 +08:00
|
|
|
import logo from '@/assets/logo.webp'
|
2025-06-22 10:58:41 +08:00
|
|
|
import {Button} from '@/components/ui/button'
|
2025-12-11 14:10:52 +08:00
|
|
|
import {useProfileStore} from '@/components/stores/profile'
|
2025-06-22 10:58:41 +08:00
|
|
|
import UserCenter from '@/components/composites/user-center'
|
2025-06-18 17:57:12 +08:00
|
|
|
import {MenuIcon} from 'lucide-react'
|
2025-06-22 10:58:41 +08:00
|
|
|
import down from '@/assets/header/down.svg'
|
|
|
|
|
import {merge} from '@/lib/utils'
|
2025-03-24 11:45:54 +08:00
|
|
|
|
2025-11-18 19:16:24 +08:00
|
|
|
export type HeaderProps = {}
|
2025-03-18 18:00:29 +08:00
|
|
|
|
2025-11-18 19:16:24 +08:00
|
|
|
export default function Header(props: HeaderProps) {
|
2025-06-18 17:57:12 +08:00
|
|
|
// ======================
|
2025-06-22 10:58:41 +08:00
|
|
|
// 滚动条状态
|
2025-06-18 17:57:12 +08:00
|
|
|
// ======================
|
|
|
|
|
|
2025-11-20 12:10:16 +08:00
|
|
|
const scroll = useSyncExternalStore((callback) => {
|
|
|
|
|
window.addEventListener('scroll', callback)
|
2025-06-18 17:57:12 +08:00
|
|
|
return () => {
|
2025-11-20 12:10:16 +08:00
|
|
|
window.removeEventListener('scroll', callback)
|
2025-06-18 17:57:12 +08:00
|
|
|
}
|
2025-11-20 12:10:16 +08:00
|
|
|
}, () => window.scrollY > 48, () => false)
|
2025-06-18 17:57:12 +08:00
|
|
|
|
|
|
|
|
// ======================
|
2025-06-22 10:58:41 +08:00
|
|
|
// 菜单状态
|
2025-06-18 17:57:12 +08:00
|
|
|
// ======================
|
|
|
|
|
|
2025-06-22 10:58:41 +08:00
|
|
|
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"/>,
|
|
|
|
|
], [])
|
|
|
|
|
|
2025-12-18 17:10:20 +08:00
|
|
|
const toggleMobileMenu = () => {
|
|
|
|
|
if (menu) {
|
|
|
|
|
setMenu(false)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
setPage(3)
|
|
|
|
|
setMenu(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-22 10:58:41 +08:00
|
|
|
const enterMenu = (i: number) => {
|
|
|
|
|
return (e: PointerEvent) => {
|
|
|
|
|
setPage(i)
|
2025-12-18 17:10:20 +08:00
|
|
|
if (page !== i) {
|
2025-06-22 10:58:41 +08:00
|
|
|
setMenu(true)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
setMenu(!menu)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 17:10:20 +08:00
|
|
|
const leaveMenu = (e: PointerEvent) => {
|
|
|
|
|
setMenu(false)
|
2025-06-22 10:58:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const enterMenuContent = (e: PointerEvent) => {
|
|
|
|
|
setMenu(true)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 17:10:20 +08:00
|
|
|
const leaveMenuContent = (e: PointerEvent) => {
|
|
|
|
|
if (page != 3) {
|
2025-06-22 10:58:41 +08:00
|
|
|
setMenu(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const enterMenuMask = (e: PointerEvent) => {
|
|
|
|
|
if (e.pointerType !== 'mouse') {
|
|
|
|
|
setMenu(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ======================
|
|
|
|
|
// 用户信息
|
|
|
|
|
// ======================
|
|
|
|
|
|
|
|
|
|
const profile = useProfileStore(store => store.profile)
|
2025-06-18 17:57:12 +08:00
|
|
|
|
|
|
|
|
// ======================
|
|
|
|
|
// render
|
|
|
|
|
// ======================
|
|
|
|
|
|
2025-03-18 18:00:29 +08:00
|
|
|
return (
|
2025-06-18 17:57:12 +08:00
|
|
|
<header
|
2025-06-22 10:58:41 +08:00
|
|
|
className={merge(
|
2025-06-25 14:43:44 +08:00
|
|
|
'fixed top-0 left-0 w-screen z-10 flex flex-col ',
|
|
|
|
|
menu ? 'h-screen' : 'pointer-events-none',
|
2025-06-22 10:58:41 +08:00
|
|
|
)}>
|
|
|
|
|
<HeaderContext.Provider value={{setMenu}}>
|
|
|
|
|
|
|
|
|
|
{/* 菜单栏 */}
|
2025-06-18 17:57:12 +08:00
|
|
|
<div className={merge(
|
2025-06-25 14:17:07 +08:00
|
|
|
`flex-none pointer-events-auto`,
|
2025-06-22 10:58:41 +08:00
|
|
|
`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`,
|
2025-06-18 17:57:12 +08:00
|
|
|
)}>
|
2025-06-22 10:58:41 +08:00
|
|
|
<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)}
|
2025-12-18 17:10:20 +08:00
|
|
|
onPointerLeave={leaveMenu}
|
2025-06-22 10:58:41 +08:00
|
|
|
/>
|
|
|
|
|
<MenuItem
|
|
|
|
|
text="业务场景"
|
|
|
|
|
active={menu && page === 1}
|
|
|
|
|
onPointerEnter={enterMenu(1)}
|
2025-12-18 17:10:20 +08:00
|
|
|
onPointerLeave={leaveMenu}
|
2025-06-22 10:58:41 +08:00
|
|
|
/>
|
|
|
|
|
<MenuItem
|
|
|
|
|
text="帮助中心"
|
|
|
|
|
active={menu && page === 2}
|
|
|
|
|
onPointerEnter={enterMenu(2)}
|
2025-12-18 17:10:20 +08:00
|
|
|
onPointerLeave={leaveMenu}
|
2025-06-22 10:58:41 +08:00
|
|
|
/>
|
|
|
|
|
<LinkItem
|
2025-12-09 18:01:12 +08:00
|
|
|
text="业务定制"
|
2025-12-18 11:52:33 +08:00
|
|
|
href="/custom"/>
|
2025-06-22 10:58:41 +08:00
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
{/* 移动端菜单 */}
|
|
|
|
|
<Button
|
|
|
|
|
theme="ghost"
|
|
|
|
|
className="lg:hidden size-10"
|
2025-12-18 17:10:20 +08:00
|
|
|
onClick={toggleMobileMenu}
|
2025-06-22 10:58:41 +08:00
|
|
|
>
|
|
|
|
|
<MenuIcon/>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* logo */}
|
|
|
|
|
<Link href="/" className="flex items-center">
|
|
|
|
|
<Image src={logo} alt="logo" height={40} className="translate-y-0.5"/>
|
|
|
|
|
</Link>
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
{/* 登录 */}
|
2025-12-11 14:10:52 +08:00
|
|
|
<Suspense>
|
|
|
|
|
<ProfileOrLogin/>
|
|
|
|
|
</Suspense>
|
2025-06-18 17:57:12 +08:00
|
|
|
</Wrap>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-14 15:25:23 +08:00
|
|
|
{/* 桌面端下拉菜单 */}
|
2025-06-22 10:58:41 +08:00
|
|
|
<div
|
|
|
|
|
className={merge(
|
2026-03-14 15:25:23 +08:00
|
|
|
`hidden lg:flex flex-auto overflow-auto lg:flex-none lg:basis-72 shadow-lg box-content`,
|
2025-06-22 10:58:41 +08:00
|
|
|
`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}
|
2025-12-18 17:10:20 +08:00
|
|
|
onPointerLeave={leaveMenuContent}
|
2025-06-22 10:58:41 +08:00
|
|
|
>
|
|
|
|
|
{pages[page]}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-14 15:25:23 +08:00
|
|
|
{/* 移动端侧边栏菜单 */}
|
|
|
|
|
{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>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 遮罩层(桌面端) */}
|
2025-06-22 10:58:41 +08:00
|
|
|
<div className="flex-auto" onPointerEnter={enterMenuMask}/>
|
|
|
|
|
|
|
|
|
|
</HeaderContext.Provider>
|
2025-03-28 15:00:46 +08:00
|
|
|
</header>
|
2025-03-18 18:00:29 +08:00
|
|
|
)
|
|
|
|
|
}
|
2025-06-18 17:57:12 +08:00
|
|
|
|
2025-06-22 10:58:41 +08:00
|
|
|
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>
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-06-18 17:57:12 +08:00
|
|
|
|
2025-06-22 10:58:41 +08:00
|
|
|
function MenuItem(props: {
|
|
|
|
|
text: string
|
|
|
|
|
active: boolean
|
|
|
|
|
} & ComponentProps<'button'>) {
|
2025-06-18 17:57:12 +08:00
|
|
|
return (
|
2025-06-22 10:58:41 +08:00
|
|
|
<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`,
|
2026-03-12 15:46:30 +08:00
|
|
|
`transition-colors duration-200 ease-in-out cursor-pointer`,
|
2025-06-22 10:58:41 +08:00
|
|
|
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>
|
2025-06-18 17:57:12 +08:00
|
|
|
)
|
|
|
|
|
}
|
2025-12-11 14:10:52 +08:00
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
)
|
|
|
|
|
}
|