feat: 全局状态管理(Zustand)、Header用户头像
This commit is contained in:
@@ -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
26
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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
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 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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user