feat: 全局状态管理(Zustand)、Header用户头像

This commit is contained in:
2026-05-15 19:21:07 +08:00
parent dead892f4e
commit 7e8470c5c2
7 changed files with 121 additions and 5 deletions

View File

@@ -17,7 +17,8 @@
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"react": "^19.2.6", "react": "^19.2.6",
"react-dom": "^19.2.6", "react-dom": "^19.2.6",
"react-router": "^7.15.1" "react-router": "^7.15.1",
"zustand": "^5.0.13"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",

26
pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
react-router: react-router:
specifier: ^7.15.1 specifier: ^7.15.1
version: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) 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: devDependencies:
'@eslint/js': '@eslint/js':
specifier: ^10.0.1 specifier: ^10.0.1
@@ -1467,6 +1470,24 @@ packages:
zod@4.4.3: zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} 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: snapshots:
'@ant-design/colors@8.0.1': '@ant-design/colors@8.0.1':
@@ -2965,3 +2986,8 @@ snapshots:
zod: 4.4.3 zod: 4.4.3
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

View File

@@ -1,9 +1,10 @@
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'; import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons';
import { Layout, Menu } from 'antd'; import { Avatar, Dropdown, Layout, Menu, Space } from 'antd';
import { useState } from 'react'; import { useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router'; import { Outlet, useLocation, useNavigate } from 'react-router';
import { routes } from '@/routes'; import { routes } from '@/routes';
import { toMenuItems } from '@/routes/utils'; import { toMenuItems } from '@/routes/utils';
import { useUserStore } from '@/store';
const { Header, Sider, Content } = Layout; const { Header, Sider, Content } = Layout;
@@ -13,6 +14,23 @@ const RootLayout = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [collapsed, setCollapsed] = useState(false); 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 ( return (
<Layout style={{ height: '100vh' }}> <Layout style={{ height: '100vh' }}>
@@ -20,12 +38,24 @@ const RootLayout = () => {
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between',
padding: '0 24px', padding: '0 24px',
background: '#fff', background: '#fff',
borderBottom: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0',
}} }}
> >
<span style={{ fontWeight: 'bold', fontSize: 18 }}>TaoTie</span> <span style={{ fontWeight: 'bold', fontSize: 18 }}>TaoTie</span>
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }}>
<Space style={{ cursor: 'pointer' }}>
<Avatar
size="small"
icon={<UserOutlined />}
src={undefined}
style={{ backgroundColor: '#00b96b' }}
/>
{userInfo && <span>{userInfo.nickName}</span>}
</Space>
</Dropdown>
</Header> </Header>
<Layout style={{ overflow: 'hidden' }}> <Layout style={{ overflow: 'hidden' }}>
<Sider <Sider

17
src/store/app.ts Normal file
View File

@@ -0,0 +1,17 @@
import { create } from 'zustand';
/** 全局共用状态(放置应用级别的全局状态) */
interface AppState {
/** 侧边栏是否折叠 */
sidebarCollapsed: boolean;
/** 切换侧边栏折叠状态 */
toggleSidebar: () => void;
}
/** 全局 app store */
const useAppStore = create<AppState>((set) => ({
sidebarCollapsed: false,
toggleSidebar: () => set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed })),
}));
export default useAppStore;

3
src/store/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { default as useAppStore } from './app';
export { default as useUserStore } from './user';
export type { UserInfo } from './user';

35
src/store/user.ts Normal file
View File

@@ -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<UserState>((set) => ({
userInfo: null,
setUserInfo: (info) => set({ userInfo: info }),
clearUserInfo: () => set({ userInfo: null }),
}));
export default useUserStore;
export type { UserInfo };

View File

@@ -1,14 +1,18 @@
import axios from 'axios'; import axios from 'axios';
import { useUserStore } from '@/store';
const request = axios.create({ const request = axios.create({
baseURL: import.meta.env.PUBLIC_BASE_URL, baseURL: import.meta.env.PUBLIC_BASE_URL,
timeout: 10000, timeout: 10000,
}); });
// 请求拦截器 // 请求拦截器:自动附加 token
request.interceptors.request.use( request.interceptors.request.use(
(config) => { (config) => {
// TODO: 添加 token 等请求头 const token = useUserStore.getState().userInfo?.token;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config; return config;
}, },
(error) => { (error) => {