这节来写用户端的会议室列表:
现在,用户端首页是这样的:
需要在 / 下添加一个二级路由:
{
path: '/',
element: <Menu/>,
children: [
{
path: '/',
element: <MeetingRoomList/>
},
{
path: 'meeting_room_list',
element: <MeetingRoomList/>
},
{
path: 'booking_history',
element: <BookingHistory/>
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后分别实现这三个组件:
src/page/menu/Menu.tsx
import { Outlet, useLocation } from "react-router-dom";
import { Menu as AntdMenu, MenuProps } from 'antd';
import './menu.css';
import { MenuClickEventHandler } from "rc-menu/lib/interface";
import { router } from "../..";
const items: MenuProps['items'] = [
{
key: '1',
label: "会议室列表"
},
{
key: '2',
label: "预定历史"
}
];
const handleMenuItemClick: MenuClickEventHandler = (info) => {
let path = '';
switch(info.key) {
case '1':
path = '/meeting_room_list';
break;
case '2':
path = '/booking_history';
break;
}
router.navigate(path);
}
export function Menu() {
const location = useLocation();
function getSelectedKeys() {
if(location.pathname === '/meeting_room_list') {
return ['1']
} else if(location.pathname === '/booking_history') {
return ['2']
} else {
return ['1']
}
}
return <div id="menu-container">
<div className="menu-area">
<AntdMenu
defaultSelectedKeys={getSelectedKeys()}
items={items}
onClick={handleMenuItemClick}
/>
</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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
引入 antd 的 Menu 实现菜单。
渲染的时候根据 useLocation 拿到的 pathname 来设置选中的菜单项。
点击菜单项的时候用 router.push 修改路径。
这里用到的 router 需要在 index.tsx 导出:
这些我们前面写过一遍。
menu.css 如下:
#menu-container {
display: flex;
flex-direction: row;
}
#menu-container .menu-area {
width: 200px;
}
2
3
4
5
6
7
然后是 src/pages/meeting_room_list/MeetingRoomList.tsx
export function MeetingRoomList() {
return <div>MeetingRoomList</div>
}
2
3
还有 src/pages/booking_history/BookingHistory.tsx
export function BookingHistory() {
return <div>BookingHistory</div>
}
2
3
在 index.tsx 里导入这些组件后,我们跑起来看看:
npm run start:dev
点击菜单项的路由切换,以及刷新选中对应菜单项,都没问题。
然后来写下列表页面,其实这个和管理端的会议室列表差不多:
我们把那个复制过来改改。
首先,在 interfaces.ts 添加用到的接口:
export async function searchMeetingRoomList(name: string, capacity: number, equipment: string, pageNo: number, pageSize: number) {
return await axiosInstance.get('/meeting-room/list', {
params: {
name,
capacity,
equipment,
pageNo,
pageSize
}
});
}
2
3
4
5
6
7
8
9
10
11
然后写下列表:
import { Badge, Button, Form, Input, Popconfirm, Table, message } from "antd";
import { useCallback, useEffect, useMemo, useState } from "react";
import './meeting_room_list.css';
import { ColumnsType } from "antd/es/table";
import { useForm } from "antd/es/form/Form";
import { searchMeetingRoomList } from "../../interface/interfaces";
interface SearchMeetingRoom {
name: string;
capacity: number;
equipment: string;
}
interface MeetingRoomSearchResult {
id: number,
name: string;
capacity: number;
location: string;
equipment: string;
description: string;
isBooked: boolean;
createTime: Date;
updateTime: Date;
}
export function MeetingRoomList() {
const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [meetingRoomResult, setMeetingRoomResult] = useState<Array<MeetingRoomSearchResult>>([]);
const columns: ColumnsType<MeetingRoomSearchResult> = useMemo(() => [
{
title: '名称',
dataIndex: 'name'
},
{
title: '容纳人数',
dataIndex: 'capacity',
},
{
title: '位置',
dataIndex: 'location'
},
{
title: '设备',
dataIndex: 'equipment'
},
{
title: '描述',
dataIndex: 'description'
},
{
title: '添加时间',
dataIndex: 'createTime'
},
{
title: '上次更新时间',
dataIndex: 'updateTime'
},
{
title: '预定状态',
dataIndex: 'isBooked',
render: (_, record) => (
record.isBooked ? <Badge status="error">已被预订</Badge> : <Badge status="success">可预定</Badge>
)
},
{
title: '操作',
render: (_, record) => (
<div>
<a href="#">预定</a>
</div>
)
}
], []);
const searchMeetingRoom = useCallback(async (values: SearchMeetingRoom) => {
const res = await searchMeetingRoomList(values.name, values.capacity, values.equipment, pageNo, pageSize);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
setMeetingRoomResult(data.meetingRooms.map((item: MeetingRoomSearchResult) => {
return {
key: item.id,
...item
}
}))
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}, []);
const [form ] = useForm();
useEffect(() => {
searchMeetingRoom({
name: form.getFieldValue('name'),
capacity: form.getFieldValue('capacity'),
equipment: form.getFieldValue('equipment')
});
}, [pageNo, pageSize]);
const changePage = useCallback(function(pageNo: number, pageSize: number) {
setPageNo(pageNo);
setPageSize(pageSize);
}, []);
return <div id="meetingRoomList-container">
<div className="meetingRoomList-form">
<Form
form={form}
onFinish={searchMeetingRoom}
name="search"
layout='inline'
colon={false}
>
<Form.Item label="会议室名称" name="name">
<Input />
</Form.Item>
<Form.Item label="容纳人数" name="capacity">
<Input />
</Form.Item>
<Form.Item label="设备" name="equipment">
<Input/>
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索会议室
</Button>
</Form.Item>
</Form>
</div>
<div className="meetingRoomList-table">
<Table columns={columns} dataSource={meetingRoomResult} 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
上面是 form、下面是 table。
调用搜索接口来搜索列表数据,然后设置到 table 的 dataSource。
每次分页变化的时候重新搜索。
然后 css 部分如下:
#meetingRoomList-container {
padding: 20px;
}
#meetingRoomList-container .meetingRoomList-form {
margin-bottom: 40px;
}
2
3
4
5
6
这样,列表页就完成了:
其实写这个模块的时候偷懒了,应该是写完后端接口,还要写 swager 文档。
然后前端根据 swagger 接口文档才能知道传啥参数,有啥返回值。
当时我们没写 swagger 文档,现在补一下:
打开后端项目,在 MeetingRoomController 里加一下 swagger 相关的装饰器:
首先加一下 delete 接口的:
@ApiParam({
name: 'id',
type: Number,
description: 'id'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'success'
})
2
3
4
5
6
7
8
9
访问 http://localhost:3005/api-doc (opens new window) 可以看到这个接口的文档:
其实会议室的接口都是需要登录才能访问的,当时为了测试方便没有加,现在加一下:
添加 @RequireLogin 装饰器,标识接口需要登录。
并且添加对应的 @ApiBearerAuth 的 swagger 装饰器,代表需要添加 Bearer 的 header。
我们现在 postman 里测试下:
这时候直接调用 delete 接口就会提示需要先登录了。
然后我们登录下,拿到 token。
把它复制到 swagger 文档这里:
然后点击这个 try it out:
数据库中现在有 3 条记录:
把 id 为 10 那条删掉。
点击 execute:
swagger 会发送请求,下面会打印响应:
这时数据库里就没有这条记录了:
可以直接在 swagger 文档里测试接口,不用 postman 也行。
然后继续写下个接口的 swagger 文档:
这个接口的参数也是用 @ApiParam 标识,但它的响应不是 string,而是 MeetingRoom。
而我们现在并没有 vo,没地方标识属性:
所以要创建个 vo:
新建 src/meeting-room/vo/meeting-room.vo.ts
import { ApiProperty } from "@nestjs/swagger";
export class MeetingRoomVo {
@ApiProperty()
id: number;
@ApiProperty()
name: string;
@ApiProperty()
capacity: number;
@ApiProperty()
location: string;
@ApiProperty()
equipment: string;
@ApiProperty()
description: string;
@ApiProperty()
isBooked: boolean;
@ApiProperty()
createTime: Date;
@ApiProperty()
updateTime: Date;
}
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
然后加一下 swagger 的装饰器:
@ApiBearerAuth()
@ApiParam({
name: 'id',
type: Number,
})
@ApiResponse({
status: HttpStatus.OK,
description: 'success',
type: MeetingRoomVo
})
2
3
4
5
6
7
8
9
10
试一下:
接下来是 update 接口:
他有两种响应:
分别写一下:
@ApiBearerAuth()
@ApiBody({
type: UpdateMeetingRoomDto,
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '会议室不存在'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'success'
})
2
3
4
5
6
7
8
9
10
11
12
然后在 dto 里标注下属性:
因为 update 的 dto 继承了 create 的 dto,所以那里也要加一下:
这样 swagger 文档就对了:
然后是 create 接口:
postman 里调用下是这样的:
所以 swagger 装饰器这样写:
@ApiBearerAuth()
@ApiBody({
type: CreateMeetingRoomDto,
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: '会议室名字已存在'
})
@ApiResponse({
status: HttpStatus.OK,
type: MeetingRoomVo
})
2
3
4
5
6
7
8
9
10
11
12
这样 swagger 文档显示的就对了:
然后还有最后一个 list 接口:
它的响应是这样的:
首先创建响应数据的 vo:
src/meeting-room/vo/meeting-room-list.vo.ts
import { ApiProperty } from "@nestjs/swagger";
import { MeetingRoomVo } from "./meeting-room.vo";
export class MeetingRoomListVo {
@ApiProperty({
type: [MeetingRoomVo]
})
users: Array<MeetingRoomVo>;
@ApiProperty()
totalCount: number;
}
2
3
4
5
6
7
8
9
10
11
12
13
然后加一下 swagger 的装饰器:
@ApiBearerAuth()
@ApiQuery({
name: 'pageNo',
type: Number,
required: false
})
@ApiQuery({
name: 'pageSize',
type: Number,
required: false
})
@ApiQuery({
name: 'name',
type: String,
required: false
})
@ApiQuery({
name: 'capacity',
type: String,
required: false
})
@ApiQuery({
name: 'equipment',
type: String,
required: false
})
@ApiResponse({
type: MeetingRoomListVo
})
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
有同学说,不用把 service 里的返回值改成 MeetingRoomListVo 对象么?
不用,只要结构对上就行。
最后,在 controller 上加上个 @ApiTags,把下面的接口分到单独一组:
这样,用户端的会议室列表页面,swagger 文档就都完成了。
案例代码上传了小册仓库:
# 总结
这节我们写了用户端的会议室列表页,并且补了 swagger 文档。
用户端列表页就是调用 list 接口,通过 form 来填写参数,通过 table 展示结果。
swagger 文档部分就是分别通过 @ApiPram @ApiQuery @ApiBody @ApiResponse 标识接口,通过 @ApiProperty 标识 dto 和 vo 的属性。
这样,会议室模块的前端后端就都完成了。