Files
web/src/app/(home)/@header/_client/provider.tsx
2025-06-12 18:51:54 +08:00

232 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import {createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'
import Link from 'next/link'
import Image from 'next/image'
import {LinkItem, MenuItem} from './navs'
import SolutionMenu from './solution'
import ProductMenu from './product'
import HelpMenu from './help'
import Wrap from '@/components/wrap'
import logo from '@/assets/logo.webp'
import {Button} from '@/components/ui/button'
import {useProfileStore} from '@/components/providers/StoreProvider'
import UserCenter from '@/components/composites/user-center'
export const HeaderContext = createContext<{
setMenu: (value: boolean) => void
isMobile: boolean
} | null>(null)
export type ProviderProps = {}
export default function Provider(props: ProviderProps) {
// ======================
// 滚动条状态
// ======================
const [scroll, setScroll] = useState(false) // Changed to false for client-side rendering
const handleScroll = useCallback(() => {
setScroll(window.scrollY > 48)
}, [])
useEffect(() => {
// Initialize scroll state on client
setScroll(window.scrollY > 48)
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [handleScroll])
// ======================
// 菜单状态
// ======================
const [menu, setMenu] = useState(false)
const [page, setPage] = useState(0)
const menuRef = useRef<HTMLDivElement>(null)
// 屏幕1024时
const [isMobile, setIsMobile] = useState(false)
// 点击外部关闭菜单
useEffect(() => {
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
const target = e.target as HTMLElement
// 排除所有交互元素
if (!target.closest('a, button, [role="button"], [role="tab"]')) {
setMenu(false)
}
}
}
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('touchstart', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('touchstart', handleClickOutside)
}
}, [])
const handleMenuEnter = (pageIndex: number) => {
if (isMobile) {
// 移动端: 点击切换当前菜单
if (menu && page === pageIndex) {
// 添加关闭动画延迟
setTimeout(() => setMenu(false), 100)
}
else {
setPage(pageIndex)
// 先设置page再设置menu为true
setTimeout(() => setMenu(true), 0)
}
}
else {
// 桌面端: 悬停打开
setPage(pageIndex)
setMenu(true)
}
}
const handleMenuLeave = useCallback(() => {
if (!isMobile) {
// 桌面端: 添加延迟关闭以避免意外关闭
setMenu(false)
}
}, [isMobile])
const pages = useMemo(() => [
<ProductMenu key="product"/>,
<SolutionMenu key="solution"/>,
<HelpMenu key="help"/>,
], [])
// ======================
// 用户信息
// ======================
const profile = useProfileStore(store => store.profile)
// ======================
// render
// ======================
// 屏幕1024时响应式处理
useEffect(() => {
const checkMobile = () => {
const mobile = window.innerWidth <= 1024
setIsMobile(mobile)
// 移除自动关闭菜单的逻辑,让交互逻辑处理菜单状态
}
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
return (
<HeaderContext.Provider value={{setMenu, isMobile}}>
<div
ref={menuRef}
className={[
`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`,
].join(' ')}>
<Wrap className="h-20 max-md:h-16 flex justify-between">
<div className="flex justify-between gap-8">
{/* logo */}
<Link href="/" className="flex items-center">
<Image src={logo} alt="logo" height={40} className="translate-y-0.5"/>
</Link>
{/* 菜单 */}
<nav>
<ul className="h-full flex items-stretch max-lg:hidden">
<LinkItem text="首页" href="/"/>
<MenuItem
text="产品订购"
active={menu && page === 0}
onEnter={() => handleMenuEnter(0)}
onLeave={handleMenuLeave}
onTouchStart={() => handleMenuEnter(0)}
/>
<MenuItem
text="业务场景"
active={menu && page === 1}
onEnter={() => handleMenuEnter(1)}
onLeave={handleMenuLeave}
onTouchStart={() => handleMenuEnter(1)}
/>
<MenuItem
text="帮助中心"
active={menu && page === 2}
onEnter={() => handleMenuEnter(2)}
onLeave={handleMenuLeave}
onTouchStart={() => handleMenuEnter(2)}
/>
<LinkItem
text="企业服务"
href="#"/>
<LinkItem
text="推广返利"
href="#"/>
</ul>
</nav>
</div>
{/* 登录 */}
<div className="flex items-center">
{profile == undefined
? (
<>
<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-gradient-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>
</>
)
: (
// <Link href="/admin">
// <Button theme="gradient" className="h-12">
// 进入控制台
// </Button>
// </Link>
<UserCenter/>
)
}
</div>
</Wrap>
</div>
{/* 下拉菜单 */}
<div
className={[
`shadow-lg`,
`overflow-hidden bg-[#fffe] backdrop-blur-sm`,
`transition-[opacity,padding,height] transition-discrete duration-200 ease-in-out`,
menu
? `delay-[0s,0s,0s] opacity-100 py-8 h-auto`
: `delay-[0s,0s,0.2s] opacity-0 py-0 h-0`,
isMobile && `max-lg:transition-all max-lg:duration-300`,
].join(' ')}
onPointerEnter={() => !isMobile && setMenu(true)}
onPointerLeave={() => !isMobile && setMenu(false)}
>
{pages[page]}
</div>
</HeaderContext.Provider>
)
}