2 Commits

Author SHA1 Message Date
Eamon-meng
d9f267e257 调整桌面端产品订购的菜单栏布局 2026-03-14 18:00:27 +08:00
Eamon-meng
83530d7f1e 修改移动端菜单栏侧边栏布局 2026-03-14 15:25:23 +08:00
4 changed files with 349 additions and 62 deletions

View File

@@ -1,31 +1,296 @@
import ProductMenu from './menu-product'
import HelpMenu from './menu-help'
import SolutionMenu from './menu-solution'
'use client'
import {useContext, useState} from 'react'
import {useRouter} from 'next/navigation'
import {X} from 'lucide-react'
import {HeaderContext} from './common'
import Image, {StaticImageData} from 'next/image'
import prod from '@/assets/header/product/prod.svg'
import custom from '@/assets/header/product/custom.svg'
import s01 from '@/assets/header/solution/01.svg'
import s02 from '@/assets/header/solution/02.svg'
import s03 from '@/assets/header/solution/03.svg'
import s04 from '@/assets/header/solution/04.svg'
import s05 from '@/assets/header/solution/05.svg'
import s06 from '@/assets/header/solution/06.svg'
import s07 from '@/assets/header/solution/07.svg'
import s08 from '@/assets/header/solution/08.svg'
import h01 from '@/assets/header/help/01.svg'
import h02 from '@/assets/header/help/02.svg'
import h03 from '@/assets/header/help/03.svg'
import {merge} from '@/lib/utils'
import Link from 'next/link'
import logo from '@/assets/logo.webp'
export type MobileMenuProps = {}
export default function MobileMenu(props: MobileMenuProps) {
const ctx = useContext(HeaderContext)
const router = useRouter()
const [productTab, setProductTab] = useState<'domestic' | 'oversea'>('domestic')
if (!ctx) {
throw new Error(`HeaderContext not found`)
}
const navigate = (href: string) => {
ctx.setMenu(false)
router.push(href)
}
return (
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-4">
<ProductMenu/>
<div className="h-full flex flex-col bg-white">
<div className="flex items-center justify-between px-4 h-16 border-b border-gray-100">
{/* logo */}
<Link href="/" className="flex items-center">
<Image src={logo} alt="logo" height={40} className="translate-y-0.5"/>
</Link>
<button
type="button"
className="rounded-md p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors"
onClick={() => ctx.setMenu(false)}
aria-label="关闭菜单"
>
<X className="h-5 w-5"/>
</button>
</div>
<div className="flex flex-col gap-4">
<MenuTitle title="帮助中心"/>
<HelpMenu/>
</div>
<div className="flex flex-col gap-4">
<MenuTitle title="业务场景"/>
<SolutionMenu/>
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-8">
<div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-500 tracking-wide">
</h3>
<div className="flex rounded-lg bg-gray-100">
<button
className={merge(
'flex-1 py-2.5 text-sm font-medium rounded-md transition-all',
productTab === 'domestic'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900',
)}
onClick={() => setProductTab('domestic')}
>
</button>
<button
className={merge(
'flex-1 py-2.5 text-sm font-medium rounded-md transition-all',
productTab === 'oversea'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900',
)}
onClick={() => setProductTab('oversea')}
>
</button>
</div>
{productTab === 'domestic' && (
<div className="space-y-2">
<ProductItem
icon={prod}
label="短效动态IP"
badge="最低4.5折"
href="/product?type=short"
onNavigate={navigate}
/>
<ProductItem
icon={prod}
label="长效静态IP"
badge="最低4.5折"
href="/product?type=long"
onNavigate={navigate}
/>
<ProductItem
icon={custom}
label="优质/企业/精选IP"
badge="专属定制"
href="/custom"
onNavigate={navigate}
/>
</div>
)}
{productTab === 'oversea' && (
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-600 text-center">
线
</p>
</div>
)}
</div>
<MenuSection title="业务场景">
<div className="grid grid-cols-2 gap-3">
<SolutionItem
icon={s01}
label="数据采集"
href="/data-capture"
onNavigate={navigate}
/>
<SolutionItem
icon={s02}
label="电商运营"
href="/e-commerce"
onNavigate={navigate}
/>
<SolutionItem
icon={s03}
label="市场调研"
href="/market-research"
onNavigate={navigate}
/>
<SolutionItem
icon={s04}
label="SEO优化"
href="/seo-optimization"
onNavigate={navigate}
/>
<SolutionItem
icon={s05}
label="社交媒体"
href="/social-media"
onNavigate={navigate}
/>
<SolutionItem
icon={s06}
label="广告投放"
href="/advertising"
onNavigate={navigate}
/>
<SolutionItem
icon={s07}
label="账号管理"
href="/account-management"
onNavigate={navigate}
/>
<SolutionItem
icon={s08}
label="网络测试"
href="/network-testing"
onNavigate={navigate}
/>
</div>
</MenuSection>
<MenuSection title="帮助中心">
<div className="space-y-2">
<HelpItem
icon={h01}
label="短效IP提取"
onClick={() => navigate('/collect?type=short')}
/>
<HelpItem
icon={h02}
label="操作指南"
onClick={() => navigate('/docs/profile-settings')}
/>
<HelpItem
icon={h03}
label="平台教程"
onClick={() => navigate('/docs/ios-proxy')}
/>
</div>
</MenuSection>
<div className="space-y-2 pt-2">
<OtherLink
label="业务定制"
href="/custom"
onNavigate={navigate}
/>
</div>
</div>
</div>
)
}
function MenuTitle(props: {title: string}) {
function MenuSection(props: {title: string, children: React.ReactNode}) {
return (
<h3 className="text-xl text-weak px-4">
{props.title}
</h3>
<div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-500 tracking-wide">
{props.title}
</h3>
{props.children}
</div>
)
}
function ProductItem(props: {
icon: StaticImageData
label: string
badge?: string
href: string
onNavigate: (href: string) => void
}) {
return (
<button
type="button"
className="w-full flex items-center gap-3 rounded-lg border border-gray-100 bg-white px-4 py-3 text-left transition-all hover:border-blue-200 hover:shadow-sm"
onClick={() => props.onNavigate(props.href)}
>
<div className="shrink-0 w-8 h-8 bg-linear-to-br from-blue-50 to-cyan-50 rounded-lg flex items-center justify-center">
<Image src={props.icon} alt="" width={20} height={20} className="opacity-80"/>
</div>
<span className="flex-1 font-medium text-sm text-gray-900">{props.label}</span>
{props.badge && (
<span className="text-xs text-orange-600 bg-orange-50 px-2 py-1 rounded-full">
{props.badge}
</span>
)}
</button>
)
}
function SolutionItem(props: {
icon: StaticImageData
label: string
href: string
onNavigate: (href: string) => void
}) {
return (
<button
type="button"
className="flex flex-col items-center gap-2 p-3 rounded-lg border border-gray-100 hover:border-blue-200 hover:bg-blue-50/50 transition-all"
onClick={() => props.onNavigate(props.href)}
>
<div className="w-10 h-10 bg-linear-to-br from-blue-50 to-cyan-50 rounded-full flex items-center justify-center">
<Image src={props.icon} alt="" width={20} height={20} className="opacity-80"/>
</div>
<span className="text-xs font-medium text-gray-700">{props.label}</span>
</button>
)
}
function HelpItem(props: {
icon: StaticImageData
label: string
onClick: () => void
}) {
return (
<button
type="button"
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-gray-50 transition-colors"
onClick={props.onClick}
>
<Image src={props.icon} alt="" width={20} height={20} className="opacity-70"/>
<span className="text-sm text-gray-700">{props.label}</span>
</button>
)
}
function OtherLink(props: {
label: string
href: string
onNavigate: (href: string) => void
}) {
return (
<button
type="button"
className="w-full flex items-center px-3 py-2.5 text-sm text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
onClick={() => props.onNavigate(props.href)}
>
{props.label}
</button>
)
}

View File

@@ -1,12 +1,13 @@
'use client'
import {ReactNode, useContext, useState} from 'react'
import Wrap from '@/components/wrap'
import Image, {StaticImageData} from 'next/image'
import Link from 'next/link'
import {merge} from '@/lib/utils'
import prod from '@/assets/header/product/prod.svg'
import custom from '@/assets/header/product/custom.svg'
import {useRouter} from 'next/navigation'
import {FragmentTitle, HeaderContext} from './common'
import {HeaderContext} from './common'
type TabType = 'domestic' | 'oversea'
@@ -53,32 +54,33 @@ export function Tab(props: {
export function Domestic(props: {}) {
return (
<section role="tabpanel" className="flex-auto flex flex-col lg:flex-row justify-evenly gap-3 lg:gap-0">
<div className="w-full lg:w-64 flex flex-col">
<FragmentTitle img={prod} text="短效 IP"/>
<DomesticLink
label="短效动态 IP"
desc="全国 300+ 城市级定位节点IP 池资源充足自动高频切换。适用于数据采集、市场调研、SEO 优化等高并发场景。稳定可靠,响应迅速,助力业务高效运转。"
href="/product?type=short"
discount={45}
/>
</div>
<div className="w-full lg:w-64 flex flex-col">
<FragmentTitle img={prod} text="长效 IP"/>
<DomesticLink
label="长效动态 IP"
desc="IP 存活时长可达数小时至数天,连接稳定不掉线。适用于账号养号、社交运营、电商管理等需要持续在线的场景。优质线路保障,为您的长期业务保驾护航。"
href="/product?type=long"
discount={45}
/>
</div>
<div className="w-full lg:w-64 flex flex-col">
<FragmentTitle img={custom} text="业务定制"/>
<DomesticLink
label="优质/企业/精选IP"
desc="超 1000 家企业共同信赖之选!大客户经理全程 1 对 1 沟通,随时为您排忧解难,提供 24 小时不间断支持"
href="/custom"
/>
<section role="tabpanel" className="flex-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div className="grid grid-cols-1 gap-3">
<ProductCard
icon={prod}
label="短效动态 IP"
discount="最低4.5折"
desc="全国 300+ 城市级定位节点IP 池资源充足自动高频切换。适用于数据采集、市场调研、SEO 优化等高并发场景。稳定可靠,响应迅速,助力业务高效运转。"
href="/product?type=short"
/>
<ProductCard
icon={prod}
label="长效静态 IP"
discount="最低4.5折"
desc="IP 存活时长可达数小时至数天,连接稳定不掉线。适用于账号养号、社交运营、电商管理等需要持续在线的场景。优质线路保障,为您的长期业务保驾护航。"
href="/product?type=long"
/>
</div>
<div className="flex flex-col gap-3">
<ProductCard
icon={custom}
label="业务定制"
discount="1V1 专属服务"
desc="超 1000 家企业共同信赖之选!大客户经理全程 1 对 1 沟通,随时为您排忧解难,提供 24 小时不间断支持"
href="/custom"
/>
</div>
</div>
</section>
)
@@ -92,11 +94,12 @@ export function Oversea(props: {}) {
)
}
export function DomesticLink(props: {
export function ProductCard(props: {
icon: StaticImageData
label: string
discount: string
desc: string
href: string
discount?: number
}) {
const router = useRouter()
const ctx = useContext(HeaderContext)
@@ -116,18 +119,24 @@ export function DomesticLink(props: {
`transition-colors duration-150 ease-in-out`,
`p-4 rounded-lg flex flex-col gap-1 hover:bg-blue-50`,
)}
onClick={onClick}>
<p className="flex gap-2">
<span>{props.label}</span>
{props.discount && (
<span className="text-orange-500 text-xs text-light px-2 py-1 bg-orange-50 rounded-full">
{props.discount}%
</span>
)}
</p>
<p className="text-gray-400 text-sm">
{props.desc}
</p>
onClick={onClick}
>
<div className="flex items-start gap-3">
<div className="flex-none">
<Image src={props.icon} alt="" width={30} height={30}/>
</div>
<div className="flex-1">
<div className="flex items-center justify-between gap-3">
<span className="font-bold">{props.label}</span>
<span className="text-xs font-medium text-orange-600 bg-orange-50 px-2 py-1 rounded-full">
{props.discount}
</span>
</div>
<div className="mt-2 text-sm text-gray-400 space-y-1">
{props.desc}
</div>
</div>
</div>
</Link>
)
}

View File

@@ -164,10 +164,10 @@ export default function Header(props: HeaderProps) {
</Wrap>
</div>
{/* 下拉菜单 */}
{/* 桌面端下拉菜单 */}
<div
className={merge(
`flex-auto overflow-auto lg:flex-none lg:basis-72 shadow-lg box-content`,
`hidden lg:flex flex-auto overflow-auto lg:flex-none lg:basis-72 shadow-lg box-content`,
`bg-[#fffe] backdrop-blur-sm`,
`transition-[opacity,padding] transition-discrete duration-200 ease-in-out`,
menu
@@ -180,7 +180,20 @@ export default function Header(props: HeaderProps) {
{pages[page]}
</div>
{/* 遮罩层 */}
{/* 移动端侧边栏菜单 */}
{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>
)}
{/* 遮罩层(桌面端) */}
<div className="flex-auto" onPointerEnter={enterMenuMask}/>
</HeaderContext.Provider>

View File

@@ -176,7 +176,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
<canvas ref={canvas} width={256} height={256}/>
<p className="text-sm text-gray-600"></p>
<Button onClick={() => handleDialogOpenChange(false)}>
</Button>
</div>
)}