• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

后台管理系统登录功能的实现

武飞扬头像
小羽苏
帮助1

之前完成了项目初始化以及第三方工具的引入,现在开始登录功能的实现。

布局

首先完成登录页的布局,最后效果如下
学新通

  1. 创建如图的目录结构,在index.tsx中编写登录相关代码
    学新通
  2. 整体布局这里没有用antd,将其划分为左边与右边部分,通过felx布局实现,具体样式通过styled-components创建,如下
import styled from "styled-components";

const LoginWarp = styled.div`
  width: 100vw;
  height: 100vh;
  display: flex;
  align-items: center;
`;
const Left = styled.div`
  height: 100vh;
  width: 65%;
  min-width: 25rem;
  background-color: #74759b;
`;
const Right = styled.div`
  height: 100vh;
  width: 35%;
  min-width: 20rem;
  background-color: #2b73af80;
`;

const Login = () => {
  return (
    <LoginWarp>
      <Left></Left>
      <Right></Right>
    </LoginWarp>
  );
};
export default Login;
学新通

这里没有做成响应式,只是通过min-width设置了最小宽度

  1. 接下来实现左边部分具体内容,左边只有一段内容,同样使用styled-component创建
const Title = styled.h3`
  font-size: 1.5rem;
  text-align: center;
`;
  1. 接下来调整LeftTitle居中,这里也是用flex布局,简单设置两个属性即可
const Left = styled.div`
  height: 100vh;
  width: 65%;
  min-width: 25rem;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #74759b;
`;

到这里左边部分相关布局就做完了

  1. 先观察右半部分的结构,为了方便布局,将有内容的部分,看作一个整体
    学新通
    可以通过一个Warp进行包裹
const Warp = styled.div`
  width: 55%;
  height: 50%;
  `
  1. Warp的内容就是一个标题、一段说明以及一个表单,标题用之前创建的Title就行,说明部分我们可以看见它是在分割线中的,在antd中正好有一个Divider组件可以实现,所以这个和表单都用antd提供的组件即可,右边部分整体结构就如下
      <Right>
        <Warp>
          <Title>Welcome Back</Title>
          <Divider style={{ color: "rgb(209,213,220)" }}>账号密码登录</Divider>
          <Form
            name="basic"
            wrapperCol={{ span: 24 }}
            initialValues={{ remember: true }}
            style={{ height: "50%" }}
            autoComplete="off"
          >
            <Form.Item
              name="username"
              rules={[{ required: true, message: "用户名不能为空" }]}
            >
              <Input
                placeholder="请输入用户名"
                prefix={<UserOutlined style={{ color: "rgb(209,213,220)" }} />}
              />
            </Form.Item>

            <Form.Item
              name="password"
              rules={[{ required: true, message: "密码不能为空" }]}
            >
              <Input.Password
                placeholder="请输入密码"
                prefix={<LockOutlined style={{ color: "rgb(209,213,220)" }} />}
              />
            </Form.Item>

            <Form.Item wrapperCol={{ span: 24 }}>
              <Button
                type="primary"
                htmlType="submit"
                shape="round"
                onClick={submit}
                style={{ width: "100%", backgroundColor: "#74759b70" }}
              >
                登录
              </Button>
            </Form.Item>
          </Form>
        </Warp>
      </Right>
学新通
  1. 在微调一下Right的样式,Login的布局就可以了
const Right = styled.div`
  display: flex;
  height: 100vh;
  width: 35%;
  min-width: 20rem;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
  background-color: #2b73af80;
`;

这里的元素用到了styled-componentsantd所以具体用法还是要看看官网,上面的styled只是最基本的用法,还有继承啥的还没有用,antd组件的话从官网复制过来在稍微改改样式属性就差不多了

styled-components的使用
antd组件

逻辑

布局在前面差不多就完成了,接下来就是登录逻辑部分,既然要提交表单的话就要先获取表单的内容,这里我们通过useState创建响应式数据,在将输入框绑定onChange事件,触发时获取数据

interface UserInfo { // 用户信息类型,在 Login 函数外边定义
  username: string;
  password: string;
}
// 创建user相关的state,修改时通过 setUserinfo,修改
const [userinfo, setUserinfo] = useState<UserInfo>({
    username: "",
    password: "",
  });
// 定义一个 handler 当 onChange 事件触发调用
const getUserinfo = (e: ChangeEvent<HTMLInputElement>) => {
	// 规定传入的 e 是后边这样一个类型
    const { type, value } = e.target;
    // 通过判断 type,分辨是 普通输入框还是密码
    if (type === "text") {
      setUserinfo({
        ...userinfo,
        username: value,
      });
    } else {
      setUserinfo({
        ...userinfo,
        password: value,
      });
    }
  };
// 事件绑定
  <Input
         placeholder="请输入用户名"
         onChange={(e) => getUserinfo(e)}
         prefix={<UserOutlined style={{ color: "rgb(209,213,220)" }} />}
              />
   <Input.Password
         placeholder="请输入密码"
         onChange={(e) => getUserinfo(e)}
         prefix={<LockOutlined style={{ color: "rgb(209,213,220)" }} />}
              />
学新通

vue不同,react响应式数据要通过特点方法修改才能重新渲染,useState方法返回的第二个值就是该方法

发送数据

上面获取完表单内容之后,接下来该发送数据了,之之前已经封装过了axios了,所以现在只需要添加一个对应的api请求就可以了,如下
学新通
login.ts中添加请求,根据接口文档,可以编写如下接口

import request from "../request";

interface UserInfo {
  username: string;
  password: string;
}

const login = async (userinfo: UserInfo) => {
  return await request.post({
    url: "admin/login",
    data: userinfo,
  });
};
export default login;

接口文档

有了这个接口方法之后,我们需要在合适的时机发送请求,这里也就是添加登录按钮时发送,所以给Button绑定事件

// views/Login/index.ts
  const submit = async () => {
    const res = await login(userinfo); // 拿到响应结果
  };
  // 绑定事件
 <Button
       type="primary"
       htmlType="submit"
       shape="round"
       onClick={submit}
       style={{ width: "100%", backgroundColor: "#74759b70" }}
  >
      登录
 </Button>

当前接口如果成功,会返回一个token,我们需要保存这个token,便于记录登录状态,接下来就是如何保存toekn

保存token

这里我选择存在localStorage中,首先封装一下,然后直接导出一个实例化对象供外界使用

// utils/storage.ts
class Storage {
  private instance = localStorage;

  getItem(key: string) {
    const value = this.instance.getItem(key);
    if (value) {
      return value;
    }
    return "";
  }

  setItem(key: string, value: unknown) {
    if (typeof value === "string") {
      this.instance.setItem(key, value);
    } else {
      this.instance.setItem(key, JSON.stringify(value));
    }
  }

  removeItem(key: string) {
    this.instance.removeItem(key);
  }
}

export default new Storage();
学新通

因为token用到的可能性比较高,所以在单独封装几个方法用于、获取、存入、删除

// utils/index.ts
export const saveToken = (token: string) => {
  storage.setItem("token", token);
};
export const getToken = () => {
  return storage.getItem("token");
};
export const removeToken = () => {
  return storage.removeItem("token");
};

而存入toekn的时间就在表单提交后,并拿到结果时,所以修改上面的sumbit方法

  const submit = async () => {
    const res = await login(userinfo);// 这里axios对响应做了拦截,所以下面取token直接从data中获取
    const { token } = res.data;
    if (token) { // 取得token就保存
      saveToken(token);
    } else {
      // 没有token执行的操作
    }
  };

axios响应拦截如下

// service/request/index.ts
 const request = new Request(config); // config 在之前 引入工具包时已经写了,要是不知道就看看
		request.instance.interceptors.response.use(
		  (res) => { //这里根据响应结果变就好了
		    return { ...res, data: res.data.data };
		  },
		  (error) => {
		    return promise.reject(err);
		  },
		);

到这一步,登录功能就差不多做完了,但是会发现,拿到token之后,当前界面还是停留在Login界面,并没有进行跳转,所以接下来的工作就是如果成功拿到token就跳转到首页

跳转首页

之前已经在react-router配置过了首页(//home),现在先去在Home添加一些内容方便一会分清

const Home = () => {
  return (
  	<h1>Home</h1>
  );
};
export default Home;

那在什么时候跳转路由呢?由上一部分可以看出,当表单提交并成功取得token时就应该进行跳转,所以修改sumbit方法

// views/Login/index.tsx
 const navigate = useNavigate();
 const submit = async () => {
    const res = await login(userinfo);
    const { token } = res.data;
    if (token) {
      saveToken(token);
      navigate("/");
    } else {
    
    }
  };

reactrouter 中提供 一个hook useNavigate,可以让我们跳转路由
调用 useNavigate 返回一个函数,该函数的第一个参数就是我们要到的路由

到这一步,当成功登录时就可以跳转到首页了,但是实际上,现在不管有没有token我们都可以通过修改URL的方式来到首页,我们需要的效果应该是只有有了token才可以来到首页,并且如果没有token的话应该除了登录页都不能去,接下来就实现这个功能

控制路由跳转

vue-router提供了导航守卫的方法如beforeEach,但是react-router中好像没有,所以这里就自己稍微实现了一下相关效果

我没有找到,要是各位大佬知道有的话,还请告诉小弟学新通

主要是通过三个hook实现

  • react 的 useEffect
  • router 的 useLocation 与 useNavigate
// src/permission.ts
import { useLocation, useNavigate } from "react-router-dom";
import { useEffect } from "react";
import { getToken } from "./utils";
const useGuardRouter = () => {
  const location = useLocation();
  const navigate = useNavigate();
  const token = getToken();

  useEffect(() => {
    if (token) { // 如果存在token
      if (location.pathname === "/login") { 
      // 并且当前是login页, 就跳转到首页
        navigate("/");
      }
    } else if (!token && location.pathname !== "/login") {
       // token不存在,且是login以外的页面,就跳到登录
      navigate("/login");
    }
  }, [token, navigate, location]);
};
export { useGuardRouter };
学新通

然后在App.tsx中引入使用即可,之后存在token就不能跳转到登录页,不存在就只能到登录页了。接下来就是登录成功后获取管理员数据了。

获取管理员数据

同理,先根据文档创一个接口

// service/api/manger.ts
import request from "../request";

import type { Manger } from "./types/manger"; // 根据接口文旦生成的接口作为类型

const getMangerInfo = async (token: string): Promise<Manger> => {
  const { data } = await request.post({
    url: "admin/getinfo",
    headers: {
      token,
    },
  });
  return data;
};
export default getMangerInfo;

我将获取时期放在了,判断token是否存在并跳转时,也就是permission.ts中,但是并不是直接调用该方法获取,因为这里的数据应该把他放在redux中,所以要先去创建一个切片,并在该切片中进行获取。

// store/reducer/mangerReducer.ts
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import storage from "utils/storage";
import { getMangerInfo } from "service/index";
import { MenuType } from "service/api/types/manger";
import { RootState } from "../index";

const initialState = { // 初始值
  username: "",
  avatar: "",
  menus: [] as MenuType[],
  roleNames: [] as string[],
};
const fetchMangerInfo = createAsyncThunk("user/fetchMangerInfo", async () => {
  let token = storage.getItem("token");
  return await getMangerInfo(token);
}); // 创建一个异步方法,用于获取数据
const userSlice = createSlice({
  name: "userinfo",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(fetchMangerInfo.fulfilled, (state, action) => {
      state.username = action.payload.username;
      state.menus = action.payload.menus;
      state.roleNames = action.payload.ruleNames;
      state.avatar = action.payload.avatar;
    });
  },
});
export default userSlice.reducer;
export const selectUsername = (state: RootState) => state.user.username;
export const selectMenus = (state: RootState) => state.user.menus;
export const selectAvatar = (state: RootState) => state.user.avatar;
export const selectRoleNames = (state: RootState) => state.user.roleNames;

export { fetchMangerInfo };
学新通

因为redux不能有直接的异步,之前通过redux-thunk来处理,而现在使用的RTK提供了createAsyncThunk方法来简化使用异步actio的过程:

  1. 首先通过其创建一个异步action,
  2. 然后在 slicerextraReducers 进行配置,这里我选择的是当fetchMangerInfo处于fulfilled状态也就是调用 resolve后要进行的操作
  3. 将数据存入 state

更多查看createAsyncThunk用法

这样就创建好了这个切片,接下来进行使用

// store/index.ts
const store = configureStore({
  // 创建 store
  reducer: {
    main: mainReducer,
    user: userReducer,
  },
});

然后在permission.ts中调用

// src/`permission.ts
import { useLocation, useNavigate } from "react-router-dom";
import { useEffect } from "react";
import { useAppDispatch } from "./store/hooks";
import { getToken, hideLoading, showFullLoading } from "./utils";
import { fetchMangerInfo } from "./store/reducer/mangerReducer";
const useGuardRouter = () => {
  const location = useLocation();
  const navigate = useNavigate();
  const dispatch = useAppDispatch();
  const token = getToken();

  useTitle();
  useEffect(() => {
    showFullLoading();
    if (token) {
      dispatch(init());
      dispatch(fetchMangerInfo());
      if (location.pathname === "/login") {
        navigate("/", {
          state: "首页",
        });
      }
    } else if (!token && location.pathname !== "/login") {
      navigate("/login", {
        state: "登录",
      });
    }
    hideLoading();
  }, [token, navigate, location, dispatch]);
};
export { useGuardRouter };
学新通

这样就可以获取管理员数据,并存入redux中了
学新通
数据到这儿也可以成功获取了,现在登录功能还差一个,退出登录

退出登录

退出登录同样是调一个接口,按照接口文档先实现一下

// service/api/logout.ts
import request from "../request";

const logout = async (token: string) => {
  const res = await request.post({
    url: "/admin/logout",
    headers: {
      token,
    },
  });
  return res.status;
};
export default logout;

然后在首页中触发一下退出进行调用,所以我们创建一个按钮并绑定事件

import React from "react";
import { Button } from "antd";

import styled from "styled-components";
import { useAppSelector } from "../../store/hooks";
import { selectUsername } from "../../store/reducer/mangerReducer";
import { selectToken } from "../../store/reducer/mainReducer";
import { logout } from "service";
import { useNavigate } from "react-router-dom";

const HomeWrap = styled.div`
  color: #fff;
  text-align: center;
`;

const Home = () => {
  const username = useAppSelector(selectUsername); // mangerSlicer中的数据,也就是管理员名
  const navigate = useNavigate();
  const token = useAppSelector(selectToken);
  const logoutHandler = () => {
    logout(token).then((res) => {
      if (res === 200) { // 这里我们写接口时,直接返回的是 status,所以这样判断
      // 如果成功的话就条状回登录
        navigate("/login");
      }
    });
  };

  return (
    <HomeWrap>
      <h1> {username ? username : "Hello"}</h1>
      <Button onClick={logoutHandler}>点我退出</Button>
    </HomeWrap>
  );
};
export default Home;
学新通

但是这样完之后,还不太行,因为这里我们用localstorage存的数据,所以还跳不出去,删除一下token就好

// views/Home/index.ts
import { removeToken } from "../../utils";
const Home = () => {
  const logoutHandler = () => {
    logout(token).then((res) => {
      if (res === 200) {
        removeToken();
        navigate("/login");
      }
    });
  };
};
export default Home;

结束语

到这大部分登录功能就实现了,还有一些小细节没有写,例如切换路由时顶部的进度条,还有token除了在localstorage中保存以外还在redux中保存了一份,代码实现了,但是我觉得有点啰嗦就没写这上面,如果觉得有意思的话可以clone一下 项目地址
学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgafaik
系列文章
更多 icon
同类精品
更多 icon
继续加载