feat: 用户状态持久化(persist) + 应用初始化刷新用户信息

This commit is contained in:
2026-05-15 19:45:40 +08:00
parent 5159e7c90d
commit 19fcfb38c8
7 changed files with 139 additions and 20 deletions

View File

@@ -1,19 +1,35 @@
import { App as AntdApp, ConfigProvider } from 'antd'; import { App as AntdApp, ConfigProvider, Spin } from 'antd';
import zhCN from 'antd/locale/zh_CN'; import zhCN from 'antd/locale/zh_CN';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn';
import { RouterProvider } from 'react-router'; import { RouterProvider } from 'react-router';
import router from '@/router'; import router from '@/router';
import useAppInit from '@/hooks/useAppInit';
import './App.css'; import './App.css';
dayjs.locale('zh-cn'); dayjs.locale('zh-cn');
const App = () => ( const App = () => {
const initialized = useAppInit();
// 初始化完成前显示全局 loading
if (!initialized) {
return (
<ConfigProvider locale={zhCN}>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<Spin size="large" />
</div>
</ConfigProvider>
);
}
return (
<ConfigProvider locale={zhCN}> <ConfigProvider locale={zhCN}>
<AntdApp> <AntdApp>
<RouterProvider router={router} /> <RouterProvider router={router} />
</AntdApp> </AntdApp>
</ConfigProvider> </ConfigProvider>
); );
};
export default App; export default App;

View File

@@ -1,4 +1,4 @@
import { post } from '@/utils/request'; import { get, post } from '@/utils/request';
/** 登录请求参数(不导出) */ /** 登录请求参数(不导出) */
interface LoginParams { interface LoginParams {
@@ -16,8 +16,20 @@ interface LoginResult {
token: string; token: string;
} }
/** 当前用户信息(不导出,与 LoginResult 结构相同) */
interface CurrentUser {
id: string;
userName: string;
nickName: string;
roles: string[];
token: string;
}
/** /**
* 登录接口 * 登录接口
* @param data 登录参数 * @param data 登录参数
*/ */
export const login = (data: LoginParams) => post<LoginResult>('/api/auth/login', data); export const login = (data: LoginParams) => post<LoginResult>('/api/auth/login', data);
/** 获取当前登录用户信息(应用初始化时调用,刷新 userInfo */
export const getCurrentUser = () => get<CurrentUser>('/api/auth/me');

View File

@@ -0,0 +1,15 @@
import { Navigate, Outlet } from 'react-router';
import { useUserStore } from '@/store';
/** 路由守卫:未登录时跳转到登录页 */
const AuthGuard = () => {
const userInfo = useUserStore((s) => s.userInfo);
if (!userInfo) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
};
export default AuthGuard;

42
src/hooks/useAppInit.ts Normal file
View File

@@ -0,0 +1,42 @@
import { useEffect, useState } from 'react';
import { getCurrentUser } from '@/api/auth';
import { useUserStore } from '@/store';
/**
* 应用初始化 hook
* 刷新页面时,如果 localStorage 中有 token则调用 /api/auth/me 刷新用户信息
* token 失效时自动清除 userInfo
*/
const useAppInit = () => {
const [initialized, setInitialized] = useState(false);
const setUserInfo = useUserStore((s) => s.setUserInfo);
const clearUserInfo = useUserStore((s) => s.clearUserInfo);
const token = useUserStore((s) => s.userInfo?.token);
useEffect(() => {
// 没有 token跳过初始化
if (!token) {
setInitialized(true);
return;
}
const init = async () => {
try {
const res = await getCurrentUser();
// 用接口返回的最新数据更新 userInfo保留原 token
setUserInfo({ ...res.data!, token });
} catch {
// token 失效,清除用户信息
clearUserInfo();
} finally {
setInitialized(true);
}
};
init();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return initialized;
};
export default useAppInit;

View File

@@ -3,6 +3,9 @@ import { http, HttpResponse } from 'msw';
/** 包装为统一响应格式 */ /** 包装为统一响应格式 */
const ok = (data: unknown) => ({ code: '0', msg: 'ok', data, time: Date.now(), ok: true }); const ok = (data: unknown) => ({ code: '0', msg: 'ok', data, time: Date.now(), ok: true });
/** 包装为错误响应格式 */
const fail = (msg: string) => ({ code: '401', msg, data: null, time: Date.now(), ok: false });
export const authHandlers = [ export const authHandlers = [
http.post('/api/auth/login', async ({ request }) => { http.post('/api/auth/login', async ({ request }) => {
const body = await request.json() as { tenantId: string; userName: string; password: string }; const body = await request.json() as { tenantId: string; userName: string; password: string };
@@ -16,4 +19,21 @@ export const authHandlers = [
}; };
return HttpResponse.json(ok(result)); return HttpResponse.json(ok(result));
}), }),
http.get('/api/auth/me', ({ request }) => {
// 从请求头中获取 token有则返回用户信息无则返回 401
const authHeader = request.headers.get('Authorization');
if (!authHeader) {
return HttpResponse.json(fail('未登录'), { status: 401 });
}
// 模拟返回当前用户信息
const result = {
id: '1',
userName: 'admin',
nickName: '管理员',
roles: ['admin'],
token: authHeader.replace('Bearer ', ''),
};
return HttpResponse.json(ok(result));
}),
]; ];

View File

@@ -1,18 +1,24 @@
import { createBrowserRouter } from 'react-router'; import { createBrowserRouter } from 'react-router';
import AuthGuard from '@/components/AuthGuard';
import RootLayout from '@/layouts/RootLayout'; import RootLayout from '@/layouts/RootLayout';
import Login from '@/pages/login/index'; import Login from '@/pages/login/index';
import { routes } from '@/routes'; import { routes } from '@/routes';
import { toRouteObjects } from '@/routes/utils'; import { toRouteObjects } from '@/routes/utils';
const router = createBrowserRouter([ const router = createBrowserRouter([
// 登录页:独立路由,不加载布局 // 登录页:独立路由,不加载布局,无需鉴权
{ path: '/login', element: <Login /> }, { path: '/login', element: <Login /> },
// 带布局的主应用 // 需要鉴权的路由AuthGuard 判断登录状态
{
element: <AuthGuard />,
children: [
{ {
path: '/', path: '/',
element: <RootLayout />, element: <RootLayout />,
children: toRouteObjects(routes), children: toRouteObjects(routes),
}, },
],
},
]); ]);
export default router; export default router;

View File

@@ -1,4 +1,5 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware';
/** 当前登录用户信息 */ /** 当前登录用户信息 */
interface UserInfo { interface UserInfo {
@@ -24,12 +25,19 @@ interface UserState {
clearUserInfo: () => void; clearUserInfo: () => void;
} }
/** 用户状态 store */ /** 用户状态 store,使用 persist 中间件持久化到 localStorage */
const useUserStore = create<UserState>((set) => ({ const useUserStore = create<UserState>()(
persist(
(set) => ({
userInfo: null, userInfo: null,
setUserInfo: (info) => set({ userInfo: info }), setUserInfo: (info) => set({ userInfo: info }),
clearUserInfo: () => set({ userInfo: null }), clearUserInfo: () => set({ userInfo: null }),
})); }),
{
name: 'taotie-user',
},
),
);
export default useUserStore; export default useUserStore;
export type { UserInfo }; export type { UserInfo };