实现响应式导航栏组件

This commit is contained in:
2025-06-18 17:57:12 +08:00
parent ba7d22168d
commit 39f30fcfa9
29 changed files with 742 additions and 223 deletions

View File

@@ -8,7 +8,7 @@ import banner from '@/assets/header/help/banner.webp'
export default function HelpMenu() {
return (
<Wrap className="w-full grid grid-cols-4 gap-4 justify-items-center h-75">
<Wrap className="w-full grid grid-cols-3 lg:grid-cols-4 gap-4 justify-items-start">
<Column
icon={h01}
title="提取IP"
@@ -35,7 +35,7 @@ export default function HelpMenu() {
{lead: '行业资讯', href: '#'},
]}
/>
<Image src={banner} alt="banner" className=""/>
<Image src={banner} alt="banner" className="hidden lg:block"/>
</Wrap>
)
}

View File

@@ -7,7 +7,7 @@ export function LinkItem(props: {
href: string
}) {
return (
<li className="group relative">
<li className="group relative flex-none">
<Link
href={props.href}
className={[
@@ -36,7 +36,7 @@ export function MenuItem(props: {
onTouchStart?: (e: React.TouchEvent) => void
}) {
return (
<li className="group relative">
<li className="group relative flex-none">
<button
onPointerEnter={props.onEnter}
onPointerLeave={props.onLeave}

View File

@@ -1,5 +1,5 @@
'use client'
import {ReactNode, useContext, useState, useEffect} from 'react'
import {ReactNode, useState, useEffect, MouseEvent} from 'react'
import Wrap from '@/components/wrap'
import Image from 'next/image'
import anno from '@/assets/header/product/anno.svg'
@@ -7,7 +7,6 @@ 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 {HeaderContext} from '@/app/(home)/@header/_client/provider'
import {useSearchParams} from 'next/navigation'
import {useRouter} from 'next/navigation'
@@ -18,8 +17,6 @@ export function Tab(props: {
onSelect: () => void
children: ReactNode
}) {
const searchParams = useSearchParams()
const currentType = searchParams?.get('type') || 'short'
return (
<li role="tab">
<button
@@ -35,6 +32,53 @@ export function Tab(props: {
)
}
export default function ProductMenu() {
const [type, setType] = useState<TabType>('domestic')
useEffect(() => {
const checkMobile = () => {
}
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
return (
<Wrap className="flex">
<ul role="tablist" className="w-48">
<Tab selected={type === 'domestic'} onSelect={() => setType('domestic')}></Tab>
<Tab selected={type === 'oversea'} onSelect={() => setType('oversea')}></Tab>
</ul>
<div className="flex-1">
{type === 'domestic'
? (
<Domestic/>
) : (
<Oversea/>
)
}
</div>
<aside className="hidden w-64 lg:block">
<h3 className="flex gap-3 items-center mb-4">
<Image src={anno} alt="公告" className="w-10 h-10"/>
<span></span>
</h3>
<div className="flex flex-col gap-2">
<p>线</p>
<p className="text-gray-400 text-sm">
1.使使
使
</p>
<p className="text-gray-400 text-sm">
2.使使
使
</p>
</div>
</aside>
</Wrap>
)
}
export function Domestic(props: {}) {
const searchParams = useSearchParams()
const currentType = searchParams?.get('type') || 'short'
@@ -86,69 +130,6 @@ export function Domestic(props: {}) {
)
}
export function Oversea(props: {}) {
return (
<section role="tabpanel">
</section>
)
}
export default function ProductMenu() {
const [type, setType] = useState<TabType>('domestic')
const searchParams = useSearchParams()
useEffect(() => {
const urlType = searchParams?.get('type')
console.log('URL参数:', urlType)
}, [searchParams])
// 响应式布局
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth <= 1024)
}
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
return (
<Wrap className="flex h-75">
<ul role="tablist" className="w-48">
<Tab selected={type === 'domestic'} onSelect={() => setType('domestic')}></Tab>
<Tab selected={type === 'oversea'} onSelect={() => setType('oversea')}></Tab>
</ul>
<div className="flex-1">
{type === 'domestic'
? (
<Domestic/>
) : (
<Oversea/>
)
}
</div>
<aside className="w-64">
<h3 className="flex gap-3 items-center mb-4">
<Image src={anno} alt="公告" className="w-10 h-10"/>
<span></span>
</h3>
<div className="flex flex-col gap-2">
<p>线</p>
<p className="text-gray-400 text-sm">
1.使使
使
</p>
<p className="text-gray-400 text-sm">
2.使使
使
</p>
</div>
</aside>
</Wrap>
)
}
export function DomesticLink(props: {
label: string
desc: string
@@ -156,22 +137,17 @@ export function DomesticLink(props: {
discount: number
active?: boolean
}) {
const ctx = useContext(HeaderContext)
const router = useRouter()
if (!ctx) {
throw new Error(`HeaderContext not found`)
}
// const onClick = () => {
// ctx.setMenu(false)
// const ctx = useContext(HeaderContext)
// if (!ctx) {
// throw new Error(`HeaderContext not found`)
// }
const onClick = (e: React.MouseEvent) => {
const onClick = (e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
ctx.setMenu(false)
setTimeout(() => {
router.push(props.href)
}, ctx.isMobile ? 100 : 0)
// ctx.setMenu(false)
router.push(props.href)
}
return (
@@ -197,3 +173,11 @@ export function DomesticLink(props: {
</Link>
)
}
export function Oversea(props: {}) {
return (
<section role="tabpanel">
</section>
)
}

View File

@@ -9,7 +9,7 @@ import HelpMenu from './help'
import Wrap from '@/components/wrap'
import logo from '@/assets/logo.webp'
import {Button} from '@/components/ui/button'
import {useProfileStore} from '@/components/providers/StoreProvider'
import {useProfileStore} from '@/app/stores'
import UserCenter from '@/components/composites/user-center'
import {MenuIcon} from 'lucide-react'
import {merge} from '@/lib/utils'
@@ -26,7 +26,6 @@ export default function Provider(props: ProviderProps) {
// 滚动条状态
// ======================
const [scroll, setScroll] = useState(false) // Changed to false for client-side rendering
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) // 控制移动端菜单展开/收起
const handleScroll = useCallback(() => {
setScroll(window.scrollY > 48)
}, [])
@@ -47,12 +46,8 @@ export default function Provider(props: ProviderProps) {
const [menu, setMenu] = useState(false)
const [page, setPage] = useState(0)
const menuRef = useRef<HTMLDivElement>(null)
// 屏幕1024时
const [isMobile, setIsMobile] = useState(false)
// 控制产品订购下拉菜单的展开/收起
const [productDropdownOpen, setProductDropdownOpen] = useState(false)
// 点击外部关闭菜单
useEffect(() => {
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
@@ -72,8 +67,13 @@ export default function Provider(props: ProviderProps) {
}
}, [])
const isMobile = () => {
if (typeof window === 'undefined') return false
return window.innerWidth <= 768
}
const handleMenuEnter = (pageIndex: number) => {
if (isMobile && menu && page === pageIndex) {
if (isMobile() && menu && page === pageIndex) {
setMenu(false)
}
else {
@@ -82,17 +82,22 @@ export default function Provider(props: ProviderProps) {
}
}
const handleMenuLeave = useCallback(() => {
if (!isMobile) {
const handleMenuLeave = () => {
if (!isMobile()) {
setMenu(false)
}
}, [isMobile])
}
const pages = useMemo(() => [
<ProductMenu key="product"/>,
<SolutionMenu key="solution"/>,
<HelpMenu key="help"/>,
], [])
const toggleMobileMenu = () => {
setMenu(!menu)
}
// ======================
// 用户信息
// ======================
@@ -103,43 +108,27 @@ export default function Provider(props: ProviderProps) {
// render
// ======================
// 屏幕1024时响应式处理
useEffect(() => {
const checkMobile = () => {
const mobile = window.innerWidth <= 1024
setIsMobile(mobile)
// 移除自动关闭菜单的逻辑,让交互逻辑处理菜单状态
}
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
// 切换移动菜单
const toggleMobileMenu = () => {
setMobileMenuOpen(!mobileMenuOpen)
}
return (
<HeaderContext.Provider value={{setMenu, isMobile}}>
<HeaderContext.Provider value={{setMenu, isMobile: isMobile()}}>
<div
ref={menuRef}
className={[
`transition-[background, shadow] duration-200 ease-in-out`,
menu || mobileMenuOpen || productDropdownOpen
menu
? `bg-[#fffe] backdrop-blur-sm`
: scroll
? `bg-[#fffe] backdrop-blur-sm shadow-lg`
: `bg-transparent shadow-none`,
].join(' ')}>
<Wrap className="h-20 flex justify-between items-center">
<div className="flex justify-between items-center gap-2 lg:gap-8 h-9 lg:h-auto max-lg:flex-row-reverse lg:items-stretch">
<Wrap className="h-14 lg:h-16 flex justify-between items-stretch">
<div className="flex justify-between items-stretch gap-2 lg:gap-8 h-9 lg:h-auto max-lg:flex-row-reverse lg:items-stretch">
{/* logo */}
<Link href="/" className="flex items-center">
<Image src={logo} alt="logo" height={40} className="translate-y-0.5"/>
<Link href="/" className="self-center">
<Image src={logo} alt="logo" height={36}/>
</Link>
{/* 菜单 */}
<nav className="flex flex-col items-center">
<nav className="flex flex-col">
<Button
theme="ghost"
className="w-9 h-9 lg:hidden"
@@ -150,13 +139,8 @@ export default function Provider(props: ProviderProps) {
</Button>
<ul
className={merge(
'h-full items-stretch max-lg:h-auto relative',
'max-lg:absolute max-lg:top-full max-lg:left-0 max-lg:w-screen',
'max-lg:bg-[#fffe] max-lg:backdrop-blur-sm',
mobileMenuOpen
? 'max-lg:flex max-lg:flex-col max-lg:max-h-[500px] max-lg:py-4 max-lg:opacity-100'
: 'max-lg:hidden max-lg:max-h-0 max-lg:py-0 max-lg:opacity-0',
'lg:flex lg:items-stretch ',
`h-full flex items-stretch`,
'max-lg:absolute max-lg:top-full max-lg:left-0 max-lg:w-screen max-lg:h-12 max-lg:overflow-auto',
)}
>
<LinkItem text="首页" href="/"/>
@@ -167,34 +151,25 @@ export default function Provider(props: ProviderProps) {
onEnter={() => handleMenuEnter(0)}
onLeave={handleMenuLeave}
onTouchStart={() => handleMenuEnter(0)}
>
{/* {!isMobile && !menu && <ProductMenu/>} */}
</MenuItem>
/>
<MenuItem
text="业务场景"
active={menu && page === 1}
onEnter={() => handleMenuEnter(1)}
onLeave={handleMenuLeave}
// onTouchStart={() => handleMenuEnter(1)}
>
{/* {!isMobile && !menu && <SolutionMenu/>} */}
</MenuItem>
/>
<MenuItem
text="帮助中心"
active={menu && page === 2}
onEnter={() => handleMenuEnter(2)}
onLeave={handleMenuLeave}
// onTouchStart={() => handleMenuEnter(2)}
>
{/* {!isMobile && !menu && <HelpMenu/>} */}
</MenuItem>
/>
<LinkItem
text="企业服务"
href="#"/>
<LinkItem
text="推广返利"
href="#"/>
</ul>
</nav>
</div>
@@ -222,7 +197,7 @@ export default function Provider(props: ProviderProps) {
</div>
)
: (
<UserCenter/>
<div></div>
)
}
</div>
@@ -234,16 +209,16 @@ export default function Provider(props: ProviderProps) {
className={[
`shadow-lg`,
`overflow-hidden bg-[#fffe] backdrop-blur-sm`,
`transition-all duration-200 ease-in-out`,
`transition-[opacity,padding,height] transition-discrete duration-200 ease-in-out`,
menu
? `opacity-100 py-8 h-auto visible`
: `opacity-0 py-0 h-0 invisible`,
isMobile
? `delay-[0s,0s,0s] opacity-100 py-8 h-auto`
: `delay-[0s,0s,0.2s] opacity-0 py-0 h-0`,
isMobile()
? `max-lg:fixed max-lg:top-65 max-lg:left-0 max-lg:right-0 max-lg:w-full max-lg:z-[60] max-lg:overflow-y-auto max-lg:max-h-[calc(100vh-5rem)]`
: `absolute top-full left-0 right-0`,
].join(' ')}
onPointerEnter={() => !isMobile && setMenu(true)}
onPointerLeave={() => !isMobile && setMenu(false)}
onPointerEnter={() => !isMobile() && setMenu(true)}
onPointerLeave={() => !isMobile() && setMenu(false)}
>
{pages[page]}
</div>

View File

@@ -12,7 +12,7 @@ import {StaticImageData} from 'next/image'
export default function SolutionMenu() {
return (
<Wrap className="grid grid-cols-4 auto-rows-fr gap-4 h-75">
<Wrap className="grid grid-cols-2 lg:grid-cols-4 lg:auto-rows-fr gap-4">
<SolutionItem
icon={s01}
title="数据抓取"
@@ -65,7 +65,7 @@ function SolutionItem(props: {
return (
<div
className={[
`h-full p-4 flex gap-4 items-start rounded-md cursor-pointer`,
`h-full lg:p-4 flex gap-4 items-start rounded-md cursor-pointer`,
`transition-colors duration-200 hover:bg-blue-50`,
].join(' ')}
>