From 5c88cd7f32a1e3b70557f62e1e9845acf0ece844 Mon Sep 17 00:00:00 2001
From: luorijun
Date: Fri, 25 Apr 2025 16:24:04 +0800
Subject: [PATCH] =?UTF-8?q?=E5=B0=81=E8=A3=85=20Header=20=E5=92=8C=20Navba?=
=?UTF-8?q?r=20=E7=BB=84=E4=BB=B6=EF=BC=8C=E8=B0=83=E6=95=B4=E7=94=A8?=
=?UTF-8?q?=E6=88=B7=E7=95=8C=E9=9D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 17 ++-
src/app/admin/_assets/logo-mini.webp | Bin 0 -> 4006 bytes
src/app/admin/_client/header.tsx | 42 +++++++
src/app/admin/_client/navbar.tsx | 108 ++++++++++++++++++
.../admin/{_server => _client}/profile.tsx | 0
src/app/admin/identify/page.tsx | 2 +-
src/app/admin/layout.tsx | 93 +++------------
src/app/admin/purchase/page.tsx | 2 +-
src/app/globals.css | 2 +-
src/components/page.tsx | 25 ++--
src/components/providers/StoreProvider.tsx | 20 +++-
src/components/ui/button.tsx | 3 +-
src/stores/layout.ts | 24 ++++
src/stores/{profile-store.ts => profile.ts} | 0
14 files changed, 244 insertions(+), 94 deletions(-)
create mode 100644 src/app/admin/_assets/logo-mini.webp
create mode 100644 src/app/admin/_client/header.tsx
create mode 100644 src/app/admin/_client/navbar.tsx
rename src/app/admin/{_server => _client}/profile.tsx (100%)
create mode 100644 src/stores/layout.ts
rename src/stores/{profile-store.ts => profile.ts} (100%)
diff --git a/README.md b/README.md
index f100b80..d5e6de9 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,19 @@
## TODO
-引入 redis,客户端密钥使用 redis 保存
-
使用 pure js 的包代替 canvas,加快编译速度
提取后刷新提取页套餐可用余量
-保存客户端信息时用 jwt 序列化
+白名单复用表格组件
----
+首次登录弹窗:需要设置初始密码,展示 实名->购买->提取 的流程介绍
+
+提取页表单性能优化,树组件性能优化
+
+后台页面:
+- [ ] 总览
+- [ ] 个人中心
+- [ ] IP 管理
+- [ ] 提取记录
+- [ ] 使用记录
-页面数据:
- - [ ] dashboard
diff --git a/src/app/admin/_assets/logo-mini.webp b/src/app/admin/_assets/logo-mini.webp
new file mode 100644
index 0000000000000000000000000000000000000000..3a586bb45d6bee46ce83faffb499da418e72828f
GIT binary patch
literal 4006
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
literal 0
HcmV?d00001
diff --git a/src/app/admin/_client/header.tsx b/src/app/admin/_client/header.tsx
new file mode 100644
index 0000000..c60d39a
--- /dev/null
+++ b/src/app/admin/_client/header.tsx
@@ -0,0 +1,42 @@
+'use client'
+import {PanelLeftCloseIcon, PanelLeftOpenIcon} from 'lucide-react'
+import {Button} from '@/components/ui/button'
+import Profile from './profile'
+import {useLayoutStore} from '@/components/providers/StoreProvider'
+import {merge} from '@/lib/utils'
+
+export type HeaderProps = {}
+
+export default function Header(props: HeaderProps) {
+
+ const navbar = useLayoutStore(store => store.navbar)
+ const toggleNavbar = useLayoutStore(store => store.toggleNavbar)
+
+ return (
+
+ {/* left */}
+
+
+
+ 欢迎来到,蓝狐代理
+
+
+
+ {/* right */}
+
+
+ )
+}
diff --git a/src/app/admin/_client/navbar.tsx b/src/app/admin/_client/navbar.tsx
new file mode 100644
index 0000000..a30ebe1
--- /dev/null
+++ b/src/app/admin/_client/navbar.tsx
@@ -0,0 +1,108 @@
+'use client'
+import {ReactNode} from 'react'
+import {merge} from '@/lib/utils'
+import {useLayoutStore} from '@/components/providers/StoreProvider'
+import Link from 'next/link'
+import Image from 'next/image'
+import logo from '@/assets/logo.webp'
+import logoMini from '../_assets/logo-mini.webp'
+
+export type NavbarProps = {}
+
+export default function Navbar(props: NavbarProps) {
+
+ const navbar = useLayoutStore(store => store.navbar)
+
+ return (
+
+ )
+}
+
+function Logo(props: {
+ mini?: boolean
+}) {
+ return (
+
+
+ {props.mini
+ ?
+ :
+ }
+
+
+ )
+}
+
+function NavTitle(props: {
+ label: string
+}) {
+ return (
+
+ {props.label}
+
+
+ )
+}
+
+function NavItem(props: {
+ href: string
+ icon?: ReactNode
+ label: string
+ expand: boolean
+}) {
+ return (
+
+ {props.icon}
+ {props.label}
+
+ )
+}
diff --git a/src/app/admin/_server/profile.tsx b/src/app/admin/_client/profile.tsx
similarity index 100%
rename from src/app/admin/_server/profile.tsx
rename to src/app/admin/_client/profile.tsx
diff --git a/src/app/admin/identify/page.tsx b/src/app/admin/identify/page.tsx
index 86f90a5..fdc17f6 100644
--- a/src/app/admin/identify/page.tsx
+++ b/src/app/admin/identify/page.tsx
@@ -98,7 +98,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
// ======================
return (
-
+
{/* banner */}
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
index 7622064..fdefde8 100644
--- a/src/app/admin/layout.tsx
+++ b/src/app/admin/layout.tsx
@@ -1,11 +1,9 @@
import {ReactNode} from 'react'
-import Image from 'next/image'
-import logo from '@/assets/logo.webp'
-import Profile from '@/app/admin/_server/profile'
import {merge} from '@/lib/utils'
-import Link from 'next/link'
import {redirect} from 'next/navigation'
import {getProfile} from '@/actions/auth/auth'
+import Header from './_client/header'
+import Navbar from '@/app/admin/_client/navbar'
export type DashboardLayoutProps = {
children: ReactNode
@@ -13,88 +11,31 @@ export type DashboardLayoutProps = {
export default async function DashboardLayout(props: DashboardLayoutProps) {
+ // ======================
+ // profile
+ // ======================
+
const user = await getProfile()
if (!user) {
return redirect(`/login?redirect=${encodeURIComponent('/admin')}`)
}
+ // ======================
+ // render
+ // ======================
+
return (
-
- {/* background */}
-
+
- {/* content */}
-
- {/* logo */}
-
-
-
-
- {/* title */}
-
- 欢迎来到,蓝狐代理
-
-
- {/* profile */}
-
-
-
-
-
+
+
+
{props.children}
)
}
-
-function NavTitle(props: {
- label: string
-}) {
- return (
-
- {props.label}
-
- )
-}
-
-function NavItem(props: {
- href: string
- icon?: ReactNode
- label: string
-}) {
- return (
-
-
{props.icon}
-
{props.label}
-
- )
-}
diff --git a/src/app/admin/purchase/page.tsx b/src/app/admin/purchase/page.tsx
index cd99552..f684176 100644
--- a/src/app/admin/purchase/page.tsx
+++ b/src/app/admin/purchase/page.tsx
@@ -5,7 +5,7 @@ export type PurchasePageProps = {}
export default async function PurchasePage(props: PurchasePageProps) {
return (
-
+
)
diff --git a/src/app/globals.css b/src/app/globals.css
index 082e65e..9cdd582 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -28,7 +28,7 @@
--fail: oklch(0.65 0.16 25);
--fail-text: oklch(1 0 0);
- --card: oklch(0.985 0 0);
+ --card: oklch(1 0 0);
--card-text: oklch(0.25 0 0);
--popover: oklch(1 0 0);
diff --git a/src/components/page.tsx b/src/components/page.tsx
index c070e53..1893c3c 100644
--- a/src/components/page.tsx
+++ b/src/components/page.tsx
@@ -8,14 +8,25 @@ export type PageProps = {
export default function Page(props: ComponentProps<'main'> & PageProps) {
return (
- {props.children}
+
+ {/* background */}
+
+
+ {/* content */}
+
+ {props.children}
+
)
}
diff --git a/src/components/providers/StoreProvider.tsx b/src/components/providers/StoreProvider.tsx
index 59b9482..d7d1ec1 100644
--- a/src/components/providers/StoreProvider.tsx
+++ b/src/components/providers/StoreProvider.tsx
@@ -1,13 +1,15 @@
'use client'
import {User} from '@/lib/models'
import {createContext, ReactNode, useContext, useRef} from 'react'
-import {createProfileStore, ProfileStore} from '@/stores/profile-store'
import {StoreApi} from 'zustand/vanilla'
import {useStore} from 'zustand/react'
+import {createProfileStore, ProfileStore} from '@/stores/profile'
+import {createLayoutStore, LayoutStore} from '@/stores/layout'
export type StoreContextType = {
profile: StoreApi
+ layout: StoreApi
}
export const StoreContext = createContext(null)
@@ -18,15 +20,23 @@ export type ProfileProviderProps = {
}
export default function StoreProvider(props: ProfileProviderProps) {
+
const profile = useRef>(null)
if (!profile.current) {
console.log('create profile store')
profile.current = createProfileStore(props.user)
}
+ const layout = useRef>(null)
+ if (!layout.current) {
+ console.log('create layout store')
+ layout.current = createLayoutStore()
+ }
+
return (
{props.children}
@@ -41,3 +51,11 @@ export function useProfileStore(selector: (store: ProfileStore) => T) {
}
return useStore(ctx.profile, selector)
}
+
+export function useLayoutStore(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)
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index e39257b..1830cbf 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -33,7 +33,7 @@ const buttonVariants = cva(
)
type ButtonProps = React.ComponentProps<'button'> & {
- theme?: 'default' | 'outline' | 'gradient' | 'error' | 'accent'
+ theme?: 'default' | 'outline' | 'gradient' | 'error' | 'accent' | 'ghost'
}
function Button(rawProps: ButtonProps) {
@@ -55,6 +55,7 @@ function Button(rawProps: ButtonProps) {
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
error: 'bg-fail text-white hover:bg-fail/90',
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 hover:bg-muted",
}[theme ?? 'default'],
className,
)}
diff --git a/src/stores/layout.ts b/src/stores/layout.ts
new file mode 100644
index 0000000..ac9dabb
--- /dev/null
+++ b/src/stores/layout.ts
@@ -0,0 +1,24 @@
+import {createStore} from 'zustand/vanilla'
+
+export type LayoutStore = LayoutState & LayoutActions
+
+export type LayoutState = {
+ navbar: boolean
+}
+
+export type LayoutActions = {
+ toggleNavbar: () => void
+ setNavbar: (navbar: boolean) => void
+}
+
+export const createLayoutStore = () => {
+ return createStore()(setState => ({
+ navbar: true,
+ toggleNavbar: () => setState(state => {
+ return {navbar: !state.navbar}
+ }),
+ setNavbar: (navbar) => setState(_ => {
+ return {navbar}
+ }),
+ }))
+}
diff --git a/src/stores/profile-store.ts b/src/stores/profile.ts
similarity index 100%
rename from src/stores/profile-store.ts
rename to src/stores/profile.ts