教育平台前端-登录页

页面效果

实现使用手机号+验证码登录,自动登录

页面结构

使用Ant Design Pro ComponentsLoginFormPage组件

js 复制代码
pnpm i @ant-design/pro-components --save
  1. 登录使用login接口、发生短信使用sendCodeMsg接口
  2. 勾选自动登录,将token保存到localStorage,否则保存到sessionStorage(关闭标签页会清除) src\views\login\index.tsx
ts 复制代码
import React from 'react';
import { login, sendCodeMsg } from '@/apis/login';
import { LockOutlined, MobileOutlined } from '@ant-design/icons';
import {
  LoginFormPage,
  ProConfigProvider,
  ProFormCaptcha,
  ProFormCheckbox,
  ProFormText,
} from '@ant-design/pro-components';
import { message, Tabs, theme } from 'antd';
import { useNavigate } from 'react-router-dom';
import styles from './index.module.less';

interface IValue {
  tel: string
  code: string
  autoLogin: boolean
}

const Login: React.FC = () => {
  const { token } = theme.useToken();
  const nav = useNavigate();

  const handleOnFinish = async (values: IValue) => {
    try {
      const res = await login(values);
      if (res.data) {
        const { userInfo, refreshToken, accessToken } = res.data;
        if (values.autoLogin) {
          localStorage.setItem('accessToken', refreshToken);
          localStorage.setItem('refreshToken', accessToken);
          localStorage.setItem('userInfo', JSON.stringify(userInfo));
          localStorage.setItem('autoLogin', JSON.stringify(values.autoLogin));

          sessionStorage.setItem('accessToken', '');
          sessionStorage.setItem('refreshToken', '');
          sessionStorage.setItem('userInfo', '');
        } else {
          sessionStorage.setItem('accessToken', refreshToken);
          sessionStorage.setItem('refreshToken', accessToken);
          sessionStorage.setItem('userInfo', JSON.stringify(userInfo));

          localStorage.setItem('accessToken', '');
          localStorage.setItem('refreshToken', '');
          localStorage.setItem('userInfo', '');
          localStorage.setItem('autoLogin', '');
        }

        message.success('登录成功');
        nav('/');
      }
    } catch (error) {
      console.log(error);
    }
  };
  return (
    <div className={styles.container}>
      <LoginFormPage
        logo="https://github.githubassets.com/images/modules/logos_page/Octocat.png"
        backgroundVideoUrl="https://gw.alipayobjects.com/v/huamei_gcee1x/afts/video/jXRBRK_VAwoAAAAAAAAAAAAAK4eUAQBr"
        onFinish={handleOnFinish}
      >
        <Tabs
          centered
          items={[
            {
              key: 'phone',
              label: '手机号登录',
            },
          ]}
        />
        <>
          <ProFormText
            fieldProps={{
              size: 'large',
              prefix: (
                <MobileOutlined
                  style={{
                    color: token.colorText,
                  }}
                  className="prefixIcon"
                />
              ),
            }}
            name="tel"
            placeholder="手机号"
            rules={[
              {
                required: true,
                message: '请输入手机号!',
              },
              {
                pattern: /^1\d{10}$/,
                message: '手机号格式错误!',
              },
            ]}
          />
          <ProFormCaptcha
            fieldProps={{
              size: 'large',
              prefix: (
                <LockOutlined
                  style={{
                    color: token.colorText,
                  }}
                  className="prefixIcon"
                />
              ),
            }}
            captchaProps={{
              size: 'large',
            }}
            placeholder="请输入验证码"
            captchaTextRender={(timing, count) => {
              if (timing) {
                return `${count} ${'获取验证码'}`;
              }
              return '获取验证码';
            }}
            name="code"
            rules={[
              {
                required: true,
                message: '请输入验证码!',
              },
            ]}
            phoneName="tel"
            onGetCaptcha={async (tel: string) => {
              await sendCodeMsg(tel);
              message.success('获取验证码成功!');
            }}
          />
        </>
        <div
          style={{
            marginBlockEnd: 24,
          }}
        >
          <ProFormCheckbox noStyle name="autoLogin">
            自动登录
          </ProFormCheckbox>
        </div>
      </LoginFormPage>
    </div>
  );
};

export default () => (
  <ProConfigProvider dark>
    <Login />
  </ProConfigProvider>
);

接口

封装axios
src\apis\index.ts

  1. 在请求拦截器添加accessToken
  2. 在响应拦截器添加失败回调,accessToken失效需要跳转到登录页面、用refreshToken重新请求获得新的accessToken、refreshToken
ts 复制代码
import { message } from 'antd';
import axios, {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
  InternalAxiosRequestConfig,
} from 'axios';

interface IResult<T> {
  code: string
  data: T
  message: string
}

interface PendingTask {
  config: AxiosRequestConfig
  resolve: Function
}

const instance: AxiosInstance = axios.create({
  baseURL: 'http://localhost:3000/', // 替换为你的 API 基础 URL
  timeout: 5000, // 请求超时时间(毫秒)
});

// 请求拦截器
instance.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const accessToken = localStorage.getItem('accessToken');

    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error),
);

let refreshing = false;
const queue: PendingTask[] = [];

// 响应拦截器
instance.interceptors.response.use(
  (response: AxiosResponse) => {
    if (response.status === 200) {
      return Promise.resolve(response.data);
    }
    return Promise.reject(response);
  },
  async (error) => {
    if (!error.response) {
      return Promise.reject(error);
    }
    const { data, config } = error.response;
    if (refreshing) {
      return new Promise((resolve) => {
        queue.push({
          config,
          resolve,
        });
      });
    }

    if (data.code === 401 && !config.url.includes('/auth/refresh')) {
      refreshing = true;

      const res = await refreshToken();

      refreshing = false;

      if (res.code === '200') {
        queue.forEach(({ config, resolve }) => {
          resolve(instance(config));
        });

        return instance(config);
      }
      message.error(error.response.data.data);

      setTimeout(() => {
        window.location.href = '/login';
      }, 1500);
    } else {
      message.error(error.response.data.data);
      return Promise.reject(error);
    }
    return Promise.reject(error);
  },
);

// 封装 GET 请求方法
export const get = async <T = any>(
  url: string,
  params?: object,
): Promise<IResult<T>> => {
  try {
    const response = (await instance.get(url, { params })) as IResult<T>;
    return response;
  } catch (error) {
    return Promise.reject(error);
  }
};

// 封装 POST 请求方法
export const post = async <T = any>(
  url: string,
  data?: object,
): Promise<IResult<T>> => {
  try {
    const response = (await instance.post(url, data)) as IResult<T>;
    return response;
  } catch (error) {
    return Promise.reject(error);
  }
};

async function refreshToken() {
  const res = await get('/auth/refresh', {
    params: {
      refresh_token: localStorage.getItem('refreshToken'),
    },
  });
  const autoLogin = localStorage.getItem('autoLogin');
  if (autoLogin) {
    localStorage.setItem('accessToken', res.data.accessToken || '');
    localStorage.setItem('refreshToken', res.data.refreshToken || '');
  } else {
    sessionStorage.setItem('accessToken', res.data.accessToken || '');
    sessionStorage.setItem('refreshToken', res.data.refreshToken || '');
  }

  return res;
}

src\apis\types.ts

ts 复制代码
export type TloginParams = {
  tel: string
  code: string,
};

export interface IUserInfo {
  id: string
  desc: string
  name: string
  tel: string
  avatar: string
}
export interface ILoginRes {
  accessToken:string
  refreshToken: string
  userInfo: IUserInfo
}

src\apis\login.ts

ts 复制代码
import { post } from '.';
import { ILoginRes, TloginParams } from './types';

// 发送验证码消息
export const sendCodeMsg = (tel: string) => post('auth/sendCodeMsg', { tel });

// 登录
export const login = (params:TloginParams) => post<ILoginRes>('auth/login', params);

路由

src\router\index.tsx

定义路由组件数组

ts 复制代码
import { lazy } from 'react';

import { RouteObject } from 'react-router-dom';

const Login = lazy(() => import('@/views/login'));
const Home = lazy(() => import('@/views/home'));
const Page404 = lazy(() => import('@/views/404'));

export const routes: RouteObject[] = [
  {
    path: '/',
    element: <Home />,
  },
  {
    path: '/login',
    element: <Login />,
  },
  {
    path: '*',
    element: <Page404 />,
  },
];

src\App.tsx

App中用useRoutes注册

ts 复制代码
import { useRoutes } from 'react-router-dom';
import { routes } from './router';

function App() {
  return useRoutes(routes);
}

export default App;

src\main.tsx

使用BrowserRouter包裹App

ts 复制代码
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import 'normalize.css';
import AuthRoute from './components/AuthRoute';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <BrowserRouter>
     <App />
  </BrowserRouter>,
);

路由守卫

由于react-router-dom没有路由守卫,通过高阶组件实现 src\components\AuthRoute.tsx

  1. 如果token存在且有值,禁止用户回到登录页,重定向到首页
  2. 如果token不存在且访问的路径在白名单中可以跳转,不在白名单则重定向至登录页
ts 复制代码
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';

type RouteProps = {
  children?: React.ReactNode
};
const loginRoute = '/login';
const indexRoute = '/';
// 路由表白名单
const allowList = ['/login', '/register'];

const AuthRoute: React.FC<RouteProps> = (props) => {
  const location = useLocation();
  // children 为子组件
  const { children } = props;
 
  const token = localStorage.getItem('accessToken') || sessionStorage.getItem('accessToken');
  if (token && token !== 'undefined') {
    // 有 token 的状态下禁止用户回到登录页,重定向到首页
    if (location.pathname === loginRoute) {
      return <Navigate to={indexRoute} />;
    }
    // 其他路由均可正常跳转
    return <>{children}</>;
  }
  // 无 token 的状态下,如果要跳转的路由是白名单中的路由,正常跳转
  if (allowList.includes(location.pathname || '')) {
    return <>{children}</>;
  }
  // 无 token 且非白名单路由,重定向至登录页
  return <Navigate to={loginRoute} />;
};
export default AuthRoute;

src\main.tsx

AuthRoute组件包裹App组件

ts 复制代码
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import 'normalize.css';
import AuthRoute from './components/AuthRoute';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <BrowserRouter>
    <AuthRoute>
      <App />
    </AuthRoute>
  </BrowserRouter>,
);

结果

发送短信成功

未勾选自动登录,把token保存到sessionStorage 勾选自动登录,把token保存到localStorage

相关推荐
Sam902910 分钟前
【Webpack--013】SourceMap源码映射设置
前端·webpack·node.js
Python私教1 小时前
Go语言现代web开发15 Mutex 互斥锁
开发语言·前端·golang
A阳俊yi1 小时前
Vue(13)——router-link
前端·javascript·vue.js
小明说Java1 小时前
Vue3祖孙组件通信探秘:运用provide与inject实现跨层级数据传递
前端
好看资源平台1 小时前
前端框架对比与选择:如何在现代Web开发中做出最佳决策
前端·前端框架
4triumph1 小时前
Vue.js教程笔记
前端·vue.js
程序员大金1 小时前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
清灵xmf2 小时前
提前解锁 Vue 3.5 的新特性
前端·javascript·vue.js·vue3.5
白云~️2 小时前
监听html元素是否被删除,删除之后重新生成被删除的元素
前端·javascript·html
金灰2 小时前
有关JS下隐藏的敏感信息
前端·网络·安全