实现响应式导航栏组件
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.14",
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||||
"@radix-ui/react-popover": "^1.1.7",
|
"@radix-ui/react-popover": "^1.1.7",
|
||||||
"@radix-ui/react-progress": "^1.1.3",
|
"@radix-ui/react-progress": "^1.1.3",
|
||||||
"@radix-ui/react-radio-group": "^1.2.3",
|
"@radix-ui/react-radio-group": "^1.2.3",
|
||||||
@@ -64,9 +65,9 @@
|
|||||||
"eslint-config-next": "15.2.1",
|
"eslint-config-next": "15.2.1",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"rehype-highlight": "^7.0.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5",
|
"typescript": "^5"
|
||||||
"rehype-highlight": "^7.0.2"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b",
|
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|||||||
75
pnpm-lock.yaml
generated
75
pnpm-lock.yaml
generated
@@ -44,6 +44,9 @@ importers:
|
|||||||
'@radix-ui/react-label':
|
'@radix-ui/react-label':
|
||||||
specifier: ^2.1.2
|
specifier: ^2.1.2
|
||||||
version: 2.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 2.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
'@radix-ui/react-navigation-menu':
|
||||||
|
specifier: ^1.2.13
|
||||||
|
version: 1.2.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
'@radix-ui/react-popover':
|
'@radix-ui/react-popover':
|
||||||
specifier: ^1.1.7
|
specifier: ^1.1.7
|
||||||
version: 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
@@ -869,6 +872,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-navigation-menu@1.2.13':
|
||||||
|
resolution: {integrity: sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-popover@1.1.7':
|
'@radix-ui/react-popover@1.1.7':
|
||||||
resolution: {integrity: sha512-I38OYWDmJF2kbO74LX8UsFydSHWOJuQ7LxPnTefjxxvdvPLempvAnmsyX9UsBlywcbSGpRH7oMLfkUf+ij4nrw==}
|
resolution: {integrity: sha512-I38OYWDmJF2kbO74LX8UsFydSHWOJuQ7LxPnTefjxxvdvPLempvAnmsyX9UsBlywcbSGpRH7oMLfkUf+ij4nrw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1338,6 +1354,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-use-previous@1.1.1':
|
||||||
|
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-rect@1.1.0':
|
'@radix-ui/react-use-rect@1.1.0':
|
||||||
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1400,6 +1425,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-visually-hidden@1.2.3':
|
||||||
|
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/rect@1.1.0':
|
'@radix-ui/rect@1.1.0':
|
||||||
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
|
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
|
||||||
|
|
||||||
@@ -4188,6 +4226,28 @@ snapshots:
|
|||||||
'@types/react': 19.0.10
|
'@types/react': 19.0.10
|
||||||
'@types/react-dom': 19.0.4(@types/react@19.0.10)
|
'@types/react-dom': 19.0.4(@types/react@19.0.10)
|
||||||
|
|
||||||
|
'@radix-ui/react-navigation-menu@1.2.13(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.2
|
||||||
|
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0)
|
||||||
|
'@radix-ui/react-direction': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||||
|
'@radix-ui/react-presence': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0)
|
||||||
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||||
|
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.0.10)(react@19.0.0)
|
||||||
|
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.0.10
|
||||||
|
'@types/react-dom': 19.0.4(@types/react@19.0.10)
|
||||||
|
|
||||||
'@radix-ui/react-popover@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
'@radix-ui/react-popover@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.2
|
'@radix-ui/primitive': 1.1.2
|
||||||
@@ -4650,6 +4710,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.10
|
'@types/react': 19.0.10
|
||||||
|
|
||||||
|
'@radix-ui/react-use-previous@1.1.1(@types/react@19.0.10)(react@19.0.0)':
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.0.10
|
||||||
|
|
||||||
'@radix-ui/react-use-rect@1.1.0(@types/react@19.0.10)(react@19.0.0)':
|
'@radix-ui/react-use-rect@1.1.0(@types/react@19.0.10)(react@19.0.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/rect': 1.1.0
|
'@radix-ui/rect': 1.1.0
|
||||||
@@ -4696,6 +4762,15 @@ snapshots:
|
|||||||
'@types/react': 19.0.10
|
'@types/react': 19.0.10
|
||||||
'@types/react-dom': 19.0.4(@types/react@19.0.10)
|
'@types/react-dom': 19.0.4(@types/react@19.0.10)
|
||||||
|
|
||||||
|
'@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.0.10
|
||||||
|
'@types/react-dom': 19.0.4(@types/react@19.0.10)
|
||||||
|
|
||||||
'@radix-ui/rect@1.1.0': {}
|
'@radix-ui/rect@1.1.0': {}
|
||||||
|
|
||||||
'@radix-ui/rect@1.1.1': {}
|
'@radix-ui/rect@1.1.1': {}
|
||||||
|
|||||||
@@ -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 '@/components/providers/StoreProvider'
|
import {useProfileStore} from '@/app/stores'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export type LoginPageProps = {}
|
export type LoginPageProps = {}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import banner from '@/assets/header/help/banner.webp'
|
|||||||
|
|
||||||
export default function HelpMenu() {
|
export default function HelpMenu() {
|
||||||
return (
|
return (
|
||||||
<Wrap className="w-full grid grid-cols-4 gap-4 justify-items-center h-75">
|
<Wrap className="w-full grid grid-cols-3 lg:grid-cols-4 gap-4 justify-items-start">
|
||||||
<Column
|
<Column
|
||||||
icon={h01}
|
icon={h01}
|
||||||
title="提取IP"
|
title="提取IP"
|
||||||
@@ -35,7 +35,7 @@ export default function HelpMenu() {
|
|||||||
{lead: '行业资讯', href: '#'},
|
{lead: '行业资讯', href: '#'},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Image src={banner} alt="banner" className=""/>
|
<Image src={banner} alt="banner" className="hidden lg:block"/>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function LinkItem(props: {
|
|||||||
href: string
|
href: string
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<li className="group relative">
|
<li className="group relative flex-none">
|
||||||
<Link
|
<Link
|
||||||
href={props.href}
|
href={props.href}
|
||||||
className={[
|
className={[
|
||||||
@@ -36,7 +36,7 @@ export function MenuItem(props: {
|
|||||||
onTouchStart?: (e: React.TouchEvent) => void
|
onTouchStart?: (e: React.TouchEvent) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<li className="group relative">
|
<li className="group relative flex-none">
|
||||||
<button
|
<button
|
||||||
onPointerEnter={props.onEnter}
|
onPointerEnter={props.onEnter}
|
||||||
onPointerLeave={props.onLeave}
|
onPointerLeave={props.onLeave}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {ReactNode, useContext, useState, useEffect} from 'react'
|
import {ReactNode, useState, useEffect, MouseEvent} from 'react'
|
||||||
import Wrap from '@/components/wrap'
|
import Wrap from '@/components/wrap'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import anno from '@/assets/header/product/anno.svg'
|
import anno from '@/assets/header/product/anno.svg'
|
||||||
@@ -7,7 +7,6 @@ import Link from 'next/link'
|
|||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import prod from '@/assets/header/product/prod.svg'
|
import prod from '@/assets/header/product/prod.svg'
|
||||||
import custom from '@/assets/header/product/custom.svg'
|
import custom from '@/assets/header/product/custom.svg'
|
||||||
import {HeaderContext} from '@/app/(home)/@header/_client/provider'
|
|
||||||
import {useSearchParams} from 'next/navigation'
|
import {useSearchParams} from 'next/navigation'
|
||||||
import {useRouter} from 'next/navigation'
|
import {useRouter} from 'next/navigation'
|
||||||
|
|
||||||
@@ -18,8 +17,6 @@ export function Tab(props: {
|
|||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const currentType = searchParams?.get('type') || 'short'
|
|
||||||
return (
|
return (
|
||||||
<li role="tab">
|
<li role="tab">
|
||||||
<button
|
<button
|
||||||
@@ -35,6 +32,53 @@ export function Tab(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function ProductMenu() {
|
||||||
|
const [type, setType] = useState<TabType>('domestic')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () => {
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMobile()
|
||||||
|
window.addEventListener('resize', checkMobile)
|
||||||
|
return () => window.removeEventListener('resize', checkMobile)
|
||||||
|
}, [])
|
||||||
|
return (
|
||||||
|
<Wrap className="flex">
|
||||||
|
<ul role="tablist" className="w-48">
|
||||||
|
<Tab selected={type === 'domestic'} onSelect={() => setType('domestic')}>国内代理</Tab>
|
||||||
|
<Tab selected={type === 'oversea'} onSelect={() => setType('oversea')}>海外代理</Tab>
|
||||||
|
</ul>
|
||||||
|
<div className="flex-1">
|
||||||
|
{type === 'domestic'
|
||||||
|
? (
|
||||||
|
<Domestic/>
|
||||||
|
) : (
|
||||||
|
<Oversea/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<aside className="hidden w-64 lg:block">
|
||||||
|
<h3 className="flex gap-3 items-center mb-4">
|
||||||
|
<Image src={anno} alt="公告" className="w-10 h-10"/>
|
||||||
|
<span>网站公告</span>
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p>官网最新上线,体验再升级!</p>
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
1.新增多样使用功能,新增多样使用
|
||||||
|
新增多样使用功能
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
2.新增多样使用功能,新增多样使用
|
||||||
|
新增多样使用功能
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</Wrap>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function Domestic(props: {}) {
|
export function Domestic(props: {}) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const currentType = searchParams?.get('type') || 'short'
|
const currentType = searchParams?.get('type') || 'short'
|
||||||
@@ -86,69 +130,6 @@ export function Domestic(props: {}) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Oversea(props: {}) {
|
|
||||||
return (
|
|
||||||
<section role="tabpanel">
|
|
||||||
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProductMenu() {
|
|
||||||
const [type, setType] = useState<TabType>('domestic')
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
useEffect(() => {
|
|
||||||
const urlType = searchParams?.get('type')
|
|
||||||
console.log('URL参数:', urlType)
|
|
||||||
}, [searchParams])
|
|
||||||
// 响应式布局
|
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkMobile = () => {
|
|
||||||
setIsMobile(window.innerWidth <= 1024)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkMobile()
|
|
||||||
window.addEventListener('resize', checkMobile)
|
|
||||||
return () => window.removeEventListener('resize', checkMobile)
|
|
||||||
}, [])
|
|
||||||
return (
|
|
||||||
<Wrap className="flex h-75">
|
|
||||||
<ul role="tablist" className="w-48">
|
|
||||||
<Tab selected={type === 'domestic'} onSelect={() => setType('domestic')}>国内代理</Tab>
|
|
||||||
<Tab selected={type === 'oversea'} onSelect={() => setType('oversea')}>海外代理</Tab>
|
|
||||||
</ul>
|
|
||||||
<div className="flex-1">
|
|
||||||
{type === 'domestic'
|
|
||||||
? (
|
|
||||||
<Domestic/>
|
|
||||||
) : (
|
|
||||||
<Oversea/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<aside className="w-64">
|
|
||||||
<h3 className="flex gap-3 items-center mb-4">
|
|
||||||
<Image src={anno} alt="公告" className="w-10 h-10"/>
|
|
||||||
<span>网站公告</span>
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p>官网最新上线,体验再升级!</p>
|
|
||||||
<p className="text-gray-400 text-sm">
|
|
||||||
1.新增多样使用功能,新增多样使用
|
|
||||||
新增多样使用功能
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-400 text-sm">
|
|
||||||
2.新增多样使用功能,新增多样使用
|
|
||||||
新增多样使用功能
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</Wrap>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DomesticLink(props: {
|
export function DomesticLink(props: {
|
||||||
label: string
|
label: string
|
||||||
desc: string
|
desc: string
|
||||||
@@ -156,22 +137,17 @@ export function DomesticLink(props: {
|
|||||||
discount: number
|
discount: number
|
||||||
active?: boolean
|
active?: boolean
|
||||||
}) {
|
}) {
|
||||||
const ctx = useContext(HeaderContext)
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
if (!ctx) {
|
// const ctx = useContext(HeaderContext)
|
||||||
throw new Error(`HeaderContext not found`)
|
// if (!ctx) {
|
||||||
}
|
// throw new Error(`HeaderContext not found`)
|
||||||
|
|
||||||
// const onClick = () => {
|
|
||||||
// ctx.setMenu(false)
|
|
||||||
// }
|
// }
|
||||||
const onClick = (e: React.MouseEvent) => {
|
|
||||||
|
const onClick = (e: MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
ctx.setMenu(false)
|
// ctx.setMenu(false)
|
||||||
setTimeout(() => {
|
|
||||||
router.push(props.href)
|
router.push(props.href)
|
||||||
}, ctx.isMobile ? 100 : 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -197,3 +173,11 @@ export function DomesticLink(props: {
|
|||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Oversea(props: {}) {
|
||||||
|
return (
|
||||||
|
<section role="tabpanel">
|
||||||
|
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import HelpMenu from './help'
|
|||||||
import Wrap from '@/components/wrap'
|
import Wrap from '@/components/wrap'
|
||||||
import logo from '@/assets/logo.webp'
|
import logo from '@/assets/logo.webp'
|
||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
import {useProfileStore} from '@/components/providers/StoreProvider'
|
import {useProfileStore} from '@/app/stores'
|
||||||
import UserCenter from '@/components/composites/user-center'
|
import UserCenter from '@/components/composites/user-center'
|
||||||
import {MenuIcon} from 'lucide-react'
|
import {MenuIcon} from 'lucide-react'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
@@ -26,7 +26,6 @@ export default function Provider(props: ProviderProps) {
|
|||||||
// 滚动条状态
|
// 滚动条状态
|
||||||
// ======================
|
// ======================
|
||||||
const [scroll, setScroll] = useState(false) // Changed to false for client-side rendering
|
const [scroll, setScroll] = useState(false) // Changed to false for client-side rendering
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) // 控制移动端菜单展开/收起
|
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
setScroll(window.scrollY > 48)
|
setScroll(window.scrollY > 48)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -47,12 +46,8 @@ export default function Provider(props: ProviderProps) {
|
|||||||
const [menu, setMenu] = useState(false)
|
const [menu, setMenu] = useState(false)
|
||||||
const [page, setPage] = useState(0)
|
const [page, setPage] = useState(0)
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
// 屏幕1024时
|
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
|
||||||
// 控制产品订购下拉菜单的展开/收起
|
|
||||||
const [productDropdownOpen, setProductDropdownOpen] = useState(false)
|
const [productDropdownOpen, setProductDropdownOpen] = useState(false)
|
||||||
|
|
||||||
// 点击外部关闭菜单
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
|
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
|
||||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||||
@@ -72,8 +67,13 @@ export default function Provider(props: ProviderProps) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const isMobile = () => {
|
||||||
|
if (typeof window === 'undefined') return false
|
||||||
|
return window.innerWidth <= 768
|
||||||
|
}
|
||||||
|
|
||||||
const handleMenuEnter = (pageIndex: number) => {
|
const handleMenuEnter = (pageIndex: number) => {
|
||||||
if (isMobile && menu && page === pageIndex) {
|
if (isMobile() && menu && page === pageIndex) {
|
||||||
setMenu(false)
|
setMenu(false)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -82,17 +82,22 @@ export default function Provider(props: ProviderProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMenuLeave = useCallback(() => {
|
const handleMenuLeave = () => {
|
||||||
if (!isMobile) {
|
if (!isMobile()) {
|
||||||
setMenu(false)
|
setMenu(false)
|
||||||
}
|
}
|
||||||
}, [isMobile])
|
}
|
||||||
|
|
||||||
const pages = useMemo(() => [
|
const pages = useMemo(() => [
|
||||||
<ProductMenu key="product"/>,
|
<ProductMenu key="product"/>,
|
||||||
<SolutionMenu key="solution"/>,
|
<SolutionMenu key="solution"/>,
|
||||||
<HelpMenu key="help"/>,
|
<HelpMenu key="help"/>,
|
||||||
], [])
|
], [])
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
setMenu(!menu)
|
||||||
|
}
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// 用户信息
|
// 用户信息
|
||||||
// ======================
|
// ======================
|
||||||
@@ -103,43 +108,27 @@ export default function Provider(props: ProviderProps) {
|
|||||||
// render
|
// render
|
||||||
// ======================
|
// ======================
|
||||||
|
|
||||||
// 屏幕1024时响应式处理
|
|
||||||
useEffect(() => {
|
|
||||||
const checkMobile = () => {
|
|
||||||
const mobile = window.innerWidth <= 1024
|
|
||||||
setIsMobile(mobile)
|
|
||||||
// 移除自动关闭菜单的逻辑,让交互逻辑处理菜单状态
|
|
||||||
}
|
|
||||||
checkMobile()
|
|
||||||
window.addEventListener('resize', checkMobile)
|
|
||||||
return () => window.removeEventListener('resize', checkMobile)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 切换移动菜单
|
|
||||||
const toggleMobileMenu = () => {
|
|
||||||
setMobileMenuOpen(!mobileMenuOpen)
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<HeaderContext.Provider value={{setMenu, isMobile}}>
|
<HeaderContext.Provider value={{setMenu, isMobile: isMobile()}}>
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={[
|
className={[
|
||||||
`transition-[background, shadow] duration-200 ease-in-out`,
|
`transition-[background, shadow] duration-200 ease-in-out`,
|
||||||
menu || mobileMenuOpen || productDropdownOpen
|
menu
|
||||||
? `bg-[#fffe] backdrop-blur-sm`
|
? `bg-[#fffe] backdrop-blur-sm`
|
||||||
: scroll
|
: scroll
|
||||||
? `bg-[#fffe] backdrop-blur-sm shadow-lg`
|
? `bg-[#fffe] backdrop-blur-sm shadow-lg`
|
||||||
: `bg-transparent shadow-none`,
|
: `bg-transparent shadow-none`,
|
||||||
].join(' ')}>
|
].join(' ')}>
|
||||||
<Wrap className="h-20 flex justify-between items-center">
|
<Wrap className="h-14 lg:h-16 flex justify-between items-stretch">
|
||||||
<div className="flex justify-between items-center gap-2 lg:gap-8 h-9 lg:h-auto max-lg:flex-row-reverse lg:items-stretch">
|
<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 */}
|
{/* logo */}
|
||||||
<Link href="/" className="flex items-center">
|
<Link href="/" className="self-center">
|
||||||
<Image src={logo} alt="logo" height={40} className="translate-y-0.5"/>
|
<Image src={logo} alt="logo" height={36}/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* 菜单 */}
|
{/* 菜单 */}
|
||||||
<nav className="flex flex-col items-center">
|
<nav className="flex flex-col">
|
||||||
<Button
|
<Button
|
||||||
theme="ghost"
|
theme="ghost"
|
||||||
className="w-9 h-9 lg:hidden"
|
className="w-9 h-9 lg:hidden"
|
||||||
@@ -150,13 +139,8 @@ export default function Provider(props: ProviderProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
<ul
|
<ul
|
||||||
className={merge(
|
className={merge(
|
||||||
'h-full items-stretch max-lg:h-auto relative',
|
`h-full flex items-stretch`,
|
||||||
'max-lg:absolute max-lg:top-full max-lg:left-0 max-lg:w-screen',
|
'max-lg:absolute max-lg:top-full max-lg:left-0 max-lg:w-screen max-lg:h-12 max-lg:overflow-auto',
|
||||||
'max-lg:bg-[#fffe] max-lg:backdrop-blur-sm',
|
|
||||||
mobileMenuOpen
|
|
||||||
? 'max-lg:flex max-lg:flex-col max-lg:max-h-[500px] max-lg:py-4 max-lg:opacity-100'
|
|
||||||
: 'max-lg:hidden max-lg:max-h-0 max-lg:py-0 max-lg:opacity-0',
|
|
||||||
'lg:flex lg:items-stretch ',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<LinkItem text="首页" href="/"/>
|
<LinkItem text="首页" href="/"/>
|
||||||
@@ -167,34 +151,25 @@ export default function Provider(props: ProviderProps) {
|
|||||||
onEnter={() => handleMenuEnter(0)}
|
onEnter={() => handleMenuEnter(0)}
|
||||||
onLeave={handleMenuLeave}
|
onLeave={handleMenuLeave}
|
||||||
onTouchStart={() => handleMenuEnter(0)}
|
onTouchStart={() => handleMenuEnter(0)}
|
||||||
>
|
/>
|
||||||
{/* {!isMobile && !menu && <ProductMenu/>} */}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
text="业务场景"
|
text="业务场景"
|
||||||
active={menu && page === 1}
|
active={menu && page === 1}
|
||||||
onEnter={() => handleMenuEnter(1)}
|
onEnter={() => handleMenuEnter(1)}
|
||||||
onLeave={handleMenuLeave}
|
onLeave={handleMenuLeave}
|
||||||
// onTouchStart={() => handleMenuEnter(1)}
|
/>
|
||||||
>
|
|
||||||
{/* {!isMobile && !menu && <SolutionMenu/>} */}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
text="帮助中心"
|
text="帮助中心"
|
||||||
active={menu && page === 2}
|
active={menu && page === 2}
|
||||||
onEnter={() => handleMenuEnter(2)}
|
onEnter={() => handleMenuEnter(2)}
|
||||||
onLeave={handleMenuLeave}
|
onLeave={handleMenuLeave}
|
||||||
// onTouchStart={() => handleMenuEnter(2)}
|
/>
|
||||||
>
|
|
||||||
{/* {!isMobile && !menu && <HelpMenu/>} */}
|
|
||||||
</MenuItem>
|
|
||||||
<LinkItem
|
<LinkItem
|
||||||
text="企业服务"
|
text="企业服务"
|
||||||
href="#"/>
|
href="#"/>
|
||||||
<LinkItem
|
<LinkItem
|
||||||
text="推广返利"
|
text="推广返利"
|
||||||
href="#"/>
|
href="#"/>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,7 +197,7 @@ export default function Provider(props: ProviderProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<UserCenter/>
|
<div></div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -234,16 +209,16 @@ export default function Provider(props: ProviderProps) {
|
|||||||
className={[
|
className={[
|
||||||
`shadow-lg`,
|
`shadow-lg`,
|
||||||
`overflow-hidden bg-[#fffe] backdrop-blur-sm`,
|
`overflow-hidden bg-[#fffe] backdrop-blur-sm`,
|
||||||
`transition-all duration-200 ease-in-out`,
|
`transition-[opacity,padding,height] transition-discrete duration-200 ease-in-out`,
|
||||||
menu
|
menu
|
||||||
? `opacity-100 py-8 h-auto visible`
|
? `delay-[0s,0s,0s] opacity-100 py-8 h-auto`
|
||||||
: `opacity-0 py-0 h-0 invisible`,
|
: `delay-[0s,0s,0.2s] opacity-0 py-0 h-0`,
|
||||||
isMobile
|
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)]`
|
? `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`,
|
: `absolute top-full left-0 right-0`,
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
onPointerEnter={() => !isMobile && setMenu(true)}
|
onPointerEnter={() => !isMobile() && setMenu(true)}
|
||||||
onPointerLeave={() => !isMobile && setMenu(false)}
|
onPointerLeave={() => !isMobile() && setMenu(false)}
|
||||||
>
|
>
|
||||||
{pages[page]}
|
{pages[page]}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {StaticImageData} from 'next/image'
|
|||||||
|
|
||||||
export default function SolutionMenu() {
|
export default function SolutionMenu() {
|
||||||
return (
|
return (
|
||||||
<Wrap className="grid grid-cols-4 auto-rows-fr gap-4 h-75">
|
<Wrap className="grid grid-cols-2 lg:grid-cols-4 lg:auto-rows-fr gap-4">
|
||||||
<SolutionItem
|
<SolutionItem
|
||||||
icon={s01}
|
icon={s01}
|
||||||
title="数据抓取"
|
title="数据抓取"
|
||||||
@@ -65,7 +65,7 @@ function SolutionItem(props: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
`h-full p-4 flex gap-4 items-start rounded-md cursor-pointer`,
|
`h-full lg:p-4 flex gap-4 items-start rounded-md cursor-pointer`,
|
||||||
`transition-colors duration-200 hover:bg-blue-50`,
|
`transition-colors duration-200 hover:bg-blue-50`,
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,154 @@
|
|||||||
import Provider from '@/app/(home)/@header/_client/provider'
|
'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 Image from 'next/image'
|
||||||
|
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 {MenuIcon} from 'lucide-react'
|
||||||
|
|
||||||
export type HeaderProps = {}
|
export type HeaderProps = {}
|
||||||
|
|
||||||
export default async function Header(props: HeaderProps) {
|
export default function Header(props: HeaderProps) {
|
||||||
|
// ======================
|
||||||
|
// 背景显示状态
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
const [expand, setExpand] = useState(false)
|
||||||
|
const [scroll, setScroll] = useState(false)
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
setScroll(window.scrollY > 48)
|
||||||
|
}, [])
|
||||||
|
useEffect(() => {
|
||||||
|
setScroll(window.scrollY > 48)
|
||||||
|
window.addEventListener('scroll', handleScroll)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll)
|
||||||
|
}
|
||||||
|
}, [handleScroll])
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// 移动端
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
const lg = useClientStore(state => state.breakpoint.lg)
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// render
|
||||||
|
// ======================
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="fixed top-0 w-full z-10">
|
<header
|
||||||
<Provider/>
|
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)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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`,
|
||||||
|
)}>
|
||||||
|
<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="首页"/>
|
||||||
|
|
||||||
|
<NavigationTriggerItem text="产品订购" className="h-80">
|
||||||
|
<ProductMenu/>
|
||||||
|
</NavigationTriggerItem>
|
||||||
|
|
||||||
|
<NavigationTriggerItem text="业务场景" className="h-80">
|
||||||
|
<SolutionMenu/>
|
||||||
|
</NavigationTriggerItem>
|
||||||
|
|
||||||
|
<NavigationTriggerItem text="帮助中心" className="h-80">
|
||||||
|
<HelpMenu/>
|
||||||
|
</NavigationTriggerItem>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AccountRegion() {
|
||||||
|
const profile = useProfileStore(state => state.profile)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="self-center">
|
||||||
|
{profile ? (
|
||||||
|
<UserCenter profile={profile}/>
|
||||||
|
) : (
|
||||||
|
<NavigationLink
|
||||||
|
href="/login"
|
||||||
|
text="登录 / 注册"
|
||||||
|
classNameOverride={buttonVariants({
|
||||||
|
theme: 'gradient',
|
||||||
|
className: 'h-10 lg:h-12',
|
||||||
|
})}/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
'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 '@/components/providers/StoreProvider'
|
import {useLayoutStore} from '@/app/stores'
|
||||||
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'
|
||||||
|
|
||||||
export type HeaderProps = {}
|
export type HeaderProps = {
|
||||||
|
profile: User
|
||||||
|
}
|
||||||
|
|
||||||
export default function Header(props: HeaderProps) {
|
export default function Header(props: HeaderProps) {
|
||||||
const navbar = useLayoutStore(store => store.navbar)
|
const navbar = useLayoutStore(store => store.navbar)
|
||||||
@@ -34,7 +37,7 @@ export default function Header(props: HeaderProps) {
|
|||||||
|
|
||||||
{/* right */}
|
{/* right */}
|
||||||
<div className="flex-none flex items-center justify-end pr-4">
|
<div className="flex-none flex items-center justify-end pr-4">
|
||||||
<UserCenter/>
|
<UserCenter profile={props.profile}/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {ReactNode} from 'react'
|
import {ReactNode} from 'react'
|
||||||
import {useLayoutStore} from '@/components/providers/StoreProvider'
|
import {useLayoutStore} from '@/app/stores'
|
||||||
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 '@/components/providers/StoreProvider'
|
import {useLayoutStore} from '@/app/stores'
|
||||||
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 '@/components/providers/StoreProvider'
|
import {useProfileStore} from '@/app/stores'
|
||||||
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'
|
||||||
|
|||||||
@@ -2,16 +2,24 @@ import {ReactNode} from 'react'
|
|||||||
import Header from './_client/header'
|
import Header from './_client/header'
|
||||||
import Navbar from './_client/navbar'
|
import Navbar from './_client/navbar'
|
||||||
import Layout from './_client/layout'
|
import Layout from './_client/layout'
|
||||||
|
import {getProfile} from '@/actions/auth'
|
||||||
|
import {redirect} from 'next/navigation'
|
||||||
|
|
||||||
export type AdminLayoutProps = {
|
export type AdminLayoutProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function AdminLayout(props: AdminLayoutProps) {
|
export default async function AdminLayout(props: AdminLayoutProps) {
|
||||||
|
const resp = await getProfile()
|
||||||
|
const profile = resp.success ? resp.data : null
|
||||||
|
if (!profile) {
|
||||||
|
redirect('/login')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
navbar={<Navbar/>}
|
navbar={<Navbar/>}
|
||||||
header={<Header/>}
|
header={<Header profile={profile}/>}
|
||||||
content={props.children}
|
content={props.children}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 '@/components/providers/StoreProvider'
|
import {useProfileStore} from '@/app/stores'
|
||||||
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'
|
||||||
|
|||||||
45
src/app/effects.tsx
Normal file
45
src/app/effects.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
'use client'
|
||||||
|
import {ReactNode, useEffect} from 'react'
|
||||||
|
import {useClientStore, useLayoutStore} from '@/app/stores'
|
||||||
|
|
||||||
|
export type EffectProviderProps = {
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Effects(props: EffectProviderProps) {
|
||||||
|
const setNavbar = useLayoutStore(state => state.setNavbar)
|
||||||
|
const setBreakpoints = useClientStore(state => state.setBreakpoints)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sm = window.matchMedia(`(width >= 40rem)`)
|
||||||
|
const md = window.matchMedia(`(width >= 48rem)`)
|
||||||
|
const lg = window.matchMedia(`(width >= 64rem)`)
|
||||||
|
const xl = window.matchMedia(`(width >= 80rem)`)
|
||||||
|
|
||||||
|
setNavbar(md.matches)
|
||||||
|
setBreakpoints({
|
||||||
|
sm: sm.matches,
|
||||||
|
md: md.matches,
|
||||||
|
lg: lg.matches,
|
||||||
|
xl: xl.matches,
|
||||||
|
})
|
||||||
|
|
||||||
|
sm.addEventListener('change', (e) => {
|
||||||
|
setBreakpoints({sm: e.matches})
|
||||||
|
})
|
||||||
|
|
||||||
|
md.addEventListener('change', (e) => {
|
||||||
|
setBreakpoints({md: e.matches})
|
||||||
|
})
|
||||||
|
|
||||||
|
lg.addEventListener('change', (e) => {
|
||||||
|
setBreakpoints({lg: e.matches})
|
||||||
|
})
|
||||||
|
|
||||||
|
xl.addEventListener('change', (e) => {
|
||||||
|
setBreakpoints({xl: e.matches})
|
||||||
|
})
|
||||||
|
}, [setBreakpoints, setNavbar])
|
||||||
|
|
||||||
|
return props.children
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
--sidebar-ring: oklch(0.707 0.022 261.325);
|
--sidebar-ring: oklch(0.707 0.022 261.325);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 颜色 */
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--idle);
|
--color-background: var(--idle);
|
||||||
--color-foreground: var(--idle-text);
|
--color-foreground: var(--idle-text);
|
||||||
@@ -112,6 +113,33 @@
|
|||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 动画 */
|
||||||
|
@theme inline {
|
||||||
|
--default-transition-timing-function: ease-in-out;
|
||||||
|
--animate-fadein: fadein var(--default-transition-duration) var(--default-transition-timing-function);
|
||||||
|
--animate-fadeout: fadeout var(--default-transition-duration) var(--default-transition-timing-function);
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ 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 StoreProvider from '@/components/providers/StoreProvider'
|
import Stores from '@/app/stores'
|
||||||
import {getProfile} from '@/actions/auth'
|
import {getProfile} from '@/actions/auth'
|
||||||
|
import Effects from '@/app/effects'
|
||||||
|
|
||||||
const font = localFont({
|
const font = localFont({
|
||||||
src: './NotoSansSC-VariableFont_wght.ttf',
|
src: './NotoSansSC-VariableFont_wght.ttf',
|
||||||
@@ -28,9 +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}`}>
|
||||||
<StoreProvider user={user}>
|
<Stores user={user}>
|
||||||
|
<Effects>
|
||||||
{children}
|
{children}
|
||||||
</StoreProvider>
|
</Effects>
|
||||||
|
</Stores>
|
||||||
<Toaster position="top-center" richColors expand/>
|
<Toaster position="top-center" richColors expand/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,24 +1,48 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {User} from '@/lib/models'
|
import {User} from '@/lib/models'
|
||||||
import {createContext, ReactNode, useContext, useRef} from 'react'
|
import {createContext, ReactNode, useContext, useEffect, useRef} from 'react'
|
||||||
import {StoreApi} from 'zustand/vanilla'
|
import {StoreApi} from 'zustand/vanilla'
|
||||||
import {useStore} from 'zustand/react'
|
import {useStore} from 'zustand/react'
|
||||||
import {createProfileStore, ProfileStore} from '@/lib/stores/profile'
|
import {createProfileStore, ProfileStore} from '@/lib/stores/profile'
|
||||||
import {createLayoutStore, LayoutStore} from '@/lib/stores/layout'
|
import {createLayoutStore, LayoutStore} from '@/lib/stores/layout'
|
||||||
|
import {ClientStore, createClientStore} from '@/lib/stores/client'
|
||||||
|
|
||||||
export type StoreContextType = {
|
const StoreContext = createContext<{
|
||||||
profile: StoreApi<ProfileStore>
|
profile?: StoreApi<ProfileStore>
|
||||||
layout: StoreApi<LayoutStore>
|
layout?: StoreApi<LayoutStore>
|
||||||
|
client?: StoreApi<ClientStore>
|
||||||
|
}>({})
|
||||||
|
|
||||||
|
export function useProfileStore<T>(selector: (store: ProfileStore) => T) {
|
||||||
|
const profile = useContext(StoreContext).profile
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error('useProfileStore must be used within a StoreProvider')
|
||||||
|
}
|
||||||
|
return useStore(profile, selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StoreContext = createContext<StoreContextType | null>(null)
|
export function useLayoutStore<T>(selector: (store: LayoutStore) => T) {
|
||||||
|
const layout = useContext(StoreContext).layout
|
||||||
|
if (!layout) {
|
||||||
|
throw new Error('useLayoutStore must be used within a StoreProvider')
|
||||||
|
}
|
||||||
|
return useStore(layout, selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useClientStore<T>(selector: (store: ClientStore) => T) {
|
||||||
|
const client = useContext(StoreContext).client
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('useClientStore must be used within a StoreProvider')
|
||||||
|
}
|
||||||
|
return useStore(client, selector)
|
||||||
|
}
|
||||||
|
|
||||||
export type ProfileProviderProps = {
|
export type ProfileProviderProps = {
|
||||||
user: User | null
|
user: User | null
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StoreProvider(props: ProfileProviderProps) {
|
export default function Stores(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')
|
||||||
@@ -28,32 +52,22 @@ export default function StoreProvider(props: ProfileProviderProps) {
|
|||||||
const layout = useRef<StoreApi<LayoutStore>>(null)
|
const layout = useRef<StoreApi<LayoutStore>>(null)
|
||||||
if (!layout.current) {
|
if (!layout.current) {
|
||||||
console.log('📦 create layout store')
|
console.log('📦 create layout store')
|
||||||
const expand = window ? window.matchMedia(`(min-width: 1024px)`).matches : true
|
layout.current = createLayoutStore()
|
||||||
layout.current = createLayoutStore(expand)
|
}
|
||||||
|
|
||||||
|
const client = useRef<StoreApi<ClientStore>>(null)
|
||||||
|
if (!client.current) {
|
||||||
|
console.log('📦 create client store')
|
||||||
|
client.current = createClientStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoreContext.Provider value={{
|
<StoreContext.Provider value={{
|
||||||
profile: profile.current,
|
profile: profile.current,
|
||||||
layout: layout.current,
|
layout: layout.current,
|
||||||
|
client: client.current,
|
||||||
}}>
|
}}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</StoreContext.Provider>
|
</StoreContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProfileStore<T>(selector: (store: ProfileStore) => T) {
|
|
||||||
const ctx = useContext(StoreContext)
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('useProfileStore must be used within a StoreProvider')
|
|
||||||
}
|
|
||||||
return useStore(ctx.profile, selector)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useLayoutStore<T>(selector: (store: LayoutStore) => T) {
|
|
||||||
const ctx = useContext(StoreContext)
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error('useLayoutStore must be used within a StoreProvider')
|
|
||||||
}
|
|
||||||
return useStore(ctx.layout, selector)
|
|
||||||
}
|
|
||||||
54
src/app/test/page.tsx
Normal file
54
src/app/test/page.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import Wrap from '@/components/wrap'
|
||||||
|
import {NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport} from '@radix-ui/react-navigation-menu'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function TestPage() {
|
||||||
|
return (
|
||||||
|
<div className="fixed left-0 top-0 w-full">
|
||||||
|
<Wrap>
|
||||||
|
<NavigationMenu>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-1">
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<NavigationMenuList className="flex justify-around">
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/test">Test 0 </Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger>
|
||||||
|
Test 1
|
||||||
|
</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/test">Test 1 Content</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuTrigger>
|
||||||
|
Test 2
|
||||||
|
</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/test">Test 2 Content</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<Link href="/test">Test 3 </Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuIndicator className="w-full h-1 bg-green-500"/>
|
||||||
|
</NavigationMenuList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NavigationMenuViewport className="bg-blue-100"/>
|
||||||
|
</NavigationMenu>
|
||||||
|
</Wrap>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 '@/components/providers/StoreProvider'
|
import {useProfileStore} from '@/app/stores'
|
||||||
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 '@/components/providers/StoreProvider'
|
import {useProfileStore} from '@/app/stores'
|
||||||
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 '@/components/providers/StoreProvider'
|
import {useProfileStore} from '@/app/stores'
|
||||||
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'
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {useEffect, useMemo, useRef, useState} from 'react'
|
|||||||
import {Loader} from 'lucide-react'
|
import {Loader} from 'lucide-react'
|
||||||
import {RechargeByPay, RechargeByAlipayConfirm, RechargeByWechat, RechargeByWechatConfirm} from '@/actions/user'
|
import {RechargeByPay, RechargeByAlipayConfirm, RechargeByWechat, RechargeByWechatConfirm} from '@/actions/user'
|
||||||
import * as qrcode from 'qrcode'
|
import * as qrcode from 'qrcode'
|
||||||
import {useProfileStore} from '@/components/providers/StoreProvider'
|
import {useProfileStore} from '@/app/stores'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
Platform,
|
Platform,
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {useProfileStore} from '@/components/providers/StoreProvider'
|
import {useProfileStore} from '@/app/stores'
|
||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from '@/components/ui/dropdown-menu'
|
|
||||||
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar'
|
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar'
|
||||||
import {LogOutIcon, UserIcon, UserPenIcon} from 'lucide-react'
|
import {LoaderIcon, LogOutIcon, UserIcon, UserPenIcon} from 'lucide-react'
|
||||||
import {usePathname, useRouter} from 'next/navigation'
|
import {usePathname, useRouter} from 'next/navigation'
|
||||||
import {logout} from '@/actions/auth'
|
import {logout} from '@/actions/auth'
|
||||||
import {useState} from 'react'
|
|
||||||
import {HoverCard, HoverCardContent, HoverCardTrigger} from '@/components/ui/hover-card'
|
import {HoverCard, HoverCardContent, HoverCardTrigger} from '@/components/ui/hover-card'
|
||||||
|
import {User} from '@/lib/models'
|
||||||
|
|
||||||
export default function UserCenter() {
|
type UserCenterProps = {
|
||||||
|
profile: User
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserCenter(props: UserCenterProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// 登录控制
|
// 登录控制
|
||||||
const profile = useProfileStore(store => store.profile)
|
|
||||||
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||||
const doLogout = async () => {
|
const doLogout = async () => {
|
||||||
const resp = await logout()
|
const resp = await logout()
|
||||||
@@ -31,13 +33,7 @@ export default function UserCenter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return !profile ? (
|
return (
|
||||||
<Button
|
|
||||||
theme="fail"
|
|
||||||
onClick={() => router.push('/login')}>
|
|
||||||
去登录
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<HoverCard openDelay={150} closeDelay={150}>
|
<HoverCard openDelay={150} closeDelay={150}>
|
||||||
<HoverCardTrigger>
|
<HoverCardTrigger>
|
||||||
<Button
|
<Button
|
||||||
@@ -46,10 +42,10 @@ export default function UserCenter() {
|
|||||||
onClick={toAdminPage}
|
onClick={toAdminPage}
|
||||||
>
|
>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={profile.avatar} alt="avatar"/>
|
<AvatarImage src={props.profile.avatar} alt="avatar"/>
|
||||||
<AvatarFallback className="bg-primary-muted"><UserIcon/></AvatarFallback>
|
<AvatarFallback className="bg-primary-muted"><UserIcon/></AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<span>{profile.name}</span>
|
<span>{props.profile.name}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="w-36 p-1" align="end">
|
<HoverCardContent className="w-36 p-1" align="end">
|
||||||
|
|||||||
@@ -4,21 +4,22 @@ import {cva, VariantProps} from 'class-variance-authority'
|
|||||||
|
|
||||||
export const buttonVariants = cva(
|
export const buttonVariants = cva(
|
||||||
[
|
[
|
||||||
`transition-all duration-200 ease-in-out`,
|
`transition-all duration-200 ease-in-out`, // 过渡动画
|
||||||
`h-10 px-4 rounded-md cursor-pointer whitespace-nowrap`,
|
`h-10 px-4 rounded-md cursor-pointer whitespace-nowrap`, // 样式
|
||||||
'inline-flex items-center justify-center gap-2',
|
'outline-none focus-visible:ring-4 ring-blue-200', // 焦点样式
|
||||||
|
'disabled:pointer-events-none disabled:opacity-50 ', // 禁用样式
|
||||||
|
'aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail', // 无效状态样式
|
||||||
|
'inline-flex items-center justify-center gap-2', // 布局
|
||||||
'[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0 ',
|
'[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0 ',
|
||||||
'outline-none focus-visible:ring-4 ring-blue-200',
|
|
||||||
'disabled:pointer-events-none disabled:opacity-50 ',
|
|
||||||
'aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail',
|
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
theme: {
|
theme: {
|
||||||
gradient: 'bg-gradient-to-r from-blue-400 to-cyan-300 text-white ring-offset-2',
|
gradient: 'bg-gradient-to-r from-blue-400 to-cyan-300 text-white ring-offset-2 hover:from-blue-500 hover:to-cyan-400',
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
outline: 'border bg-background hover:bg-secondary hover:text-secondary-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
outline: 'border bg-background hover:bg-secondary hover:text-secondary-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||||
ghost: 'text-foreground bg-transparent',
|
ghost: 'text-foreground bg-transparent',
|
||||||
|
text: '',
|
||||||
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
||||||
fail: 'bg-fail text-white hover:bg-fail/90',
|
fail: 'bg-fail text-white hover:bg-fail/90',
|
||||||
warn: '',
|
warn: '',
|
||||||
@@ -70,6 +71,11 @@ export const buttonVariants = cva(
|
|||||||
color: 'fail',
|
color: 'fail',
|
||||||
className: 'hover:bg-fail/10 text-fail',
|
className: 'hover:bg-fail/10 text-fail',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
theme: 'text',
|
||||||
|
color: 'primary',
|
||||||
|
className: 'hover:text-primary',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
|
|||||||
126
src/components/ui/navigation-menu.tsx
Normal file
126
src/components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import {ComponentProps, ReactNode} from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import * as Primitive from '@radix-ui/react-navigation-menu'
|
||||||
|
import {ChevronDownIcon, ChevronUpIcon} from 'lucide-react'
|
||||||
|
import {Button, buttonVariants} from './button'
|
||||||
|
import {merge} from '@/lib/utils'
|
||||||
|
|
||||||
|
const NavigationMenuItemStyle = buttonVariants({
|
||||||
|
theme: 'text',
|
||||||
|
color: `primary`,
|
||||||
|
className: 'h-full rounded-none',
|
||||||
|
})
|
||||||
|
|
||||||
|
function Navigation(props: ComponentProps<typeof Primitive.Root>) {
|
||||||
|
return <Primitive.Root {...props} delayDuration={0} skipDelayDuration={0}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationGroup(props: ComponentProps<typeof Primitive.List>) {
|
||||||
|
return <Primitive.List {...props} className={merge('h-full flex items-stretch', props.className)}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationItem(props: ComponentProps<typeof Primitive.Item>) {
|
||||||
|
return <Primitive.Item {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationLink(props: {
|
||||||
|
text: ReactNode
|
||||||
|
href: string
|
||||||
|
classNameOverride?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Primitive.Link asChild className={merge(props.classNameOverride ?? NavigationMenuItemStyle, 'text-lg')}>
|
||||||
|
<Link href={props.href}>
|
||||||
|
{props.text}
|
||||||
|
</Link>
|
||||||
|
</Primitive.Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationTrigger(props: {
|
||||||
|
suffix?: boolean
|
||||||
|
text: ReactNode
|
||||||
|
}) {
|
||||||
|
const suffix = props.suffix ?? true
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Primitive.Trigger asChild>
|
||||||
|
<Button theme="text" className="text-lg gap-2 h-full group/trigger">
|
||||||
|
{props.text}
|
||||||
|
{suffix && (
|
||||||
|
<ChevronUpIcon
|
||||||
|
className="size-4 transition-transform duration-150 ease-in-out rotate-0 group-data-[state=open]/trigger:rotate-180"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Primitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationItemContent(props: ComponentProps<typeof Primitive.Content>) {
|
||||||
|
return (
|
||||||
|
<Primitive.Content {...props}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationLinkItem(props: {
|
||||||
|
href: string
|
||||||
|
text: ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Primitive.Item>
|
||||||
|
<NavigationLink {...props}/>
|
||||||
|
</Primitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationTriggerItem(props: {
|
||||||
|
text: ReactNode
|
||||||
|
suffix?: boolean
|
||||||
|
children?: ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Primitive.Item>
|
||||||
|
<NavigationTrigger text={props.text} suffix={props.suffix}/>
|
||||||
|
<NavigationItemContent className={props.className}>
|
||||||
|
{props.children}
|
||||||
|
</NavigationItemContent>
|
||||||
|
</Primitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationIndicator() {
|
||||||
|
return (
|
||||||
|
<Primitive.Indicator className={merge(
|
||||||
|
'w-full h-1 rounded-xs bg-primary z-10 top-[calc(100%-4px)]',
|
||||||
|
'transition-transform duration-150 ease-out',
|
||||||
|
'data-[state=visible]:animate-fadein',
|
||||||
|
'data-[state=hidden]:animate-fadeout',
|
||||||
|
)}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuViewport(props: ComponentProps<typeof Primitive.Viewport>) {
|
||||||
|
return (
|
||||||
|
<div className={props.className}>
|
||||||
|
<Primitive.Viewport
|
||||||
|
{...props}
|
||||||
|
className="data-[state=open]:animate-fadein data-[state=closed]:animate-fadeout"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Navigation,
|
||||||
|
NavigationGroup,
|
||||||
|
NavigationItem,
|
||||||
|
NavigationItemContent,
|
||||||
|
NavigationTrigger,
|
||||||
|
NavigationLink,
|
||||||
|
NavigationIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
NavigationLinkItem,
|
||||||
|
NavigationTriggerItem,
|
||||||
|
}
|
||||||
46
src/lib/stores/client.ts
Normal file
46
src/lib/stores/client.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {createStore} from 'zustand/vanilla'
|
||||||
|
import {persist} from 'zustand/middleware'
|
||||||
|
|
||||||
|
export type ClientStore = ClientState & ClientActions
|
||||||
|
|
||||||
|
type Point = 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
|
||||||
|
export type ClientState = {
|
||||||
|
breakpoint: {
|
||||||
|
sm: boolean
|
||||||
|
md: boolean
|
||||||
|
lg: boolean
|
||||||
|
xl: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientActions = {
|
||||||
|
setBreakpoints: (breakpoints: Partial<ClientState['breakpoint']>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createClientStore = () => {
|
||||||
|
return createStore<ClientStore>()(persist(
|
||||||
|
setState => ({
|
||||||
|
breakpoint: {
|
||||||
|
sm: false,
|
||||||
|
md: false,
|
||||||
|
lg: false,
|
||||||
|
xl: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
setBreakpoints: breakpoints => setState(state => ({
|
||||||
|
breakpoint: {
|
||||||
|
...state.breakpoint,
|
||||||
|
...breakpoints,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'client-store',
|
||||||
|
partialize: state => ({
|
||||||
|
device: state.breakpoint,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {createStore} from 'zustand/vanilla'
|
import {createStore} from 'zustand/vanilla'
|
||||||
|
import {persist} from 'zustand/middleware'
|
||||||
|
|
||||||
export type LayoutStore = LayoutState & LayoutActions
|
export type LayoutStore = LayoutState & LayoutActions
|
||||||
|
|
||||||
@@ -11,14 +12,25 @@ export type LayoutActions = {
|
|||||||
setNavbar: (navbar: boolean) => void
|
setNavbar: (navbar: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLayoutStore = (open: boolean) => {
|
export const createLayoutStore = () => {
|
||||||
return createStore<LayoutStore>()(setState => ({
|
return createStore<LayoutStore>()(persist(
|
||||||
navbar: open,
|
setState => ({
|
||||||
|
navbar: false,
|
||||||
|
|
||||||
toggleNavbar: () => setState((state) => {
|
toggleNavbar: () => setState((state) => {
|
||||||
return {navbar: !state.navbar}
|
return {navbar: !state.navbar}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setNavbar: navbar => setState((_) => {
|
setNavbar: navbar => setState((_) => {
|
||||||
return {navbar}
|
return {navbar}
|
||||||
}),
|
}),
|
||||||
}))
|
}),
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'layout-store',
|
||||||
|
partialize: state => ({
|
||||||
|
navbar: state.navbar,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user