feat: 用户状态持久化(persist) + 应用初始化刷新用户信息
This commit is contained in:
32
src/App.tsx
32
src/App.tsx
@@ -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 = () => (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<AntdApp>
|
||||
<RouterProvider router={router} />
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
);
|
||||
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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
15
src/components/AuthGuard.tsx
Normal file
15
src/components/AuthGuard.tsx
Normal 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
42
src/hooks/useAppInit.ts
Normal 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;
|
||||
@@ -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));
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
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 判断登录状态
|
||||
{
|
||||
path: '/',
|
||||
element: <RootLayout />,
|
||||
children: toRouteObjects(routes),
|
||||
element: <AuthGuard />,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: <RootLayout />,
|
||||
children: toRouteObjects(routes),
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -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) => ({
|
||||
userInfo: null,
|
||||
setUserInfo: (info) => set({ userInfo: info }),
|
||||
clearUserInfo: () => set({ userInfo: null }),
|
||||
}));
|
||||
/** 用户状态 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 };
|
||||
|
||||
Reference in New Issue
Block a user