170 lines
4.5 KiB
TypeScript
170 lines
4.5 KiB
TypeScript
import { useRef, useState, useSyncExternalStore } from "react"
|
|
import { NavLink } from "react-router-dom"
|
|
import ProductDropdown from "@/components/productDropdown"
|
|
import ProfileOrLogin from "@/components/profileOrLogin"
|
|
import Wrap from "@/components/wrap"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
export default function Header() {
|
|
const scroll = useSyncExternalStore(
|
|
callback => {
|
|
window.addEventListener("scroll", callback)
|
|
return () => {
|
|
window.removeEventListener("scroll", callback)
|
|
}
|
|
},
|
|
() => window.scrollY > 48,
|
|
() => false,
|
|
)
|
|
|
|
// ======================
|
|
// 菜单状态
|
|
// ======================
|
|
const [openMenu, setOpenMenu] = useState(false)
|
|
|
|
const handleEnter = () => setOpenMenu(true)
|
|
const handleLeave = () => setOpenMenu(false)
|
|
|
|
return (
|
|
<header className={cn("fixed top-0 left-0 w-screen z-10 flex flex-col")}>
|
|
<div
|
|
className={cn(
|
|
`flex-none pointer-events-auto`,
|
|
`transition-[background,shadow] duration-200 ease-in-out`,
|
|
scroll
|
|
? `bg-[#fffe] backdrop-blur-sm shadow-lg`
|
|
: `bg-transparent shadow-none`,
|
|
)}
|
|
>
|
|
<Wrap className="h-20 max-md:h-16 flex justify-between pl-15 pr-15">
|
|
<nav className="flex items-center justify-between gap-4 lg:gap-8">
|
|
<ul className="h-full items-stretch hidden lg:flex">
|
|
<NavItem to="/">首页</NavItem>
|
|
<MenuItem
|
|
text="产品购买"
|
|
isOpen={openMenu}
|
|
onMouseEnter={handleEnter}
|
|
onMouseLeave={handleLeave}
|
|
>
|
|
<ProductDropdown onNavigate={() => setOpenMenu(false)} />
|
|
</MenuItem>
|
|
<NavItem to="/ipline">IP线路表</NavItem>
|
|
<NavItem to="/softdownload">软件下载</NavItem>
|
|
<NavItem to="/help">帮助/教程</NavItem>
|
|
</ul>
|
|
</nav>
|
|
<ProfileOrLogin />
|
|
</Wrap>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|
|
|
|
const NavItem = ({
|
|
to,
|
|
children,
|
|
}: {
|
|
to: string
|
|
children: React.ReactNode
|
|
}) => (
|
|
<NavLink
|
|
to={to}
|
|
className="group h-full flex items-center relative px-3 text-slate-700 hover:text-blue-500 transition-colors duration-200"
|
|
>
|
|
{children}
|
|
<span
|
|
className={cn(
|
|
"absolute bottom-0 left-0 right-0 h-0.5 transition-colors duration-200",
|
|
"bg-transparent group-hover:bg-blue-500",
|
|
)}
|
|
/>
|
|
</NavLink>
|
|
)
|
|
|
|
const MenuItem = ({
|
|
text,
|
|
children,
|
|
isOpen,
|
|
onMouseEnter,
|
|
onMouseLeave,
|
|
}: {
|
|
text: string
|
|
children: React.ReactNode
|
|
isOpen: boolean
|
|
onMouseEnter: () => void
|
|
onMouseLeave: () => void
|
|
}) => {
|
|
const timeoutRef = useRef<number | null>(null)
|
|
|
|
const handleMouseEnter = () => {
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current)
|
|
timeoutRef.current = null
|
|
}
|
|
onMouseEnter()
|
|
}
|
|
|
|
const handleMouseLeave = () => {
|
|
timeoutRef.current = setTimeout(() => {
|
|
onMouseLeave()
|
|
}, 150)
|
|
}
|
|
|
|
return (
|
|
<li
|
|
className="group relative h-full flex items-center"
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
<button
|
|
className={cn(
|
|
"h-full flex items-center gap-1 px-3 text-lg transition-colors duration-200",
|
|
isOpen ? "text-blue-500" : "text-slate-700 hover:text-blue-500",
|
|
)}
|
|
>
|
|
<span>{text}</span>
|
|
<svg
|
|
className={cn(
|
|
"w-4 h-4 transition-transform duration-200",
|
|
isOpen ? "rotate-180" : "",
|
|
)}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 9l-7 7-7-7"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
<span
|
|
className={cn(
|
|
"absolute bottom-0 left-0 right-0 h-0.5 transition-colors duration-200",
|
|
isOpen ? "bg-blue-500" : "bg-transparent group-hover:bg-blue-500",
|
|
)}
|
|
/>
|
|
|
|
<div
|
|
className={cn(
|
|
"absolute top-full left-0 pt-3 w-screen max-w-4xl transition-all duration-200",
|
|
isOpen
|
|
? "opacity-100 visible translate-y-0"
|
|
: "opacity-0 invisible -translate-y-2 pointer-events-none",
|
|
)}
|
|
style={{
|
|
left: "50%",
|
|
transform: `translateX(-7%)`,
|
|
}}
|
|
>
|
|
<div className="bg-white rounded-xl shadow-2xl border border-gray-100/80 p-6">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
)
|
|
}
|