教育平台前端-登录页

页面效果

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

页面结构

使用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

相关推荐
大怪v8 分钟前
【Virtual World 04】我们的目标,无限宇宙!!
前端·javascript·代码规范
狂炫冰美式29 分钟前
不谈技术,搞点文化 🧀 —— 从复活一句明代残诗破局产品迭代
前端·人工智能·后端
xw51 小时前
npm几个实用命令
前端·npm
!win !1 小时前
npm几个实用命令
前端·npm
代码狂想家1 小时前
使用openEuler从零构建用户管理系统Web应用平台
前端
dorisrv3 小时前
优雅的React表单状态管理
前端
蓝瑟3 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式
dorisrv3 小时前
高性能的懒加载与无限滚动实现
前端
韭菜炒大葱4 小时前
别等了!用 Vue 3 让 AI 边想边说,字字蹦到你脸上
前端·vue.js·aigc
StarkCoder4 小时前
求求你,别在 Swift 协程开头写 guard let self = self 了!
前端