feat: 全局状态管理(Zustand)、Header用户头像
This commit is contained in:
@@ -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",
|
||||
|
||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<Layout style={{ height: '100vh' }}>
|
||||
@@ -20,12 +38,24 @@ const RootLayout = () => {
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 24px',
|
||||
background: '#fff',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<Layout style={{ overflow: 'hidden' }}>
|
||||
<Sider
|
||||
|
||||
17
src/store/app.ts
Normal file
17
src/store/app.ts
Normal 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
3
src/store/index.ts
Normal 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
35
src/store/user.ts
Normal 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 };
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user