feat: 用户状态持久化(persist) + 应用初始化刷新用户信息
This commit is contained in:
20
src/App.tsx
20
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 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;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
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 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));
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user