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 + ? {`logo`} + : {`logo`} + } + +
+ ) +} + +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 */} -
- {`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