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

View File

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

View File

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