From 7e8470c5c26dfe83566d6085f9a472be6c0589b5 Mon Sep 17 00:00:00 2001 From: xie2can <384968446@qq.com> Date: Fri, 15 May 2026 19:21:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=A8=E5=B1=80=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=AE=A1=E7=90=86(Zustand)=E3=80=81Header=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=A4=B4=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- pnpm-lock.yaml | 26 ++++++++++++++++++++++++++ src/layouts/RootLayout.tsx | 34 ++++++++++++++++++++++++++++++++-- src/store/app.ts | 17 +++++++++++++++++ src/store/index.ts | 3 +++ src/store/user.ts | 35 +++++++++++++++++++++++++++++++++++ src/utils/request.ts | 8 ++++++-- 7 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 src/store/app.ts create mode 100644 src/store/index.ts create mode 100644 src/store/user.ts diff --git a/package.json b/package.json index a6b11bb..338f325 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "dayjs": "^1.11.20", "react": "^19.2.6", "react-dom": "^19.2.6", - "react-router": "^7.15.1" + "react-router": "^7.15.1", + "zustand": "^5.0.13" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6587d29..1c03298 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: react-router: specifier: ^7.15.1 version: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + zustand: + specifier: ^5.0.13 + version: 5.0.13(@types/react@19.2.14)(react@19.2.6) devDependencies: '@eslint/js': specifier: ^10.0.1 @@ -1467,6 +1470,24 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zustand@5.0.13: + resolution: {integrity: sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@ant-design/colors@8.0.1': @@ -2965,3 +2986,8 @@ snapshots: zod: 4.4.3 zod@4.4.3: {} + + zustand@5.0.13(@types/react@19.2.14)(react@19.2.6): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.6 diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx index 9dee121..1e99508 100644 --- a/src/layouts/RootLayout.tsx +++ b/src/layouts/RootLayout.tsx @@ -1,9 +1,10 @@ -import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; -import { Layout, Menu } from 'antd'; +import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons'; +import { Avatar, Dropdown, Layout, Menu, Space } from 'antd'; import { useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router'; import { routes } from '@/routes'; import { toMenuItems } from '@/routes/utils'; +import { useUserStore } from '@/store'; const { Header, Sider, Content } = Layout; @@ -13,6 +14,23 @@ const RootLayout = () => { const navigate = useNavigate(); const location = useLocation(); const [collapsed, setCollapsed] = useState(false); + const userInfo = useUserStore((s) => s.userInfo); + const clearUserInfo = useUserStore((s) => s.clearUserInfo); + + /** 用户头像下拉菜单 */ + const userMenuItems = [ + { key: 'profile', label: '个人信息' }, + { type: 'divider' as const }, + { key: 'logout', label: '退出登录', danger: true }, + ]; + + /** 下拉菜单点击事件 */ + const handleUserMenuClick = ({ key }: { key: string }) => { + if (key === 'logout') { + clearUserInfo(); + // TODO: 跳转到登录页 + } + }; return ( @@ -20,12 +38,24 @@ const RootLayout = () => { style={{ display: 'flex', alignItems: 'center', + justifyContent: 'space-between', padding: '0 24px', background: '#fff', borderBottom: '1px solid #f0f0f0', }} > TaoTie + + + } + src={undefined} + style={{ backgroundColor: '#00b96b' }} + /> + {userInfo && {userInfo.nickName}} + + void; +} + +/** 全局 app store */ +const useAppStore = create((set) => ({ + sidebarCollapsed: false, + toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })), +})); + +export default useAppStore; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..0352332 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,3 @@ +export { default as useAppStore } from './app'; +export { default as useUserStore } from './user'; +export type { UserInfo } from './user'; diff --git a/src/store/user.ts b/src/store/user.ts new file mode 100644 index 0000000..b2ee886 --- /dev/null +++ b/src/store/user.ts @@ -0,0 +1,35 @@ +import { create } from 'zustand'; + +/** 当前登录用户信息 */ +interface UserInfo { + /** 用户 ID */ + id: string; + /** 用户名 */ + userName: string; + /** 昵称 */ + nickName: string; + /** 角色标识列表 */ + roles: string[]; + /** 访问令牌 */ + token: string; +} + +/** 用户状态 */ +interface UserState { + /** 当前登录用户信息,未登录时为 null */ + userInfo: UserInfo | null; + /** 设置用户信息(登录成功后调用) */ + setUserInfo: (info: UserInfo | null) => void; + /** 清除用户信息(退出登录时调用) */ + clearUserInfo: () => void; +} + +/** 用户状态 store */ +const useUserStore = create((set) => ({ + userInfo: null, + setUserInfo: (info) => set({ userInfo: info }), + clearUserInfo: () => set({ userInfo: null }), +})); + +export default useUserStore; +export type { UserInfo }; diff --git a/src/utils/request.ts b/src/utils/request.ts index 65783b5..1a4e4f9 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,14 +1,18 @@ import axios from 'axios'; +import { useUserStore } from '@/store'; const request = axios.create({ baseURL: import.meta.env.PUBLIC_BASE_URL, timeout: 10000, }); -// 请求拦截器 +// 请求拦截器:自动附加 token request.interceptors.request.use( (config) => { - // TODO: 添加 token 等请求头 + const token = useUserStore.getState().userInfo?.token; + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } return config; }, (error) => {