前面我们写了用户端的一些页面,这节继续来写管理端的。
涉及到这些页面:
这节我们来写前两个。
先新建个 react 项目:
npx create-react-app --template=typescript meeting_room_booking_system_frontend_admin
进入项目目录,把开发服务跑起来:
npm run start
浏览器访问 http://localhost:3000 可以看到这个界面:
就说明 react 项目成功跑起来了。
然后我们添加 router:
npm install --save react-router-dom
在 index.tsx 加上路由的配置:
import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider, createBrowserRouter, Link, Outlet } from 'react-router-dom';
function Index() {
return <div>index<Outlet></Outlet></div>
}
function ErrorPage() {
return <div>Error Page</div>
}
function UserManage() {
return <div>user manage</div>
}
function Login() {
return <div>login</div>
}
const routes = [
{
path: "/",
element: <Index></Index>,
errorElement: <ErrorPage />,
children: [
{
path: 'user_manage',
element: <UserManage/>
}
]
},
{
path: "login",
element: <Login />,
}
];
const router = createBrowserRouter(routes);
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(<RouterProvider router={router}/>);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
配置了 4 个路由:
访问 /login 的时候,渲染 Login 组件。
访问 / 的时候,渲染 Index 组件。
访问 /user_manage 的时候,渲染 / 和 user_manage 的二级路由,也就是 Index + UserManage 组件。
以及出错的时候,渲染 ErrorPage 组件。
测试下:
都没问题。
把 src 目录下其余文件去掉:
然后创建 4 个组件:
src/pages/Login/Login.tsx
export function Login() {
return <div>login</div>
}
2
3
src/pages/Index/Index.tsx
import { Outlet } from "react-router-dom";
export function Index() {
return <div>Index<Outlet></Outlet></div>
}
2
3
4
5
src/pages/UserManage/UserManage.tsx
export function UserManage() {
return <div>UserManage</div>
}
2
3
src/pages/ErrorPage/ErrorPage.tsx
export function ErrorPage() {
return <div>Error Page</div>
}
2
3
改下 index.tsx 配置对应的路由:
import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider, createBrowserRouter, Link, Outlet } from 'react-router-dom';
import { Index } from './pages/Index/Index';
import { ErrorPage } from './pages/ErrorPage/ErrorPage';
import { UserManage } from './pages/UserManage/UserManage';
import { Login } from './pages/Login/Login';
const routes = [
{
path: "/",
element: <Index></Index>,
errorElement: <ErrorPage />,
children: [
{
path: 'user_manage',
element: <UserManage/>
}
]
},
{
path: "login",
element: <Login />,
}
];
const router = createBrowserRouter(routes);
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(<RouterProvider router={router}/>);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
测试下:
都没啥问题。
然后来写 Login 页面:
引入 Ant Design 组件库:
npm install antd --save
在 Login 组件引入 DatePicker 组件:
import { DatePicker } from "antd";
export function Login() {
return <div><DatePicker/></div>
}
2
3
4
5
没啥问题,说明 antd 引入成功了。
然后我们把登录页面写一下:
import { Button, Checkbox, Form, Input } from 'antd';
import './login.css';
import { useCallback } from 'react';
interface LoginUser {
username: string;
password: string;
}
const layout1 = {
labelCol: { span: 4 },
wrapperCol: { span: 20 }
}
export function Login() {
const onFinish = useCallback((values: LoginUser) => {
console.log(values);
}, []);
return <div id="login-container">
<h1>会议室预订系统</h1>
<Form
{...layout1}
onFinish={onFinish}
colon={false}
autoComplete="off"
>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true, message: '请输入用户名!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: '请输入密码!' }]}
>
<Input.Password />
</Form.Item>
<Form.Item label=" ">
<Button className='btn' type="primary" htmlType="submit">
登录
</Button>
</Form.Item>
</Form>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
这里和用户端差不多.
login.css 如下:
#login-container {
width: 400px;
margin: 100px auto 0 auto;
text-align: center;
}
#login-container .links {
display: flex;
justify-content: space-between;
}
#login-container .btn {
width: 100%;
}
2
3
4
5
6
7
8
9
10
11
12
访问 /login,可以看到现在的登录页面:
然后看一下接口文档 http://localhost:3005/api-doc
传入用户名、密码、返回用户信息和 token。
在 postman 里测试下登录接口:
然后在点击登录按钮之后,用 axios 调用它:
安装 axios:
npm install axios
在前端项目创建个 src/interfaces/interfaces.ts
import axios from "axios";
const axiosInstance = axios.create({
baseURL: 'http://localhost:3005/',
timeout: 3000
});
export async function login(username: string, password: string) {
return await axiosInstance.post('/user/admin/login', {
username, password
});
}
2
3
4
5
6
7
8
9
10
11
12
在这里集中管理接口。
然后 onFinish 里调用:
const navigate = useNavigate();
const onFinish = useCallback(async (values: LoginUser) => {
const res = await login(values.username, values.password);
const { code, message: msg, data} = res.data;
if(res.status === 201 || res.status === 200) {
message.success('登录成功');
localStorage.setItem('access_token', data.accessToken);
localStorage.setItem('refresh_token', data.refreshToken);
localStorage.setItem('user_info', JSON.stringify(data.userInfo));
setTimeout(() => {
navigate('/');
}, 1000);
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}, []);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这里和用户端一摸一样。
登录下:
提示 400 错误没处理。
因为接口返回 400 的时候,axios 会抛异常:
我们加一个响应的 interceptor,返回 error.response 而不是 Promise.reject(error.response)
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
return error.response;
}
);
2
3
4
5
6
7
8
再测试下:
当用户不存在时:
当密码错误时:
登录成功时:
都没啥问题。
这样,管理员登录的前后端功能就都完成了。
然后是用户管理的页面:
修改下 Index.tsx
import { UserOutlined } from "@ant-design/icons";
import { Outlet } from "react-router-dom";
import './index.css';
export function Index() {
return <div id="index-container">
<div className="header">
<h1>会议室预定系统-后台管理</h1>
<UserOutlined className="icon"/>
</div>
<div className="body">
<Outlet></Outlet>
</div>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这里用到了 antd 的 icon 组件,需要安装用到的包:
npm install @ant-design/icons --save
css 如下:
#index-container{
height: 100vh;
display: flex;
flex-direction: column;
}
#index-container .header{
height: 80px;
border-bottom: 1px solid #aaa;
line-height: 80px;
display: flex;
justify-content: space-between;
padding: 0 20px;
}
#index-container h1{
margin: 0;
}
#index-container .icon {
font-size: 40px;
margin-top: 20px;
}
#index-container .body{
flex: 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
测试下:
没啥问题。
不知道同学们有没有发现,其实这个页面应该是三级路由:
因为左边这部分也是要多个页面共用的。
我们改一下路由配置:
const routes = [
{
path: "/",
element: <Index></Index>,
errorElement: <ErrorPage />,
children: [
{
path: "/",
element: <Menu></Menu>,
children: [
{
path: 'user_manage',
element: <UserManage/>
}
]
}
]
},
{
path: "login",
element: <Login />,
}
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
添加 src/pages/Menu/Menu.tsx
import { Outlet } from "react-router-dom";
export function Menu() {
return <div>
menu <Outlet></Outlet>
</div>
}
2
3
4
5
6
7
渲染出来是这样的:
我们来写一下 Menu 组件:
import { Outlet } from "react-router-dom";
import { Menu as AntdMenu, MenuProps } from 'antd';
import './menu.css';
const items: MenuProps['items'] = [
{
key: '1',
label: "会议室管理"
},
{
key: '2',
label: "预定管理"
},
{
key: '3',
label: "用户管理"
},
{
key: '4',
label: "统计"
}
];
export function Menu() {
return <div id="menu-container">
<div className="menu-area">
<AntdMenu
defaultSelectedKeys={['3']}
items={items}
/>
</div>
<div className="content-area">
<Outlet></Outlet>
</div>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
menu.css 如下:
#menu-container {
display: flex;
flex-direction: row;
}
#menu-container .menu-area {
width: 200px;
}
2
3
4
5
6
7
渲染出来是这样的:
然后来写 UserManage 组件:
可以分为 2 部分,上面的搜索表单、下面的结果表格。
我们来写一下:
import { Button, Form, Input, Table } from "antd";
import { useCallback } from "react";
import './UserManage.css';
interface SearchUser {
username: string;
nickName: string;
email: string;
}
export function UserManage() {
const searchUser = useCallback(async (values: SearchUser) => {
console.log(values);
}, []);
return <div id="userManage-container">
<div className="userManage-form">
<Form
onFinish={searchUser}
name="search"
layout='inline'
colon={false}
>
<Form.Item label="用户名" name="username">
<Input />
</Form.Item>
<Form.Item label="昵称" name="nickName">
<Input />
</Form.Item>
<Form.Item label="邮箱" name="email" rules={[
{ type: "email", message: '请输入合法邮箱地址!'}
]}>
<Input/>
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索用户
</Button>
</Form.Item>
</Form>
</div>
<div className="userManage-table">
</div>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
UserManage.css
#userManage-container {
padding: 20px;
}
2
3
先把 form 部分写完。
测试下:
然后再写 table 部分:
import { Button, Form, Input, Table } from "antd";
import { useCallback } from "react";
import './UserManage.css';
import { ColumnsType } from "antd/es/table";
interface SearchUser {
username: string;
nickName: string;
email: string;
}
interface UserSearchResult {
username: string;
nickName: string;
email: string;
headPic: string;
createTime: Date;
}
const columns: ColumnsType<UserSearchResult> = [
{
title: '用户名',
dataIndex: 'username'
},
{
title: '头像',
dataIndex: 'headPic'
},
{
title: '昵称',
dataIndex: 'nickName'
},
{
title: '邮箱',
dataIndex: 'email'
},
{
title: '注册时间',
dataIndex: 'createTime'
}
];
const data = [
{
key: '1',
username: 'xx',
headPic: 'xxx.png',
nickName: 'xxx',
email: 'xx@xx.com',
createTime: new Date()
},
{
key: '12',
username: 'yy',
headPic: 'yy.png',
nickName: 'yyy',
email: 'yy@yy.com',
createTime: new Date()
}
]
export function UserManage() {
const searchUser = useCallback(async (values: SearchUser) => {
console.log(values);
}, []);
return <div id="userManage-container">
<div className="userManage-form">
<Form
onFinish={searchUser}
name="search"
layout='inline'
colon={false}
>
<Form.Item label="用户名" name="username">
<Input />
</Form.Item>
<Form.Item label="昵称" name="nickName">
<Input />
</Form.Item>
<Form.Item label="邮箱" name="email" rules={[
{ type: "email", message: '请输入合法邮箱地址!'}
]}>
<Input/>
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索用户
</Button>
</Form.Item>
</Form>
</div>
<div className="userManage-table">
<Table columns={columns} dataSource={data} pagination={ {
pageSize: 10
}}/>
</div>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
渲染出来是这样的:
然后我们调用下搜索接口。
看下接口文档:
在 postman 里调用下:
这个接口是需要登录的。
我们先登录一下:
带上 access_token 再访问:
返回了 8 条数据。
然后我们在页面里调用下:
先把之前写的 axios 的 interceptors 自动添加 authorization 的 header,自动 refresh token 的逻辑拿过来:
axiosInstance.interceptors.request.use(function (config) {
const accessToken = localStorage.getItem('access_token');
if(accessToken) {
config.headers.authorization = 'Bearer ' + accessToken;
}
return config;
})
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
let { data, config } = error.response;
if (data.code === 401 && !config.url.includes('/user/admin/refresh')) {
const res = await refreshToken();
if(res.status === 200 || res.status === 201) {
return axiosInstance(config);
} else {
message.error(res.data);
setTimeout(() => {
window.location.href = '/login';
}, 1500);
}
} else {
return error.response;
}
}
)
async function refreshToken() {
const res = await axiosInstance.get('/user/admin/refresh', {
params: {
refresh_token: localStorage.getItem('refresh_token')
}
});
localStorage.setItem('access_token', res.data.access_token);
localStorage.setItem('refresh_token', res.data.refresh_token);
return res;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
然后添加一个接口:
export async function userSearch(username: string, nickName: string, email: string, pageNo: number, pageSize: number) {
return await axiosInstance.get('/user/list', {
params: {
username,
nickName,
email,
pageNo,
pageSize
}
});
}
2
3
4
5
6
7
8
9
10
11
在页面调用下:
const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [userResult, setUserResult] = useState<UserSearchResult[]>();
const searchUser = useCallback(async (values: SearchUser) => {
const res = await userSearch(values.username,values.nickName, values.email, pageNo, pageSize);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
setUserResult(data.users.map((item: UserSearchResult) => {
return {
key: item.username,
...item
}
}))
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}, []);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用 useState 创建 pageNo、pageSize、userResult 这三个状态。
请求接口,成功后把数据设置到 userResult。
然后修改下 table 的 dataSource:
测试下:
先登录。
然后访问 http://localhost:3000/user_manage
搜索接口对接成功。
然后再对接下分页:
<Table columns={columns} dataSource={userResult} pagination={ {
current: pageNo,
pageSize: pageSize,
onChange: changePage
}}/>
2
3
4
5
设置 pageNo 和 pageSize,并监听 onChange 事件
useEffect(() => {
searchUser({
username: '',
email: '',
nickName: ''
});
}, [pageNo, pageSize]);
const changePage = useCallback(function(pageNo: number, pageSize: number) {
setPageNo(pageNo);
setPageSize(pageSize);
}, []);
2
3
4
5
6
7
8
9
10
11
12
分页设置改变的时候,设置 pageNo 和 pageSize。
并且 useEffect 监听这两个 state,在变化的时候,重新发送请求。
这样,刚进入页面的时候,就会触发一次渲染,并且在分页设置改变时也会触发:
然后修改下 headPic,改为图片:
const columns: ColumnsType<UserSearchResult> = [
{
title: '用户名',
dataIndex: 'username'
},
{
title: '头像',
dataIndex: 'headPic',
render: value => {
return value ? <Image
width={50}
src={`http://localhost:3005/${value}`}
/> : '';
}
},
{
title: '昵称',
dataIndex: 'nickName'
},
{
title: '邮箱',
dataIndex: 'email'
},
{
title: '注册时间',
dataIndex: 'createTime'
}
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
这里用的是 antd 的 Image 组件,有预览的功能:
原型图还有个冻结功能:
看下接口文档:
很简单,就是个 get 接口。
我们在表格里加一列:
{
title: '操作',
render: (_, record) => (
<a href="#" onClick={() => {freezeUser(record.id)}}>冻结</a>
)
}
2
3
4
5
6
这里用到了 id,我们在类型里加一下:
然后在 interfaces.tsx 添加这个接口:
export async function freeze(id: number) {
return await axiosInstance.get('/user/freeze', {
params: {
id
}
});
}
2
3
4
5
6
7
在组件里创建 freezeUser 方法:
async function freezeUser(id: number) {
const res = await freeze(id);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
message.success('冻结成功');
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}
2
3
4
5
6
7
8
9
10
测试下:
然后我们还要把冻结状态显示出来:
这部分数据是返回了的:
需要添加一列:
{
title: '状态',
dataIndex: 'isFrozen',
render: (_, record) => (
record.isFrozen ? <Badge status="success">已冻结</Badge> : ''
)
},
2
3
4
5
6
7
在类型部分也要添加下:
测试下:
冻结之后,刷新页面,会显示已冻结。
这里我们在冻结之后自动刷新下。
这需要把逻辑移到组件内:
把 columns 移到组件内,用 useMemo 包裹,这样只会创建一次:
freeezeUser 也是:
const freezeUser = useCallback(async (id: number) => {
const res = await freeze(id);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
message.success('冻结成功');
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}, []);
2
3
4
5
6
7
8
9
10
添加一个 num 的 state,冻结之后设置一个随机值:
把它添加到 useEffect 的依赖里,这样就能触发重新搜索:
测试下:
但其实现在这个重新搜索有问题:
我搜索之后再冻结,然后刷新就丢失了搜索条件了。
这里需要搜索的时候带上当前的条件:
用 useForm 拿到 form 的 api:
然后在搜索的时候拿到最新的表单值:
useEffect(() => {
searchUser({
username: form.getFieldValue('username'),
email: form.getFieldValue('email'),
nickName: form.getFieldValue('nickName')
});
}, [pageNo, pageSize, num]);
2
3
4
5
6
7
这样就可以了:
这样,用户管理页面就写完了。
全部代码如下:
import { Badge, Button, Form, Image, Input, Table, message } from "antd";
import { useCallback, useEffect, useMemo, useState } from "react";
import './UserManage.css';
import { ColumnsType } from "antd/es/table";
import { freeze, userSearch } from "../../interfaces/interfaces";
import { useForm } from "antd/es/form/Form";
interface SearchUser {
username: string;
nickName: string;
email: string;
}
interface UserSearchResult {
id: number,
username: string;
nickName: string;
email: string;
headPic: string;
createTime: Date;
isFrozen: boolean;
}
export function UserManage() {
const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [userResult, setUserResult] = useState<UserSearchResult[]>();
const [num, setNum] = useState(0);
const columns: ColumnsType<UserSearchResult> = useMemo(() => [
{
title: '用户名',
dataIndex: 'username'
},
{
title: '头像',
dataIndex: 'headPic',
render: value => {
return value ? <Image
width={50}
src={`http://localhost:3005/${value}`}
/> : '';
}
},
{
title: '昵称',
dataIndex: 'nickName'
},
{
title: '邮箱',
dataIndex: 'email'
},
{
title: '注册时间',
dataIndex: 'createTime'
},
{
title: '状态',
dataIndex: 'isFrozen',
render: (_, record) => (
record.isFrozen ? <Badge status="success">已冻结</Badge> : ''
)
},
{
title: '操作',
render: (_, record) => (
<a href="#" onClick={() => {freezeUser(record.id)}}>冻结</a>
)
}
], []);
const freezeUser = useCallback(async (id: number) => {
const res = await freeze(id);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
message.success('冻结成功');
setNum(Math.random())
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}, []);
const searchUser = useCallback(async (values: SearchUser) => {
const res = await userSearch(values.username,values.nickName, values.email, pageNo, pageSize);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
setUserResult(data.users.map((item: UserSearchResult) => {
return {
key: item.username,
...item
}
}))
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}, []);
const [form ] = useForm();
useEffect(() => {
searchUser({
username: form.getFieldValue('username'),
email: form.getFieldValue('email'),
nickName: form.getFieldValue('nickName')
});
}, [pageNo, pageSize, num]);
const changePage = useCallback(function(pageNo: number, pageSize: number) {
setPageNo(pageNo);
setPageSize(pageSize);
}, []);
return <div id="userManage-container">
<div className="userManage-form">
<Form
form={form}
onFinish={searchUser}
name="search"
layout='inline'
colon={false}
>
<Form.Item label="用户名" name="username">
<Input />
</Form.Item>
<Form.Item label="昵称" name="nickName">
<Input />
</Form.Item>
<Form.Item label="邮箱" name="email" rules={[
{ type: "email", message: '请输入合法邮箱地址!'}
]}>
<Input/>
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索用户
</Button>
</Form.Item>
</Form>
</div>
<div className="userManage-table">
<Table columns={columns} dataSource={userResult} pagination={ {
current: pageNo,
pageSize: pageSize,
onChange: changePage
}}/>
</div>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
案例代码上传了小册仓库 (opens new window)。
# 总结
这节我们实现了管理端的登录和用户管理页面。
和用户端的一样,都是通过 axios interceptor 自动添加 header 和自动 refresh token。
这里涉及到三级路由,第一级展示上面的 header,第二级展示左侧的 menu,第三级才是具体的页面。
使用 table 组件来渲染列表,通过 useEffect 在 pageNo、pageSize 改变的时候自动重发请求。
这样,这两个页面的前后端代码都完成了。