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 dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import { RouterProvider } from 'react-router';
import router from '@/router';
import useAppInit from '@/hooks/useAppInit';
import './App.css';
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}>
<AntdApp>
<RouterProvider router={router} />
</AntdApp>
</ConfigProvider>
);
};
export default App;

View File

@@ -1,4 +1,4 @@
import { post } from '@/utils/request';
import { get, post } from '@/utils/request';
/** 登录请求参数(不导出) */
interface LoginParams {
@@ -16,8 +16,20 @@ interface LoginResult {
token: string;
}
/** 当前用户信息(不导出,与 LoginResult 结构相同) */
interface CurrentUser {
id: string;
userName: string;
nickName: string;
roles: string[];
token: string;
}
/**
* 登录接口
* @param 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 fail = (msg: string) => ({ code: '401', msg, data: null, time: Date.now(), ok: false });
export const authHandlers = [
http.post('/api/auth/login', async ({ request }) => {
const body = await request.json() as { tenantId: string; userName: string; password: string };
@@ -16,4 +19,21 @@ export const authHandlers = [
};
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 AuthGuard from '@/components/AuthGuard';
import RootLayout from '@/layouts/RootLayout';
import Login from '@/pages/login/index';
import { routes } from '@/routes';
import { toRouteObjects } from '@/routes/utils';
const router = createBrowserRouter([
// 登录页:独立路由,不加载布局
// 登录页:独立路由,不加载布局,无需鉴权
{ path: '/login', element: <Login /> },
// 带布局的主应用
// 需要鉴权的路由AuthGuard 判断登录状态
{
element: <AuthGuard />,
children: [
{
path: '/',
element: <RootLayout />,
children: toRouteObjects(routes),
},
],
},
]);
export default router;

View File

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