后台管理系统登录功能的实现
之前完成了项目初始化以及第三方工具的引入,现在开始登录功能的实现。
布局
首先完成登录页的布局,最后效果如下
- 创建如图的目录结构,在
index.tsx
中编写登录相关代码 - 整体布局这里没有用
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
设置了最小宽度
- 接下来实现左边部分具体内容,左边只有一段内容,同样使用
styled-component
创建
const Title = styled.h3`
font-size: 1.5rem;
text-align: center;
`;
- 接下来调整
Left
让Title
居中,这里也是用flex
布局,简单设置两个属性即可
const Left = styled.div`
height: 100vh;
width: 65%;
min-width: 25rem;
display: flex;
justify-content: center;
align-items: center;
background-color: #74759b;
`;
到这里左边部分相关布局就做完了
- 先观察右半部分的结构,为了方便布局,将有内容的部分,看作一个整体
可以通过一个Warp
进行包裹
const Warp = styled.div`
width: 55%;
height: 50%;
`
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>
- 在微调一下
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-components
与antd
所以具体用法还是要看看官网,上面的styled
只是最基本的用法,还有继承啥的还没有用,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的过程:
- 首先通过其创建一个异步action,
- 然后在
slicer
的extraReducers
进行配置,这里我选择的是当fetchMangerInfo
处于fulfilled
状态也就是调用resolve
后要进行的操作- 将数据存入 state
这样就创建好了这个切片,接下来进行使用
// 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
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01 -
怎样阻止微信小程序自动打开
PHP中文网 06-13