优化导航栏性能,完善导航栏弹出菜单的移动端适配;调整导航栏与 store 公共组件的所在目录结构
This commit is contained in:
@@ -25,6 +25,7 @@ const eslintConfig = [
|
|||||||
}],
|
}],
|
||||||
'@stylistic/jsx-closing-bracket-location': 'off',
|
'@stylistic/jsx-closing-bracket-location': 'off',
|
||||||
'@stylistic/jsx-curly-newline': 'off',
|
'@stylistic/jsx-curly-newline': 'off',
|
||||||
|
'@stylistic/jsx-one-expression-per-line': 'off',
|
||||||
'@stylistic/multiline-ternary': 'off',
|
'@stylistic/multiline-ternary': 'off',
|
||||||
'@stylistic/block-spacing': 'off',
|
'@stylistic/block-spacing': 'off',
|
||||||
'@typescript-eslint/no-empty-object-type': 'off',
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {ApiResponse} from '@/lib/api'
|
|||||||
import {Label} from '@/components/ui/label'
|
import {Label} from '@/components/ui/label'
|
||||||
import logo from '@/assets/logo.webp'
|
import logo from '@/assets/logo.webp'
|
||||||
import bg from './_assets/bg.webp'
|
import bg from './_assets/bg.webp'
|
||||||
import {useProfileStore} from '@/app/stores'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export type LoginPageProps = {}
|
export type LoginPageProps = {}
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
import Image from 'next/image'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import down from '@/assets/header/down.svg'
|
|
||||||
|
|
||||||
export function LinkItem(props: {
|
|
||||||
text: string
|
|
||||||
href: string
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<li className="group relative flex-none">
|
|
||||||
<Link
|
|
||||||
href={props.href}
|
|
||||||
className={[
|
|
||||||
`h-full px-4 flex items-center text-lg`,
|
|
||||||
`transition-colors duration-200 ease-in-out`,
|
|
||||||
`hover:text-blue-500`,
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
{props.text}
|
|
||||||
</Link>
|
|
||||||
<div className={[
|
|
||||||
`absolute bottom-0 w-full h-0.5 bg-transparent `,
|
|
||||||
`transition-colors duration-200`,
|
|
||||||
`group-hover:bg-blue-500`,
|
|
||||||
].join(' ')}>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MenuItem(props: {
|
|
||||||
text: string
|
|
||||||
active: boolean
|
|
||||||
onEnter: (e: React.PointerEvent) => void
|
|
||||||
onLeave: (e: React.PointerEvent) => void
|
|
||||||
onTouchStart?: (e: React.TouchEvent) => void
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<li className="group relative flex-none">
|
|
||||||
<button
|
|
||||||
onPointerEnter={props.onEnter}
|
|
||||||
onPointerLeave={props.onLeave}
|
|
||||||
onTouchStart={props.onTouchStart}
|
|
||||||
className={[
|
|
||||||
`h-full px-4 flex gap-3 items-center cursor-pointer text-lg`,
|
|
||||||
`transition-colors duration-200 ease-in-out`,
|
|
||||||
props.active
|
|
||||||
? `lg:text-blue-500`
|
|
||||||
: ``,
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
<span>{props.text}</span>
|
|
||||||
<Image
|
|
||||||
src={down}
|
|
||||||
alt="drop_menu"
|
|
||||||
className={[
|
|
||||||
`transition-transform duration-200 ease-in-out`,
|
|
||||||
props.active
|
|
||||||
? `rotate-180`
|
|
||||||
: ``,
|
|
||||||
].join(' ')}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
`absolute bottom-0 w-full h-0.5 pointer-events-none`,
|
|
||||||
`transition-colors duration-200`,
|
|
||||||
props.active
|
|
||||||
? `lg:bg-blue-500`
|
|
||||||
: 'bg-transparent',
|
|
||||||
].join(' ')}/>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移动端
|
|
||||||
export function MobileLinkItem(props: {
|
|
||||||
text: string
|
|
||||||
href: string
|
|
||||||
onClick?: () => void
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href={props.href}
|
|
||||||
onClick={props.onClick}
|
|
||||||
className={[
|
|
||||||
`block px-4 py-2 text-lg rounded-md`,
|
|
||||||
`transition-colors duration-200 ease-in-out`,
|
|
||||||
`hover:bg-blue-50 hover:text-blue-600`,
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
{props.text}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
'use client'
|
|
||||||
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'
|
|
||||||
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 {useSearchParams} from 'next/navigation'
|
|
||||||
import {useRouter} from 'next/navigation'
|
|
||||||
|
|
||||||
type TabType = 'domestic' | 'oversea'
|
|
||||||
|
|
||||||
export function Tab(props: {
|
|
||||||
selected: boolean
|
|
||||||
onSelect: () => void
|
|
||||||
children: ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<li role="tab">
|
|
||||||
<button
|
|
||||||
className={[
|
|
||||||
`p-8 text-lg cursor-pointer border-r`,
|
|
||||||
props.selected ? `bg-gradient-to-r from-transparent to-blue-200 border-blue-400` : `border-gray-200`,
|
|
||||||
].join(' ')}
|
|
||||||
onClick={props.onSelect}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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'
|
|
||||||
return (
|
|
||||||
<section role="tabpanel" className="flex gap-16 mr-16">
|
|
||||||
<div className="w-64 flex flex-col">
|
|
||||||
<h3 className="mb-6 font-bold flex items-center gap-3">
|
|
||||||
<Image src={prod} alt="产品" className="w-10 h-=10"/>
|
|
||||||
<span>代理产品</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<DomesticLink
|
|
||||||
label="动态IP"
|
|
||||||
desc="全国300+城市级定位节点"
|
|
||||||
href="/product?type=short"
|
|
||||||
discount={45}
|
|
||||||
active={currentType === 'short'}
|
|
||||||
/>
|
|
||||||
<DomesticLink
|
|
||||||
label="长效静态IP"
|
|
||||||
desc="IP 资源覆盖全国"
|
|
||||||
href="/product?type=long"
|
|
||||||
discount={45}
|
|
||||||
active={currentType === 'long'}
|
|
||||||
/>
|
|
||||||
<DomesticLink
|
|
||||||
label="固定IP"
|
|
||||||
desc="全国300+城市级定位节点"
|
|
||||||
href="/product?type=fixed"
|
|
||||||
discount={45}
|
|
||||||
active={currentType === 'fixed'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-64 flex flex-col gap-4 max-lg:hidden">
|
|
||||||
<h3 className="font-bold mb-2 flex items-center gap-3">
|
|
||||||
<Image src={custom} alt="定制" className="w-10 h-10"/>
|
|
||||||
<span>业务定制</span>
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p>优质/企业/精选IP</p>
|
|
||||||
<p className="text-gray-400 text-sm">
|
|
||||||
超 1000 家企业共同信赖之选!大客户经理全
|
|
||||||
程 1 对 1 沟通,随时为您排忧解难,提供 24
|
|
||||||
小时不间断支持
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DomesticLink(props: {
|
|
||||||
label: string
|
|
||||||
desc: string
|
|
||||||
href: string
|
|
||||||
discount: number
|
|
||||||
active?: boolean
|
|
||||||
}) {
|
|
||||||
const router = useRouter()
|
|
||||||
// const ctx = useContext(HeaderContext)
|
|
||||||
// if (!ctx) {
|
|
||||||
// throw new Error(`HeaderContext not found`)
|
|
||||||
// }
|
|
||||||
|
|
||||||
const onClick = (e: MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
// ctx.setMenu(false)
|
|
||||||
router.push(props.href)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={props.href}
|
|
||||||
className={merge(
|
|
||||||
`transition-colors duration-150 ease-in-out`,
|
|
||||||
`p-4 rounded-lg flex flex-col gap-2 hover:bg-blue-50`,
|
|
||||||
props.active ? 'bg-blue-100' : '',
|
|
||||||
)}
|
|
||||||
onClick={onClick}>
|
|
||||||
<p className="flex gap-2">
|
|
||||||
<span>{props.label}</span>
|
|
||||||
<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>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Oversea(props: {}) {
|
|
||||||
return (
|
|
||||||
<section role="tabpanel">
|
|
||||||
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import {createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import {LinkItem, MenuItem} from './navs'
|
|
||||||
import SolutionMenu from './solution'
|
|
||||||
import ProductMenu from './product'
|
|
||||||
import HelpMenu from './help'
|
|
||||||
import Wrap from '@/components/wrap'
|
|
||||||
import logo from '@/assets/logo.webp'
|
|
||||||
import {Button} from '@/components/ui/button'
|
|
||||||
import {useProfileStore} from '@/app/stores'
|
|
||||||
import UserCenter from '@/components/composites/user-center'
|
|
||||||
import {MenuIcon} from 'lucide-react'
|
|
||||||
import {merge} from '@/lib/utils'
|
|
||||||
|
|
||||||
export const HeaderContext = createContext<{
|
|
||||||
setMenu: (value: boolean) => void
|
|
||||||
isMobile: boolean
|
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
export type ProviderProps = {}
|
|
||||||
|
|
||||||
export default function Provider(props: ProviderProps) {
|
|
||||||
// ======================
|
|
||||||
// 滚动条状态
|
|
||||||
// ======================
|
|
||||||
const [scroll, setScroll] = useState(false) // Changed to false for client-side rendering
|
|
||||||
const handleScroll = useCallback(() => {
|
|
||||||
setScroll(window.scrollY > 48)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize scroll state on client
|
|
||||||
setScroll(window.scrollY > 48)
|
|
||||||
window.addEventListener('scroll', handleScroll)
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('scroll', handleScroll)
|
|
||||||
}
|
|
||||||
}, [handleScroll])
|
|
||||||
|
|
||||||
// ======================
|
|
||||||
// 菜单状态
|
|
||||||
// ======================
|
|
||||||
|
|
||||||
const [menu, setMenu] = useState(false)
|
|
||||||
const [page, setPage] = useState(0)
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [productDropdownOpen, setProductDropdownOpen] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
|
|
||||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
||||||
const target = e.target as HTMLElement
|
|
||||||
// 排除所有交互元素
|
|
||||||
if (!target.closest('a, button, [role="button"], [role="tab"]')) {
|
|
||||||
setMenu(false)
|
|
||||||
setProductDropdownOpen(false) // 同时关闭产品下拉菜单
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener('mousedown', handleClickOutside)
|
|
||||||
document.addEventListener('touchstart', handleClickOutside)
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside)
|
|
||||||
document.removeEventListener('touchstart', handleClickOutside)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const isMobile = () => {
|
|
||||||
if (typeof window === 'undefined') return false
|
|
||||||
return window.innerWidth <= 768
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMenuEnter = (pageIndex: number) => {
|
|
||||||
if (isMobile() && menu && page === pageIndex) {
|
|
||||||
setMenu(false)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setPage(pageIndex)
|
|
||||||
setMenu(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMenuLeave = () => {
|
|
||||||
if (!isMobile()) {
|
|
||||||
setMenu(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pages = useMemo(() => [
|
|
||||||
<ProductMenu key="product"/>,
|
|
||||||
<SolutionMenu key="solution"/>,
|
|
||||||
<HelpMenu key="help"/>,
|
|
||||||
], [])
|
|
||||||
|
|
||||||
const toggleMobileMenu = () => {
|
|
||||||
setMenu(!menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================
|
|
||||||
// 用户信息
|
|
||||||
// ======================
|
|
||||||
|
|
||||||
const profile = useProfileStore(store => store.profile)
|
|
||||||
|
|
||||||
// ======================
|
|
||||||
// render
|
|
||||||
// ======================
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HeaderContext.Provider value={{setMenu, isMobile: isMobile()}}>
|
|
||||||
<div
|
|
||||||
ref={menuRef}
|
|
||||||
className={[
|
|
||||||
`transition-[background, shadow] duration-200 ease-in-out`,
|
|
||||||
menu
|
|
||||||
? `bg-[#fffe] backdrop-blur-sm`
|
|
||||||
: scroll
|
|
||||||
? `bg-[#fffe] backdrop-blur-sm shadow-lg`
|
|
||||||
: `bg-transparent shadow-none`,
|
|
||||||
].join(' ')}>
|
|
||||||
<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="self-center">
|
|
||||||
<Image src={logo} alt="logo" height={36}/>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* 菜单 */}
|
|
||||||
<nav className="flex flex-col">
|
|
||||||
<Button
|
|
||||||
theme="ghost"
|
|
||||||
className="w-9 h-9 lg:hidden"
|
|
||||||
onClick={toggleMobileMenu}
|
|
||||||
aria-expanded={productDropdownOpen}
|
|
||||||
>
|
|
||||||
<MenuIcon/>
|
|
||||||
</Button>
|
|
||||||
<ul
|
|
||||||
className={merge(
|
|
||||||
`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="/"/>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
text="产品订购"
|
|
||||||
active={menu && page === 0}
|
|
||||||
onEnter={() => handleMenuEnter(0)}
|
|
||||||
onLeave={handleMenuLeave}
|
|
||||||
onTouchStart={() => handleMenuEnter(0)}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
text="业务场景"
|
|
||||||
active={menu && page === 1}
|
|
||||||
onEnter={() => handleMenuEnter(1)}
|
|
||||||
onLeave={handleMenuLeave}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
text="帮助中心"
|
|
||||||
active={menu && page === 2}
|
|
||||||
onEnter={() => handleMenuEnter(2)}
|
|
||||||
onLeave={handleMenuLeave}
|
|
||||||
/>
|
|
||||||
<LinkItem
|
|
||||||
text="企业服务"
|
|
||||||
href="#"/>
|
|
||||||
<LinkItem
|
|
||||||
text="推广返利"
|
|
||||||
href="#"/>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
{/* 登录 */}
|
|
||||||
<div className="flex items-center">
|
|
||||||
{profile == undefined
|
|
||||||
? (
|
|
||||||
<div className="flex items-center max-lg:gap-2">
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className="px-2 lg:w-24 h-9 lg:h-12 flex items-center justify-center lg:text-lg"
|
|
||||||
>
|
|
||||||
<span>登录</span>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className={[
|
|
||||||
`px-2 lg:w-24 h-9 lg:h-12 bg-gradient-to-r rounded-sm 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>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<div></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Wrap>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 下拉菜单 */}
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
`shadow-lg`,
|
|
||||||
`overflow-hidden bg-[#fffe] backdrop-blur-sm`,
|
|
||||||
`transition-[opacity,padding,height] transition-discrete duration-200 ease-in-out`,
|
|
||||||
menu
|
|
||||||
? `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)}
|
|
||||||
>
|
|
||||||
{pages[page]}
|
|
||||||
</div>
|
|
||||||
</HeaderContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
18
src/app/(home)/@header/common.tsx
Normal file
18
src/app/(home)/@header/common.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {createContext} from 'react'
|
||||||
|
import Image, {StaticImageData} from 'next/image'
|
||||||
|
|
||||||
|
export const HeaderContext = createContext<{
|
||||||
|
setMenu: (value: boolean) => void
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
export function FragmentTitle(props: {
|
||||||
|
text: string
|
||||||
|
img: StaticImageData
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<h3 className="font-bold flex items-center gap-3">
|
||||||
|
<Image src={props.img} alt="icon" aria-hidden className="size-8 lg:size-9"/>
|
||||||
|
<span>{props.text}</span>
|
||||||
|
</h3>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,16 +5,17 @@ import h01 from '@/assets/header/help/01.svg'
|
|||||||
import h02 from '@/assets/header/help/02.svg'
|
import h02 from '@/assets/header/help/02.svg'
|
||||||
import h03 from '@/assets/header/help/03.svg'
|
import h03 from '@/assets/header/help/03.svg'
|
||||||
import banner from '@/assets/header/help/banner.webp'
|
import banner from '@/assets/header/help/banner.webp'
|
||||||
|
import {FragmentTitle} from '@/app/(home)/@header/common'
|
||||||
|
|
||||||
export default function HelpMenu() {
|
export default function HelpMenu() {
|
||||||
return (
|
return (
|
||||||
<Wrap className="w-full grid grid-cols-3 lg:grid-cols-4 gap-4 justify-items-start">
|
<Wrap className="w-full grid sm:grid-cols-3 lg:grid-cols-4 gap-4 justify-items-start">
|
||||||
<Column
|
<Column
|
||||||
icon={h01}
|
icon={h01}
|
||||||
title="提取IP"
|
title="提取 IP"
|
||||||
items={[
|
items={[
|
||||||
{lead: '短效动态IP提取', href: '#'},
|
{lead: '短效 IP 提取', href: '#'},
|
||||||
{lead: '长效静态IP提取', href: '#'},
|
{lead: '长效 IP 提取', href: '#'},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
@@ -49,14 +50,13 @@ function Column(props: {
|
|||||||
}[]
|
}[]
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex-1 flex flex-col gap-4">
|
||||||
<h3 className="font-bold flex gap-3 items-center">
|
<FragmentTitle img={props.icon} text={props.title}/>
|
||||||
<Image src={props.icon} alt={props.title} className="w-10 h-10"/>
|
<ul className="flex flex-col gap-2">
|
||||||
<span>{props.title}</span>
|
|
||||||
</h3>
|
|
||||||
<ul className=" text-gray-500 text-sm flex flex-col items-end gap-2">
|
|
||||||
{props.items.map((item, index) => (
|
{props.items.map((item, index) => (
|
||||||
<li key={index}><Link href={item.href}>{item.lead}</Link></li>
|
<li key={index}>
|
||||||
|
<Link href={item.href} className="px-4 py-2">{item.lead}</Link>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
31
src/app/(home)/@header/menu-mobile.tsx
Normal file
31
src/app/(home)/@header/menu-mobile.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import ProductMenu from './menu-product'
|
||||||
|
import HelpMenu from './menu-help'
|
||||||
|
import SolutionMenu from './menu-solution'
|
||||||
|
|
||||||
|
export type MobileMenuProps = {}
|
||||||
|
|
||||||
|
export default function MobileMenu(props: MobileMenuProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<ProductMenu/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<MenuTitle title="帮助中心"/>
|
||||||
|
<HelpMenu/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<MenuTitle title="业务场景"/>
|
||||||
|
<SolutionMenu/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuTitle(props: {title: string}) {
|
||||||
|
return (
|
||||||
|
<h3 className="text-xl text-weak px-4">
|
||||||
|
{props.title}
|
||||||
|
</h3>
|
||||||
|
)
|
||||||
|
}
|
||||||
150
src/app/(home)/@header/menu-product.tsx
Normal file
150
src/app/(home)/@header/menu-product.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
'use client'
|
||||||
|
import {ReactNode, useContext, useState} from 'react'
|
||||||
|
import Wrap from '@/components/wrap'
|
||||||
|
import anno from '@/assets/header/product/anno.svg'
|
||||||
|
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'
|
||||||
|
|
||||||
|
type TabType = 'domestic' | 'oversea'
|
||||||
|
|
||||||
|
export default function ProductMenu() {
|
||||||
|
const [type, setType] = useState<TabType>('domestic')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrap className="flex flex-col items-stretch lg:flex-row gap-4 lg:gap-0">
|
||||||
|
<ul role="tablist" className="flex-none lg:basis-36 flex lg:flex-col">
|
||||||
|
<Tab selected={type === 'domestic'} onSelect={() => setType('domestic')}>国内代理</Tab>
|
||||||
|
<Tab selected={type === 'oversea'} onSelect={() => setType('oversea')}>海外代理</Tab>
|
||||||
|
</ul>
|
||||||
|
{type === 'domestic'
|
||||||
|
? (
|
||||||
|
<Domestic/>
|
||||||
|
) : (
|
||||||
|
<Oversea/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<aside className="w-full lg:w-64 hidden lg:block">
|
||||||
|
<FragmentTitle img={anno} text="网站公告"/>
|
||||||
|
<div className="flex flex-col gap-2 p-4">
|
||||||
|
<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 Tab(props: {
|
||||||
|
selected: boolean
|
||||||
|
onSelect: () => void
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li role="tab" className="flex-1 lg:flex-none">
|
||||||
|
<button
|
||||||
|
className={[
|
||||||
|
`w-full p-4 lg:p-6 text-base lg:text-lg cursor-pointer border-b lg:border-b-0 lg:border-r flex justify-center`,
|
||||||
|
props.selected ? `bg-gradient-to-b lg:bg-gradient-to-r from-transparent to-blue-200 border-blue-400` : `border-gray-200`,
|
||||||
|
].join(' ')}
|
||||||
|
onClick={props.onSelect}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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="代理产品"/>
|
||||||
|
<DomesticLink
|
||||||
|
label="短效动态 IP"
|
||||||
|
desc="全国300+城市级定位节点"
|
||||||
|
href="/product?type=short"
|
||||||
|
discount={45}
|
||||||
|
/>
|
||||||
|
<DomesticLink
|
||||||
|
label="长效动态 IP"
|
||||||
|
desc="IP 资源覆盖全国"
|
||||||
|
href="/product?type=long"
|
||||||
|
discount={45}
|
||||||
|
/>
|
||||||
|
<DomesticLink
|
||||||
|
label="静态 IP"
|
||||||
|
desc="全国300+城市级定位节点"
|
||||||
|
href="/product?type=fixed"
|
||||||
|
discount={45}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full lg:w-64 flex flex-col lg:max-lg:hidden">
|
||||||
|
<FragmentTitle img={custom} text="业务定制"/>
|
||||||
|
<DomesticLink
|
||||||
|
label="优质/企业/精选IP"
|
||||||
|
desc="超 1000 家企业共同信赖之选!大客户经理全程 1 对 1 沟通,随时为您排忧解难,提供 24 小时不间断支持"
|
||||||
|
href="/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Oversea(props: {}) {
|
||||||
|
return (
|
||||||
|
<section role="tabpanel" className="flex-auto flex items-center justify-center">
|
||||||
|
<p className="text-2xl text-primary">产品即将上线,敬请期待~</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DomesticLink(props: {
|
||||||
|
label: string
|
||||||
|
desc: string
|
||||||
|
href: string
|
||||||
|
discount?: number
|
||||||
|
}) {
|
||||||
|
const router = useRouter()
|
||||||
|
const ctx = useContext(HeaderContext)
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error(`HeaderContext not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
ctx.setMenu(false)
|
||||||
|
router.push(props.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={props.href}
|
||||||
|
className={merge(
|
||||||
|
`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>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import Image from 'next/image'
|
|
||||||
import Wrap from '@/components/wrap'
|
import Wrap from '@/components/wrap'
|
||||||
import s01 from '@/assets/header/solution/01.svg'
|
import s01 from '@/assets/header/solution/01.svg'
|
||||||
import s02 from '@/assets/header/solution/02.svg'
|
import s02 from '@/assets/header/solution/02.svg'
|
||||||
@@ -9,10 +8,11 @@ import s06 from '@/assets/header/solution/06.svg'
|
|||||||
import s07 from '@/assets/header/solution/07.svg'
|
import s07 from '@/assets/header/solution/07.svg'
|
||||||
import s08 from '@/assets/header/solution/08.svg'
|
import s08 from '@/assets/header/solution/08.svg'
|
||||||
import {StaticImageData} from 'next/image'
|
import {StaticImageData} from 'next/image'
|
||||||
|
import {FragmentTitle} from '@/app/(home)/@header/common'
|
||||||
|
|
||||||
export default function SolutionMenu() {
|
export default function SolutionMenu() {
|
||||||
return (
|
return (
|
||||||
<Wrap className="grid grid-cols-2 lg:grid-cols-4 lg:auto-rows-fr gap-4">
|
<Wrap className="h-full grid grid-cols-2 lg:grid-cols-4 auto-rows-fr gap-4">
|
||||||
<SolutionItem
|
<SolutionItem
|
||||||
icon={s01}
|
icon={s01}
|
||||||
title="数据抓取"
|
title="数据抓取"
|
||||||
@@ -65,15 +65,12 @@ function SolutionItem(props: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
`h-full lg:p-4 flex gap-4 items-start rounded-md cursor-pointer`,
|
`flex flex-col gap-2 items-start rounded-md cursor-pointer`,
|
||||||
`transition-colors duration-200 hover:bg-blue-50`,
|
`transition-colors duration-200 hover:bg-blue-50`,
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<Image src={props.icon} alt={props.title} className="w-10 h-10"/>
|
<FragmentTitle img={props.icon} text={props.title}/>
|
||||||
<div className="flex flex-col gap-1">
|
<p className="text-gray-400 text-sm">{props.desc}</p>
|
||||||
<h3 className="font-bold">{props.title}</h3>
|
|
||||||
<p className="text-gray-400 text-sm">{props.desc}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,36 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import UserCenter from '@/components/composites/user-center'
|
import {useCallback, useEffect, useMemo, useState, PointerEvent, ComponentProps} from 'react'
|
||||||
import {useClientStore, useProfileStore} from '@/app/stores'
|
import Link from 'next/link'
|
||||||
import {buttonVariants} from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
Navigation,
|
|
||||||
NavigationIndicator,
|
|
||||||
NavigationLink,
|
|
||||||
NavigationLinkItem,
|
|
||||||
NavigationGroup,
|
|
||||||
NavigationTriggerItem,
|
|
||||||
NavigationMenuViewport,
|
|
||||||
} from '@/components/ui/navigation-menu'
|
|
||||||
import Wrap from '@/components/wrap'
|
|
||||||
import {merge} from '@/lib/utils'
|
|
||||||
import {useState, useCallback, useEffect, useContext} from 'react'
|
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import SolutionMenu from './menu-solution'
|
||||||
|
import ProductMenu from './menu-product'
|
||||||
|
import HelpMenu from './menu-help'
|
||||||
|
import MobileMenu from './menu-mobile'
|
||||||
|
import Wrap from '@/components/wrap'
|
||||||
import logo from '@/assets/logo.webp'
|
import logo from '@/assets/logo.webp'
|
||||||
import ProductMenu, {Domestic} from '@/app/(home)/@header/_client/product'
|
import {Button} from '@/components/ui/button'
|
||||||
import SolutionMenu from '@/app/(home)/@header/_client/solution'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import HelpMenu from '@/app/(home)/@header/_client/help'
|
import UserCenter from '@/components/composites/user-center'
|
||||||
import {MenuIcon} from 'lucide-react'
|
import {MenuIcon} from 'lucide-react'
|
||||||
|
import down from '@/assets/header/down.svg'
|
||||||
|
import {merge} from '@/lib/utils'
|
||||||
|
import {HeaderContext} from './common'
|
||||||
|
|
||||||
export type HeaderProps = {}
|
export type ProviderProps = {}
|
||||||
|
|
||||||
export default function Header(props: HeaderProps) {
|
export default function Page(props: ProviderProps) {
|
||||||
// ======================
|
// ======================
|
||||||
// 背景显示状态
|
// 滚动条状态
|
||||||
// ======================
|
// ======================
|
||||||
|
|
||||||
const [expand, setExpand] = useState(false)
|
const [scroll, setScroll] = useState(false) // Changed to false for client-side rendering
|
||||||
const [scroll, setScroll] = useState(false)
|
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
setScroll(window.scrollY > 48)
|
setScroll(window.scrollY > 48)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Initialize scroll state on client
|
||||||
setScroll(window.scrollY > 48)
|
setScroll(window.scrollY > 48)
|
||||||
window.addEventListener('scroll', handleScroll)
|
window.addEventListener('scroll', handleScroll)
|
||||||
return () => {
|
return () => {
|
||||||
@@ -42,10 +39,57 @@ export default function Header(props: HeaderProps) {
|
|||||||
}, [handleScroll])
|
}, [handleScroll])
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// 移动端
|
// 菜单状态
|
||||||
// ======================
|
// ======================
|
||||||
|
|
||||||
const lg = useClientStore(state => state.breakpoint.lg)
|
const [menu, setMenu] = useState(false)
|
||||||
|
const [page, setPage] = useState(0)
|
||||||
|
const pages = useMemo(() => [
|
||||||
|
<ProductMenu key="product"/>,
|
||||||
|
<SolutionMenu key="solution"/>,
|
||||||
|
<HelpMenu key="help"/>,
|
||||||
|
<MobileMenu key="mobile"/>,
|
||||||
|
], [])
|
||||||
|
|
||||||
|
const enterMenu = (i: number) => {
|
||||||
|
return (e: PointerEvent) => {
|
||||||
|
setPage(i)
|
||||||
|
if (e.pointerType === 'mouse' || page !== i) {
|
||||||
|
setMenu(true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setMenu(!menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitMenu = (e: PointerEvent) => {
|
||||||
|
if (e.pointerType === 'mouse') {
|
||||||
|
setMenu(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enterMenuContent = (e: PointerEvent) => {
|
||||||
|
setMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitMenuContent = (e: PointerEvent) => {
|
||||||
|
if (e.pointerType === 'mouse') {
|
||||||
|
setMenu(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enterMenuMask = (e: PointerEvent) => {
|
||||||
|
if (e.pointerType !== 'mouse') {
|
||||||
|
setMenu(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// 用户信息
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
const profile = useProfileStore(store => store.profile)
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// render
|
// render
|
||||||
@@ -53,102 +97,188 @@ export default function Header(props: HeaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
data-expand={expand}
|
className={merge(
|
||||||
data-scroll={scroll}
|
'fixed top-0 left-0 w-screen z-10 flex flex-col',
|
||||||
data-effect={expand || scroll}
|
menu && 'h-screen',
|
||||||
className="group/header w-full fixed left-0 top-0 z-20"
|
)}>
|
||||||
>
|
<HeaderContext.Provider value={{setMenu}}>
|
||||||
<Navigation
|
|
||||||
onValueChange={(value) => {
|
{/* 菜单栏 */}
|
||||||
setExpand(!!value)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={merge(
|
<div className={merge(
|
||||||
`transition-[background-color,backdrop-filter,box-shadow] duration-200 ease-in-out`,
|
`flex-none`,
|
||||||
`bg-transparent backdrop-blur-none shadow-none`,
|
`transition-[background,shadow] duration-200 ease-in-out`,
|
||||||
`group-data-[effect=true]/header:bg-card/90`,
|
menu
|
||||||
`group-data-[effect=true]/header:backdrop-blur-sm`,
|
? `bg-[#fffe] backdrop-blur-sm`
|
||||||
`group-data-[scroll=true]/header:shadow-lg`,
|
: scroll
|
||||||
|
? `bg-[#fffe] backdrop-blur-sm shadow-lg`
|
||||||
|
: `bg-transparent shadow-none`,
|
||||||
)}>
|
)}>
|
||||||
<Wrap className={merge('h-14 md:h-16 flex justify-between items-stretch')}>
|
<Wrap className="h-20 max-md:h-16 flex justify-between">
|
||||||
<div className="flex items-stretch lg:flex-row-reverse">
|
<nav className="flex items-center justify-between lg:flex-row-reverse gap-4 lg:gap-8">
|
||||||
{lg ? (
|
|
||||||
<NavigationGroup className="gap-0 flex">
|
|
||||||
<NavigationLinkItem href="/" text="首页"/>
|
|
||||||
|
|
||||||
<NavigationTriggerItem text="产品订购" className="h-80">
|
{/* 桌面端菜单 */}
|
||||||
<ProductMenu/>
|
<ul className="h-full items-stretch hidden lg:flex">
|
||||||
</NavigationTriggerItem>
|
<LinkItem text="首页" href="/"/>
|
||||||
|
<MenuItem
|
||||||
|
text="产品订购"
|
||||||
|
active={menu && page === 0}
|
||||||
|
onPointerEnter={enterMenu(0)}
|
||||||
|
onPointerLeave={exitMenu}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
text="业务场景"
|
||||||
|
active={menu && page === 1}
|
||||||
|
onPointerEnter={enterMenu(1)}
|
||||||
|
onPointerLeave={exitMenu}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
text="帮助中心"
|
||||||
|
active={menu && page === 2}
|
||||||
|
onPointerEnter={enterMenu(2)}
|
||||||
|
onPointerLeave={exitMenu}
|
||||||
|
/>
|
||||||
|
<LinkItem
|
||||||
|
text="企业服务"
|
||||||
|
href="#"/>
|
||||||
|
<LinkItem
|
||||||
|
text="推广返利"
|
||||||
|
href="#"/>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<NavigationTriggerItem text="业务场景" className="h-80">
|
{/* 移动端菜单 */}
|
||||||
<SolutionMenu/>
|
<Button
|
||||||
</NavigationTriggerItem>
|
theme="ghost"
|
||||||
|
className="lg:hidden size-10"
|
||||||
|
onPointerEnter={enterMenu(3)}
|
||||||
|
onPointerLeave={exitMenu}
|
||||||
|
>
|
||||||
|
<MenuIcon/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<NavigationTriggerItem text="帮助中心" className="h-80">
|
{/* logo */}
|
||||||
<HelpMenu/>
|
<Link href="/" className="flex items-center">
|
||||||
</NavigationTriggerItem>
|
<Image src={logo} alt="logo" height={40} className="translate-y-0.5"/>
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<NavigationLinkItem href="/" text="企业服务"/>
|
{/* 登录 */}
|
||||||
<NavigationLinkItem href="/" text="推广返利"/>
|
<div className="flex items-center">
|
||||||
|
{profile == undefined
|
||||||
<NavigationIndicator/>
|
? (
|
||||||
</NavigationGroup>
|
<>
|
||||||
) : (
|
<Link
|
||||||
<NavigationGroup className="flex">
|
href="/login"
|
||||||
<NavigationTriggerItem
|
className="w-24 h-12 flex items-center justify-center lg:text-lg"
|
||||||
suffix={false}
|
>
|
||||||
text={<MenuIcon/>}
|
<span>登录</span>
|
||||||
className={merge(
|
</Link>
|
||||||
`flex flex-col items-start gap-6`,
|
<Link
|
||||||
)}
|
href="/login"
|
||||||
>
|
className={[
|
||||||
<ProductMenu/>
|
`w-20 lg:w-24 h-10 lg:h-12 bg-gradient-to-r rounded-sm flex items-center justify-center lg:text-lg text-white`,
|
||||||
|
`transition-colors duration-200 ease-in-out`,
|
||||||
<SolutionMenu/>
|
`from-blue-500 to-cyan-400 hover:from-blue-500 hover:to-cyan-300`,
|
||||||
|
].join(' ')}
|
||||||
<HelpMenu/>
|
>
|
||||||
|
<span>注册</span>
|
||||||
<NavigationLink href="/" text="企业服务"/>
|
</Link>
|
||||||
<NavigationLink href="/" text="推广返利"/>
|
</>
|
||||||
</NavigationTriggerItem>
|
)
|
||||||
</NavigationGroup>
|
: (
|
||||||
)}
|
<UserCenter profile={profile}/>
|
||||||
|
)
|
||||||
<NavigationLink href="/" text={<Image src={logo} alt="logo" height={36}/>}/>
|
}
|
||||||
</div>
|
</div>
|
||||||
<AccountRegion/>
|
|
||||||
</Wrap>
|
</Wrap>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NavigationMenuViewport className={merge(
|
{/* 下拉菜单 */}
|
||||||
`bg-card/90 backdrop-blur-sm shadow-lg`,
|
<div
|
||||||
`transition-[padding,opacity,max-height]`,
|
className={merge(
|
||||||
`group-data-[expand=false]/header:delay-[0s,0s,0.2s] group-data-[expand=true]/header:delay-0`,
|
`flex-auto overflow-auto lg:flex-none lg:basis-72 shadow-lg box-content`,
|
||||||
`p-0 group-data-[expand=true]/header:py-4`,
|
`bg-[#fffe] backdrop-blur-sm`,
|
||||||
`opacity-0 group-data-[effect=true]/header:opacity-100`,
|
`transition-[opacity,padding] transition-discrete duration-200 ease-in-out`,
|
||||||
`max-h-0 group-data-[effect=true]/header:max-h-[calc(100vh-56px)] overflow-auto`,
|
menu
|
||||||
)}/>
|
? `delay-[0s,0s] opacity-100 py-4`
|
||||||
</Navigation>
|
: `delay-[0s,0s] opacity-0 py-0 pointer-events-none`,
|
||||||
|
)}
|
||||||
|
onPointerEnter={enterMenuContent}
|
||||||
|
onPointerLeave={exitMenuContent}
|
||||||
|
>
|
||||||
|
{pages[page]}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 遮罩层 */}
|
||||||
|
<div className="flex-auto" onPointerEnter={enterMenuMask}/>
|
||||||
|
|
||||||
|
</HeaderContext.Provider>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountRegion() {
|
function LinkItem(props: {
|
||||||
const profile = useProfileStore(state => state.profile)
|
text: string
|
||||||
|
href: string
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="self-center">
|
<li className="group relative">
|
||||||
{profile ? (
|
<Link
|
||||||
<UserCenter profile={profile}/>
|
href={props.href}
|
||||||
) : (
|
className={[
|
||||||
<NavigationLink
|
`h-full px-4 flex items-center text-lg`,
|
||||||
href="/login"
|
`transition-colors duration-200 ease-in-out`,
|
||||||
text="登录 / 注册"
|
`hover:text-blue-500`,
|
||||||
classNameOverride={buttonVariants({
|
].join(' ')}
|
||||||
theme: 'gradient',
|
>
|
||||||
className: 'h-10 lg:h-12',
|
{props.text}
|
||||||
})}/>
|
</Link>
|
||||||
)}
|
<div className={[
|
||||||
</div>
|
`absolute bottom-0 w-full h-0.5 bg-transparent `,
|
||||||
|
`transition-colors duration-200`,
|
||||||
|
`group-hover:bg-blue-500`,
|
||||||
|
].join(' ')}>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem(props: {
|
||||||
|
text: string
|
||||||
|
active: boolean
|
||||||
|
} & ComponentProps<'button'>) {
|
||||||
|
return (
|
||||||
|
<li className="group relative">
|
||||||
|
<button
|
||||||
|
onPointerEnter={props.onPointerEnter}
|
||||||
|
onPointerLeave={props.onPointerLeave}
|
||||||
|
className={[
|
||||||
|
`h-full px-4 flex gap-3 items-center cursor-pointer text-lg`,
|
||||||
|
`transition-colors duration-200 ease-in-out`,
|
||||||
|
props.active
|
||||||
|
? `text-blue-500`
|
||||||
|
: ``,
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<span>{props.text}</span>
|
||||||
|
<Image
|
||||||
|
src={down}
|
||||||
|
alt="drop_menu"
|
||||||
|
className={[
|
||||||
|
`transition-transform duration-200 ease-in-out`,
|
||||||
|
props.active
|
||||||
|
? `rotate-180`
|
||||||
|
: ``,
|
||||||
|
].join(' ')}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
`absolute bottom-0 w-full h-0.5 pointer-events-none`,
|
||||||
|
`transition-colors duration-200`,
|
||||||
|
props.active
|
||||||
|
? `bg-blue-500`
|
||||||
|
: 'bg-transparent',
|
||||||
|
].join(' ')}/>
|
||||||
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {PanelLeftCloseIcon, PanelLeftOpenIcon} from 'lucide-react'
|
import {PanelLeftCloseIcon, PanelLeftOpenIcon} from 'lucide-react'
|
||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
import {useLayoutStore} from '@/app/stores'
|
import {useLayoutStore} from '@/components/stores-provider'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import UserCenter from '@/components/composites/user-center'
|
import UserCenter from '@/components/composites/user-center'
|
||||||
import {User} from '@/lib/models'
|
import {User} from '@/lib/models'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {ReactNode} from 'react'
|
import {ReactNode} from 'react'
|
||||||
import {useLayoutStore} from '@/app/stores'
|
import {useLayoutStore} from '@/components/stores-provider'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
|
|
||||||
type AdminLayoutProps = {
|
type AdminLayoutProps = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {ComponentProps, ReactNode, useState} from 'react'
|
import {ComponentProps, ReactNode, useState} from 'react'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import {useLayoutStore} from '@/app/stores'
|
import {useLayoutStore} from '@/components/stores-provider'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import logoAvatar from '../_assets/logo-avatar.svg'
|
import logoAvatar from '../_assets/logo-avatar.svg'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {Identify} from '@/actions/user'
|
|||||||
import {toast} from 'sonner'
|
import {toast} from 'sonner'
|
||||||
import {useEffect, useRef, useState} from 'react'
|
import {useEffect, useRef, useState} from 'react'
|
||||||
import * as qrcode from 'qrcode'
|
import * as qrcode from 'qrcode'
|
||||||
import {useProfileStore} from '@/app/stores'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import banner from './_assets/banner.webp'
|
import banner from './_assets/banner.webp'
|
||||||
import personal from './_assets/personal.webp'
|
import personal from './_assets/personal.webp'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {Button} from '@/components/ui/button'
|
|||||||
import {useForm} from 'react-hook-form'
|
import {useForm} from 'react-hook-form'
|
||||||
import {zodResolver} from '@hookform/resolvers/zod'
|
import {zodResolver} from '@hookform/resolvers/zod'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import {useProfileStore} from '@/app/stores'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import {toast} from 'sonner'
|
import {toast} from 'sonner'
|
||||||
import {CheckCircle, QrCodeIcon} from 'lucide-react'
|
import {CheckCircle, QrCodeIcon} from 'lucide-react'
|
||||||
import * as qrcode from 'qrcode'
|
import * as qrcode from 'qrcode'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {ReactNode, useEffect} from 'react'
|
import {ReactNode, useEffect} from 'react'
|
||||||
import {useClientStore, useLayoutStore} from '@/app/stores'
|
import {useClientStore, useLayoutStore} from '@/components/stores-provider'
|
||||||
|
|
||||||
export type EffectProviderProps = {
|
export type EffectProviderProps = {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {Metadata} from 'next'
|
|||||||
import './globals.css'
|
import './globals.css'
|
||||||
import localFont from 'next/font/local'
|
import localFont from 'next/font/local'
|
||||||
import {Toaster} from '@/components/ui/sonner'
|
import {Toaster} from '@/components/ui/sonner'
|
||||||
import Stores from '@/app/stores'
|
import StoresProvider from '@/components/stores-provider'
|
||||||
import {getProfile} from '@/actions/auth'
|
import {getProfile} from '@/actions/auth'
|
||||||
import Effects from '@/app/effects'
|
import Effects from '@/app/effects'
|
||||||
|
|
||||||
@@ -29,11 +29,11 @@ export default async function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<body className={`${font.className}`}>
|
<body className={`${font.className}`}>
|
||||||
<Stores user={user}>
|
<StoresProvider user={user}>
|
||||||
<Effects>
|
<Effects>
|
||||||
{children}
|
{children}
|
||||||
</Effects>
|
</Effects>
|
||||||
</Stores>
|
</StoresProvider>
|
||||||
<Toaster position="top-center" richColors expand/>
|
<Toaster position="top-center" richColors expand/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Image from 'next/image'
|
|||||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||||
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
||||||
import balance from '@/components/composites/purchase/_assets/balance.svg'
|
import balance from '@/components/composites/purchase/_assets/balance.svg'
|
||||||
import {useProfileStore} from '@/app/stores'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import RechargeModal from '@/components/composites/recharge'
|
import RechargeModal from '@/components/composites/recharge'
|
||||||
import Pay from '@/components/composites/purchase/pay'
|
import Pay from '@/components/composites/purchase/pay'
|
||||||
import {buttonVariants} from '@/components/ui/button'
|
import {buttonVariants} from '@/components/ui/button'
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import wechat from './_assets/wechat.svg'
|
|||||||
import balance from './_assets/balance.svg'
|
import balance from './_assets/balance.svg'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import {useEffect, useRef, useState} from 'react'
|
import {useEffect, useRef, useState} from 'react'
|
||||||
import {useProfileStore} from '@/app/stores'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import {Alert, AlertTitle} from '@/components/ui/alert'
|
import {Alert, AlertTitle} from '@/components/ui/alert'
|
||||||
import {ApiResponse, ExtraResp, ExtraReq} from '@/lib/api'
|
import {ApiResponse, ExtraResp, ExtraReq} from '@/lib/api'
|
||||||
import {toast} from 'sonner'
|
import {toast} from 'sonner'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Image from 'next/image'
|
|||||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||||
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
||||||
import balance from '@/components/composites/purchase/_assets/balance.svg'
|
import balance from '@/components/composites/purchase/_assets/balance.svg'
|
||||||
import {useProfileStore} from '@/app/stores'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import RechargeModal from '@/components/composites/recharge'
|
import RechargeModal from '@/components/composites/recharge'
|
||||||
import {buttonVariants} from '@/components/ui/button'
|
import {buttonVariants} from '@/components/ui/button'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {useEffect, useMemo, useRef, useState} from 'react'
|
|||||||
import {Loader} from 'lucide-react'
|
import {Loader} from 'lucide-react'
|
||||||
import {RechargeComplete, RechargePrepare} from '@/actions/user'
|
import {RechargeComplete, RechargePrepare} from '@/actions/user'
|
||||||
import * as qrcode from 'qrcode'
|
import * as qrcode from 'qrcode'
|
||||||
import {useProfileStore} from '@/app/stores'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
Platform,
|
Platform,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {useProfileStore} from '@/app/stores'
|
import {useProfileStore} from '@/components/stores-provider'
|
||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar'
|
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar'
|
||||||
import {LoaderIcon, LogOutIcon, UserIcon, UserPenIcon} from 'lucide-react'
|
import {LoaderIcon, LogOutIcon, UserIcon, UserPenIcon} from 'lucide-react'
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export type ProfileProviderProps = {
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Stores(props: ProfileProviderProps) {
|
export default function StoresProvider(props: ProfileProviderProps) {
|
||||||
const profile = useRef<StoreApi<ProfileStore>>(null)
|
const profile = useRef<StoreApi<ProfileStore>>(null)
|
||||||
if (!profile.current) {
|
if (!profile.current) {
|
||||||
console.log('📦 create profile store')
|
console.log('📦 create profile store')
|
||||||
Reference in New Issue
Block a user