diff --git a/package-lock.json b/package-lock.json index 01ea515..34fab69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@ant-design/icons": "^6.1.1", "@google/genai": "^1.29.0", + "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.1.14", "@vitejs/plugin-react": "^5.0.4", "antd": "^5.24.1", @@ -23,6 +24,7 @@ "qrcode": "^1.5.4", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-redux": "^9.2.0", "react-router-dom": "^7.14.1", "recharts": "^3.8.1", "tailwind-merge": "^3.5.0", diff --git a/package.json b/package.json index 95b5ac6..75981ee 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@ant-design/icons": "^6.1.1", "@google/genai": "^1.29.0", + "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.1.14", "@vitejs/plugin-react": "^5.0.4", "antd": "^5.24.1", @@ -26,6 +27,7 @@ "qrcode": "^1.5.4", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-redux": "^9.2.0", "react-router-dom": "^7.14.1", "recharts": "^3.8.1", "tailwind-merge": "^3.5.0", diff --git a/src/api/request.ts b/src/api/request.ts index 0e61dbb..98fcf48 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -2,78 +2,81 @@ import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'ax import envConfig from '../../env'; import Cookies from 'js-cookie'; import { message } from 'antd'; +import store from '../store'; +import { logout } from '../store/userSlice'; -const API_BASE_URL = envConfig.baseURL; +const API_BASE_URL = envConfig.baseURL; const api = axios.create({ - baseURL: API_BASE_URL, - timeout: 20000, - headers: { - 'Content-Type': 'application/json', + baseURL: API_BASE_URL, + timeout: 20000, + headers: { + 'Content-Type': 'application/json', - }, - withCredentials: true, + }, + withCredentials: true, }); // 请求拦截器:自动添加token // 请求拦截器 api.interceptors.request.use( - (config: InternalAxiosRequestConfig) => { - // 从 localStorage 获取 token - const token = localStorage.getItem('token'); - if (token && config.headers) { - // 将 token 添加到请求头 - config.headers['Authorization'] = `Bearer ${token}`; - } - return config; - }, - (error: AxiosError) => { - return Promise.reject(error); - } + (config: InternalAxiosRequestConfig) => { + // 从 localStorage 获取 token + const token = localStorage.getItem('token'); + if (token && config.headers) { + // 将 token 添加到请求头 + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; + }, + (error: AxiosError) => { + return Promise.reject(error); + } ); // 响应拦截器 api.interceptors.response.use( - (response: AxiosResponse) => { - // 处理成功响应 - const { data } = response; - // 如果后端规范定义了 code,可以在这里统一处理 - // if (data.code !== 200) { - // message.error(data.message || '请求失败'); - // return Promise.reject(new Error(data.message || 'Error')); - // } - return data; - }, - (error: AxiosError) => { - // 处理错误响应 - if (error.response) { - switch (error.response.status) { - case 401: - // 未授权,清除本地信息并跳转到登录页 - localStorage.removeItem('token'); - localStorage.removeItem('isAuthenticated'); - localStorage.removeItem('user'); - window.location.href = '/login'; - message.error('会话已过期,请重新登录'); - break; - case 403: - message.error('拒绝访问:权限不足'); - break; - case 404: - message.error('资源不存在'); - break; - case 500: - message.error('服务器内部错误'); - break; - default: - message.error(error.message || '网络错误'); - } - } else { - message.error('连接服务器失败'); - } - return Promise.reject(error); - } + (response: AxiosResponse) => { + // 处理成功响应 + const { data } = response; + // 如果后端规范定义了 code,可以在这里统一处理 + // if (data.code !== 200) { + // message.error(data.message || '请求失败'); + // return Promise.reject(new Error(data.message || 'Error')); + // } + return data; + }, + (error: AxiosError) => { + // 处理错误响应 + if (error.response) { + switch (error.response.status) { + case 401: + // 未授权,清除本地信息并跳转到登录页 + localStorage.removeItem('token'); + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('user'); + store.dispatch(logout()); + window.location.href = '/login'; + message.error('会话已过期,请重新登录'); + break; + case 403: + message.error('拒绝访问:权限不足'); + break; + case 404: + message.error('资源不存在'); + break; + case 500: + message.error('服务器内部错误'); + break; + default: + message.error(error.message || '网络错误'); + } + } else { + message.error('连接服务器失败'); + } + return Promise.reject(error); + } ); export default api; diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 6f94d10..95ce427 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,12 +1,15 @@ import React from 'react'; +import { useSelector } from 'react-redux'; import { Navigate, Outlet } from 'react-router-dom'; export default function ProtectedRoute() { - const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true'; + const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true'; + //const { isAuthenticated } = useSelector(state => state.user); - if (!isAuthenticated) { - return ; - } - return ; + if (!isAuthenticated) { + return ; + } + + return ; } diff --git a/src/main.tsx b/src/main.tsx index 080dac3..2d3de05 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,15 @@ -import {StrictMode} from 'react'; -import {createRoot} from 'react-dom/client'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; import App from './App.tsx'; +import { Provider } from 'react-redux'; +import { store } from './store'; import './index.css'; createRoot(document.getElementById('root')!).render( - - - , + + + + , + + ); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 714d0ea..a6f3459 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -16,11 +16,14 @@ import { import { motion } from 'motion/react'; import bgcImg from '../assets/bgc2.png'; import logincard from '../assets/logincard.png'; +import { useDispatch } from 'react-redux'; +import { login } from '../store/userSlice'; const { Text, Title } = Typography; export default function Login() { const navigate = useNavigate(); + const dispatch = useDispatch(); // 账号密码登录 const onFinish = (values: any) => { @@ -49,6 +52,11 @@ export default function Login() { localStorage.setItem('user', JSON.stringify(userInfo)); message.success(`欢迎回来, ${name}`); + dispatch(login({ + token: 'mock_token_' + Date.now(), + userInfo: userInfo + })); + navigate('/'); }; diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..f8f21ce --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,10 @@ +import { configureStore } from '@reduxjs/toolkit'; +import userReducer from './userSlice'; + +export const store = configureStore({ + reducer: { + user: userReducer, + }, +}); + +export default store; diff --git a/src/store/userSlice.js b/src/store/userSlice.js new file mode 100644 index 0000000..4ba7a19 --- /dev/null +++ b/src/store/userSlice.js @@ -0,0 +1,37 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + token: localStorage.getItem('token') || null, + userInfo: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null, + isAuthenticated: localStorage.getItem('isAuthenticated') === 'true', +}; + +const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + // 登录 + login: (state, action) => { + state.token = action.payload.token; + state.userInfo = action.payload.userInfo; + state.isAuthenticated = true; + + localStorage.setItem('token', action.payload.token); + localStorage.setItem('user', JSON.stringify(action.payload.userInfo)); + localStorage.setItem('isAuthenticated', 'true'); + }, + // 退出 + logout: (state) => { + state.token = null; + state.userInfo = null; + state.isAuthenticated = false; + + localStorage.removeItem('token'); + localStorage.removeItem('user'); + localStorage.removeItem('isAuthenticated'); + }, + }, +}); + +export const { login, logout } = userSlice.actions; +export default userSlice.reducer;