Files
juip-web/src/home/header.tsx
2026-08-03 10:53:12 +08:00

303 lines
8.5 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.
import { useRef, useState, useSyncExternalStore } from "react"
import { NavLink, useNavigate } from "react-router-dom"
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>
)
function ProfileOrLogin() {
return (
<div className="flex items-center">
<NavLink
to="/login"
state={{ tab: "login" }}
className="w-24 h-12 flex items-center justify-center lg:text-lg"
>
<span></span>
</NavLink>
<NavLink
to="/login"
state={{ tab: "register" }}
className={[
`w-20 lg:w-24 h-10 lg:h-12 bg-linear-to-r rounded-full 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>
</NavLink>
</div>
)
}
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>
)
}
const ProductDropdown = ({ onNavigate }: { onNavigate: () => void }) => {
const navigate = useNavigate()
const handleClick = (path: string) => {
navigate(path)
onNavigate()
}
return (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<ProductCard
title="动静态IP"
desc="千万级动态短效IP自动去重高并发采集"
tag="热门"
onClick={() => handleClick("/product/short")}
/>
<ProductCard
title="新HTTP/S5"
desc="IP可用时长从数小时到365天全覆盖"
tag="稳定"
onClick={() => handleClick("/product/long")}
/>
<ProductCard
title="HTTP/S5"
desc="海量全球住宅优质IP4G/5G真实手机IP"
tag="全球"
onClick={() => handleClick("/product/global")}
/>
<ProductCard
title="软路由成本价"
desc="云端自动切换IP更易于调用与开发"
onClick={() => handleClick("/product/tunnel")}
/>
<ProductCard
title="动态Vps"
desc="IP独享高带宽灵活控制存活周期"
onClick={() => handleClick("/product/exclusive")}
/>
<ProductCard
title="静态独享IP"
desc="纯净IP更符合跨境卖家需求的云主机"
onClick={() => handleClick("/product/static")}
/>
</div>
<div className="flex items-center flex-wrap gap-3 pt-3 border-t border-gray-100">
<span className="text-xs font-medium text-gray-400">:</span>
<button
onClick={() => handleClick("/product/short")}
className="text-sm text-blue-600 hover:underline whitespace-nowrap"
>
</button>
<button
onClick={() => handleClick("/product/global")}
className="text-sm text-blue-600 hover:underline whitespace-nowrap"
>
</button>
<button
onClick={() => handleClick("/product/tunnel")}
className="text-sm text-blue-600 hover:underline whitespace-nowrap"
>
</button>
<span className="text-xs text-gray-300">|</span>
<button
onClick={() => handleClick("/custom")}
className="text-sm text-orange-500 hover:underline font-medium whitespace-nowrap"
>
</button>
</div>
</div>
)
}
const ProductCard = ({
title,
desc,
tag,
onClick,
}: {
title: string
desc: string
tag?: string
onClick: () => void
}) => (
<button
onClick={onClick}
className="group flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 transition-all cursor-pointer border border-transparent hover:border-blue-200 text-left w-full"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-gray-800 group-hover:text-blue-600 transition-colors text-sm whitespace-nowrap">
{title}
</span>
{tag && (
<span className="text-[10px] bg-blue-100 text-blue-600 px-2 py-0.5 rounded-full shrink-0">
{tag}
</span>
)}
</div>
<p className="text-xs text-gray-500 mt-1 leading-relaxed line-clamp-2">
{desc}
</p>
</div>
</button>
)