优化导航栏性能,完善导航栏弹出菜单的移动端适配;调整导航栏与 store 公共组件的所在目录结构
This commit is contained in:
@@ -25,6 +25,7 @@ const eslintConfig = [
|
||||
}],
|
||||
'@stylistic/jsx-closing-bracket-location': 'off',
|
||||
'@stylistic/jsx-curly-newline': 'off',
|
||||
'@stylistic/jsx-one-expression-per-line': 'off',
|
||||
'@stylistic/multiline-ternary': 'off',
|
||||
'@stylistic/block-spacing': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
|
||||
@@ -27,7 +27,7 @@ import {ApiResponse} from '@/lib/api'
|
||||
import {Label} from '@/components/ui/label'
|
||||
import logo from '@/assets/logo.webp'
|
||||
import bg from './_assets/bg.webp'
|
||||
import {useProfileStore} from '@/app/stores'
|
||||
import {useProfileStore} from '@/components/stores-provider'
|
||||
import Link from 'next/link'
|
||||
|
||||
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 h03 from '@/assets/header/help/03.svg'
|
||||
import banner from '@/assets/header/help/banner.webp'
|
||||
import {FragmentTitle} from '@/app/(home)/@header/common'
|
||||
|
||||
export default function HelpMenu() {
|
||||
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
|
||||
icon={h01}
|
||||
title="提取IP"
|
||||
title="提取 IP"
|
||||
items={[
|
||||
{lead: '短效动态IP提取', href: '#'},
|
||||
{lead: '长效静态IP提取', href: '#'},
|
||||
{lead: '短效 IP 提取', href: '#'},
|
||||
{lead: '长效 IP 提取', href: '#'},
|
||||
]}
|
||||
/>
|
||||
<Column
|
||||
@@ -49,14 +50,13 @@ function Column(props: {
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="font-bold flex gap-3 items-center">
|
||||
<Image src={props.icon} alt={props.title} className="w-10 h-10"/>
|
||||
<span>{props.title}</span>
|
||||
</h3>
|
||||
<ul className=" text-gray-500 text-sm flex flex-col items-end gap-2">
|
||||
<div className="flex-1 flex flex-col gap-4">
|
||||
<FragmentTitle img={props.icon} text={props.title}/>
|
||||
<ul className="flex flex-col gap-2">
|
||||
{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>
|
||||
</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 s01 from '@/assets/header/solution/01.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 s08 from '@/assets/header/solution/08.svg'
|
||||
import {StaticImageData} from 'next/image'
|
||||
import {FragmentTitle} from '@/app/(home)/@header/common'
|
||||
|
||||
export default function SolutionMenu() {
|
||||
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
|
||||
icon={s01}
|
||||
title="数据抓取"
|
||||
@@ -65,15 +65,12 @@ function SolutionItem(props: {
|
||||
return (
|
||||
<div
|
||||
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`,
|
||||
].join(' ')}
|
||||
>
|
||||
<Image src={props.icon} alt={props.title} className="w-10 h-10"/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="font-bold">{props.title}</h3>
|
||||
<p className="text-gray-400 text-sm">{props.desc}</p>
|
||||
</div>
|
||||
<FragmentTitle img={props.icon} text={props.title}/>
|
||||
<p className="text-gray-400 text-sm">{props.desc}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +1,36 @@
|
||||
'use client'
|
||||
import UserCenter from '@/components/composites/user-center'
|
||||
import {useClientStore, useProfileStore} from '@/app/stores'
|
||||
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 {useCallback, useEffect, useMemo, useState, PointerEvent, ComponentProps} from 'react'
|
||||
import Link from 'next/link'
|
||||
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 ProductMenu, {Domestic} from '@/app/(home)/@header/_client/product'
|
||||
import SolutionMenu from '@/app/(home)/@header/_client/solution'
|
||||
import HelpMenu from '@/app/(home)/@header/_client/help'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {useProfileStore} from '@/components/stores-provider'
|
||||
import UserCenter from '@/components/composites/user-center'
|
||||
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)
|
||||
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 () => {
|
||||
@@ -42,10 +39,57 @@ export default function Header(props: HeaderProps) {
|
||||
}, [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
|
||||
@@ -53,102 +97,188 @@ export default function Header(props: HeaderProps) {
|
||||
|
||||
return (
|
||||
<header
|
||||
data-expand={expand}
|
||||
data-scroll={scroll}
|
||||
data-effect={expand || scroll}
|
||||
className="group/header w-full fixed left-0 top-0 z-20"
|
||||
>
|
||||
<Navigation
|
||||
onValueChange={(value) => {
|
||||
setExpand(!!value)
|
||||
}}
|
||||
>
|
||||
className={merge(
|
||||
'fixed top-0 left-0 w-screen z-10 flex flex-col',
|
||||
menu && 'h-screen',
|
||||
)}>
|
||||
<HeaderContext.Provider value={{setMenu}}>
|
||||
|
||||
{/* 菜单栏 */}
|
||||
<div className={merge(
|
||||
`transition-[background-color,backdrop-filter,box-shadow] duration-200 ease-in-out`,
|
||||
`bg-transparent backdrop-blur-none shadow-none`,
|
||||
`group-data-[effect=true]/header:bg-card/90`,
|
||||
`group-data-[effect=true]/header:backdrop-blur-sm`,
|
||||
`group-data-[scroll=true]/header:shadow-lg`,
|
||||
`flex-none`,
|
||||
`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`,
|
||||
)}>
|
||||
<Wrap className={merge('h-14 md:h-16 flex justify-between items-stretch')}>
|
||||
<div className="flex items-stretch lg:flex-row-reverse">
|
||||
{lg ? (
|
||||
<NavigationGroup className="gap-0 flex">
|
||||
<NavigationLinkItem href="/" text="首页"/>
|
||||
<Wrap className="h-20 max-md:h-16 flex justify-between">
|
||||
<nav className="flex items-center justify-between lg:flex-row-reverse gap-4 lg:gap-8">
|
||||
|
||||
<NavigationTriggerItem text="产品订购" className="h-80">
|
||||
<ProductMenu/>
|
||||
</NavigationTriggerItem>
|
||||
{/* 桌面端菜单 */}
|
||||
<ul className="h-full items-stretch hidden lg:flex">
|
||||
<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/>
|
||||
</NavigationTriggerItem>
|
||||
{/* 移动端菜单 */}
|
||||
<Button
|
||||
theme="ghost"
|
||||
className="lg:hidden size-10"
|
||||
onPointerEnter={enterMenu(3)}
|
||||
onPointerLeave={exitMenu}
|
||||
>
|
||||
<MenuIcon/>
|
||||
</Button>
|
||||
|
||||
<NavigationTriggerItem text="帮助中心" className="h-80">
|
||||
<HelpMenu/>
|
||||
</NavigationTriggerItem>
|
||||
{/* logo */}
|
||||
<Link href="/" className="flex items-center">
|
||||
<Image src={logo} alt="logo" height={40} className="translate-y-0.5"/>
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<NavigationLinkItem href="/" text="企业服务"/>
|
||||
<NavigationLinkItem href="/" text="推广返利"/>
|
||||
|
||||
<NavigationIndicator/>
|
||||
</NavigationGroup>
|
||||
) : (
|
||||
<NavigationGroup className="flex">
|
||||
<NavigationTriggerItem
|
||||
suffix={false}
|
||||
text={<MenuIcon/>}
|
||||
className={merge(
|
||||
`flex flex-col items-start gap-6`,
|
||||
)}
|
||||
>
|
||||
<ProductMenu/>
|
||||
|
||||
<SolutionMenu/>
|
||||
|
||||
<HelpMenu/>
|
||||
|
||||
<NavigationLink href="/" text="企业服务"/>
|
||||
<NavigationLink href="/" text="推广返利"/>
|
||||
</NavigationTriggerItem>
|
||||
</NavigationGroup>
|
||||
)}
|
||||
|
||||
<NavigationLink href="/" text={<Image src={logo} alt="logo" height={36}/>}/>
|
||||
{/* 登录 */}
|
||||
<div className="flex items-center">
|
||||
{profile == undefined
|
||||
? (
|
||||
<>
|
||||
<Link
|
||||
href="/login"
|
||||
className="w-24 h-12 flex items-center justify-center lg:text-lg"
|
||||
>
|
||||
<span>登录</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/login"
|
||||
className={[
|
||||
`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`,
|
||||
`from-blue-500 to-cyan-400 hover:from-blue-500 hover:to-cyan-300`,
|
||||
].join(' ')}
|
||||
>
|
||||
<span>注册</span>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<UserCenter profile={profile}/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<AccountRegion/>
|
||||
</Wrap>
|
||||
</div>
|
||||
|
||||
<NavigationMenuViewport className={merge(
|
||||
`bg-card/90 backdrop-blur-sm shadow-lg`,
|
||||
`transition-[padding,opacity,max-height]`,
|
||||
`group-data-[expand=false]/header:delay-[0s,0s,0.2s] group-data-[expand=true]/header:delay-0`,
|
||||
`p-0 group-data-[expand=true]/header:py-4`,
|
||||
`opacity-0 group-data-[effect=true]/header:opacity-100`,
|
||||
`max-h-0 group-data-[effect=true]/header:max-h-[calc(100vh-56px)] overflow-auto`,
|
||||
)}/>
|
||||
</Navigation>
|
||||
{/* 下拉菜单 */}
|
||||
<div
|
||||
className={merge(
|
||||
`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
|
||||
? `delay-[0s,0s] opacity-100 py-4`
|
||||
: `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>
|
||||
)
|
||||
}
|
||||
|
||||
function AccountRegion() {
|
||||
const profile = useProfileStore(state => state.profile)
|
||||
|
||||
function LinkItem(props: {
|
||||
text: string
|
||||
href: string
|
||||
}) {
|
||||
return (
|
||||
<div className="self-center">
|
||||
{profile ? (
|
||||
<UserCenter profile={profile}/>
|
||||
) : (
|
||||
<NavigationLink
|
||||
href="/login"
|
||||
text="登录 / 注册"
|
||||
classNameOverride={buttonVariants({
|
||||
theme: 'gradient',
|
||||
className: 'h-10 lg:h-12',
|
||||
})}/>
|
||||
)}
|
||||
</div>
|
||||
<li className="group relative">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
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'
|
||||
import {PanelLeftCloseIcon, PanelLeftOpenIcon} from 'lucide-react'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {useLayoutStore} from '@/app/stores'
|
||||
import {useLayoutStore} from '@/components/stores-provider'
|
||||
import {merge} from '@/lib/utils'
|
||||
import UserCenter from '@/components/composites/user-center'
|
||||
import {User} from '@/lib/models'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import {ReactNode} from 'react'
|
||||
import {useLayoutStore} from '@/app/stores'
|
||||
import {useLayoutStore} from '@/components/stores-provider'
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
type AdminLayoutProps = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import {ComponentProps, ReactNode, useState} from 'react'
|
||||
import {merge} from '@/lib/utils'
|
||||
import {useLayoutStore} from '@/app/stores'
|
||||
import {useLayoutStore} from '@/components/stores-provider'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import logoAvatar from '../_assets/logo-avatar.svg'
|
||||
|
||||
@@ -11,7 +11,7 @@ import {Identify} from '@/actions/user'
|
||||
import {toast} from 'sonner'
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import * as qrcode from 'qrcode'
|
||||
import {useProfileStore} from '@/app/stores'
|
||||
import {useProfileStore} from '@/components/stores-provider'
|
||||
import {merge} from '@/lib/utils'
|
||||
import banner from './_assets/banner.webp'
|
||||
import personal from './_assets/personal.webp'
|
||||
|
||||
@@ -7,7 +7,7 @@ import {Button} from '@/components/ui/button'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import * as z from 'zod'
|
||||
import {useProfileStore} from '@/app/stores'
|
||||
import {useProfileStore} from '@/components/stores-provider'
|
||||
import {toast} from 'sonner'
|
||||
import {CheckCircle, QrCodeIcon} from 'lucide-react'
|
||||
import * as qrcode from 'qrcode'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import {ReactNode, useEffect} from 'react'
|
||||
import {useClientStore, useLayoutStore} from '@/app/stores'
|
||||
import {useClientStore, useLayoutStore} from '@/components/stores-provider'
|
||||
|
||||
export type EffectProviderProps = {
|
||||
children?: ReactNode
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Metadata} from 'next'
|
||||
import './globals.css'
|
||||
import localFont from 'next/font/local'
|
||||
import {Toaster} from '@/components/ui/sonner'
|
||||
import Stores from '@/app/stores'
|
||||
import StoresProvider from '@/components/stores-provider'
|
||||
import {getProfile} from '@/actions/auth'
|
||||
import Effects from '@/app/effects'
|
||||
|
||||
@@ -29,11 +29,11 @@ export default async function RootLayout({
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className={`${font.className}`}>
|
||||
<Stores user={user}>
|
||||
<StoresProvider user={user}>
|
||||
<Effects>
|
||||
{children}
|
||||
</Effects>
|
||||
</Stores>
|
||||
</StoresProvider>
|
||||
<Toaster position="top-center" richColors expand/>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,7 +8,7 @@ import Image from 'next/image'
|
||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||
import wechat from '@/components/composites/purchase/_assets/wechat.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 Pay from '@/components/composites/purchase/pay'
|
||||
import {buttonVariants} from '@/components/ui/button'
|
||||
|
||||
@@ -6,7 +6,7 @@ import wechat from './_assets/wechat.svg'
|
||||
import balance from './_assets/balance.svg'
|
||||
import Image from 'next/image'
|
||||
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 {ApiResponse, ExtraResp, ExtraReq} from '@/lib/api'
|
||||
import {toast} from 'sonner'
|
||||
|
||||
@@ -8,7 +8,7 @@ import Image from 'next/image'
|
||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||
import wechat from '@/components/composites/purchase/_assets/wechat.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 {buttonVariants} from '@/components/ui/button'
|
||||
import Link from 'next/link'
|
||||
|
||||
@@ -19,7 +19,7 @@ import {useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {Loader} from 'lucide-react'
|
||||
import {RechargeComplete, RechargePrepare} from '@/actions/user'
|
||||
import * as qrcode from 'qrcode'
|
||||
import {useProfileStore} from '@/app/stores'
|
||||
import {useProfileStore} from '@/components/stores-provider'
|
||||
import {merge} from '@/lib/utils'
|
||||
import {
|
||||
Platform,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import {useProfileStore} from '@/app/stores'
|
||||
import {useProfileStore} from '@/components/stores-provider'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar'
|
||||
import {LoaderIcon, LogOutIcon, UserIcon, UserPenIcon} from 'lucide-react'
|
||||
|
||||
@@ -42,7 +42,7 @@ export type ProfileProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function Stores(props: ProfileProviderProps) {
|
||||
export default function StoresProvider(props: ProfileProviderProps) {
|
||||
const profile = useRef<StoreApi<ProfileStore>>(null)
|
||||
if (!profile.current) {
|
||||
console.log('📦 create profile store')
|
||||
Reference in New Issue
Block a user