2026-08-03 10:53:12 +08:00
|
|
|
|
import { useRef, useState, useSyncExternalStore } from "react"
|
|
|
|
|
|
import { NavLink, useNavigate } from "react-router-dom"
|
2026-07-24 18:00:22 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-03 10:53:12 +08:00
|
|
|
|
// ======================
|
|
|
|
|
|
// 菜单状态
|
|
|
|
|
|
// ======================
|
|
|
|
|
|
const [openMenu, setOpenMenu] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
const handleEnter = () => setOpenMenu(true)
|
|
|
|
|
|
const handleLeave = () => setOpenMenu(false)
|
|
|
|
|
|
|
2026-07-24 18:00:22 +08:00
|
|
|
|
return (
|
2026-08-03 10:53:12 +08:00
|
|
|
|
<header className={cn("fixed top-0 left-0 w-screen z-10 flex flex-col")}>
|
2026-07-24 18:00:22 +08:00
|
|
|
|
<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>
|
2026-08-03 10:53:12 +08:00
|
|
|
|
<MenuItem
|
|
|
|
|
|
text="产品购买"
|
|
|
|
|
|
isOpen={openMenu}
|
|
|
|
|
|
onMouseEnter={handleEnter}
|
|
|
|
|
|
onMouseLeave={handleLeave}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ProductDropdown onNavigate={() => setOpenMenu(false)} />
|
|
|
|
|
|
</MenuItem>
|
2026-07-24 18:00:22 +08:00
|
|
|
|
<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}
|
2026-08-03 10:53:12 +08:00
|
|
|
|
className="group h-full flex items-center relative px-3 text-slate-700 hover:text-blue-500 transition-colors duration-200"
|
2026-07-24 18:00:22 +08:00
|
|
|
|
>
|
|
|
|
|
|
{children}
|
2026-08-03 10:53:12 +08:00
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"absolute bottom-0 left-0 right-0 h-0.5 transition-colors duration-200",
|
|
|
|
|
|
"bg-transparent group-hover:bg-blue-500",
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
2026-07-24 18:00:22 +08:00
|
|
|
|
</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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-08-03 10:53:12 +08:00
|
|
|
|
|
|
|
|
|
|
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="海量全球住宅优质IP,4G/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>
|
|
|
|
|
|
)
|