From 19fcfb38c8dcdea4793d5432b5800c450e320a05 Mon Sep 17 00:00:00 2001 From: xie2can <384968446@qq.com> Date: Fri, 15 May 2026 19:45:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96(persist)=20+=20=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E5=88=B7=E6=96=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 32 ++++++++++++++++++++------- src/api/auth.ts | 14 +++++++++++- src/components/AuthGuard.tsx | 15 +++++++++++++ src/hooks/useAppInit.ts | 42 ++++++++++++++++++++++++++++++++++++ src/mock/auth.ts | 20 +++++++++++++++++ src/router.tsx | 16 +++++++++----- src/store/user.ts | 20 +++++++++++------ 7 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 src/components/AuthGuard.tsx create mode 100644 src/hooks/useAppInit.ts diff --git a/src/App.tsx b/src/App.tsx index f072a18..a23bff7 100644 --- a/src/App.tsx +++ b/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 = () => ( - - - - - -); +const App = () => { + const initialized = useAppInit(); + + // 初始化完成前显示全局 loading + if (!initialized) { + return ( + +
+ +
+
+ ); + } + + return ( + + + + + + ); +}; export default App; diff --git a/src/api/auth.ts b/src/api/auth.ts index ff738bd..c9bb0c4 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -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('/api/auth/login', data); + +/** 获取当前登录用户信息(应用初始化时调用,刷新 userInfo) */ +export const getCurrentUser = () => get('/api/auth/me'); diff --git a/src/components/AuthGuard.tsx b/src/components/AuthGuard.tsx new file mode 100644 index 0000000..0b06ed6 --- /dev/null +++ b/src/components/AuthGuard.tsx @@ -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 ; + } + + return ; +}; + +export default AuthGuard; diff --git a/src/hooks/useAppInit.ts b/src/hooks/useAppInit.ts new file mode 100644 index 0000000..bbd09a2 --- /dev/null +++ b/src/hooks/useAppInit.ts @@ -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; diff --git a/src/mock/auth.ts b/src/mock/auth.ts index 5363e40..624c638 100644 --- a/src/mock/auth.ts +++ b/src/mock/auth.ts @@ -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)); + }), ]; diff --git a/src/router.tsx b/src/router.tsx index e10aef9..0b500dd 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -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: }, - // 带布局的主应用 + // 需要鉴权的路由:AuthGuard 判断登录状态 { - path: '/', - element: , - children: toRouteObjects(routes), + element: , + children: [ + { + path: '/', + element: , + children: toRouteObjects(routes), + }, + ], }, ]); diff --git a/src/store/user.ts b/src/store/user.ts index b2ee886..f1d6566 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -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((set) => ({ - userInfo: null, - setUserInfo: (info) => set({ userInfo: info }), - clearUserInfo: () => set({ userInfo: null }), -})); +/** 用户状态 store,使用 persist 中间件持久化到 localStorage */ +const useUserStore = create()( + persist( + (set) => ({ + userInfo: null, + setUserInfo: (info) => set({ userInfo: info }), + clearUserInfo: () => set({ userInfo: null }), + }), + { + name: 'taotie-user', + }, + ), +); export default useUserStore; export type { UserInfo };