这节来实现预定管理模块的后端。
涉及到这些接口:
| 接口路径 | 请求方式 | 描述 |
|---|---|---|
| /booking/list | GET | 预订列表 |
| /booking/approve | POST | 审批预订申请 |
| /booking/add | POST | 申请预订 |
| /booking/apply/:id | GET | 通过预订 |
| /booking/reject/:id | GET | 取消预订 |
| /booking/unbind/:id | GET | 解除预订 |
| /booking/history | GET | 预订历史 |
| /booking/urge | GET | 催办 |
来写一下。
先创建 Booking 的 entity。
在后端项目下创建一个 booking 模块:
nest g resource booking然后修改 booking.entity.ts
根据当时设计的表来写:
| 字段名 | 数据类型 | 描述 |
|---|---|---|
| id | INT | 预订ID |
| user_id | INT | 预订用户ID |
| room_id | INT | 会议室ID |
| start_time | DATETIME | 会议开始时间 |
| end_time | DATETIME | 会议结束时间 |
| status | VARCHAR(20) | 状态(申请中、审批通过、审批驳回、已解除) |
| note | VARCHAR(100) | 备注 |
| create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 |
import { MeetingRoom } from "src/meeting-room/entities/meeting-room.entity";
import { User } from "src/user/entities/user.entity";
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class Booking {
@PrimaryGeneratedColumn()
id: number;
@Column({
comment: '会议开始时间'
})
startTime: Date;
@Column({
comment: '会议结束时间'
})
endTime: Date;
@Column({
length: 20,
comment: '状态(申请中、审批通过、审批驳回、已解除)',
default: '申请中'
})
status: string;
@Column({
length: 100,
comment: '备注',
default: ''
})
note: string;
@ManyToOne(() => User)
user: User;
@ManyToOne(() => MeetingRoom)
room: MeetingRoom;
@CreateDateColumn({
comment: '创建时间'
})
createTime: Date;
@UpdateDateColumn({
comment: '更新时间'
})
updateTime: Date;
}这里 Booking 和 User、MeetingRoom 是多对一的关系:
在 entities 引入:
typeorm 会自动建表和创建外键:
在 mysql workbench 里可以看到这个表:
这两个外键都是 restrict 的约束:
restrict 是只有从表没有相关记录,才可以删除主表记录。
但会议室是支持删除的,怎么办呢?
可以在删除失败时提示有预订记录,然后手动取消预订后再删除。
这个后面再处理。
先插入一些数据:
在 BookingService 增加一个 initData 方法:
@InjectEntityManager()
private entityManager: EntityManager;
async initData() {
const user1 = await this.entityManager.findOneBy(User, {
id: 1
});
const user2 = await this.entityManager.findOneBy(User, {
id: 2
});
const room1 = await this.entityManager.findOneBy(MeetingRoom, {
id: 3
});
const room2 = await this.entityManager.findOneBy(MeetingRoom, {
id: 6
});
const booking1 = new Booking();
booking1.room = room1;
booking1.user = user1;
booking1.startTime = new Date();
booking1.endTime = new Date(Date.now() + 1000 * 60 * 60);
await this.entityManager.save(Booking, booking1);
const booking2 = new Booking();
booking2.room = room2;
booking2.user = user2;
booking2.startTime = new Date();
booking2.endTime = new Date(Date.now() + 1000 * 60 * 60);
await this.entityManager.save(Booking, booking2);
const booking3 = new Booking();
booking3.room = room1;
booking3.user = user2;
booking3.startTime = new Date();
booking3.endTime = new Date(Date.now() + 1000 * 60 * 60);
await this.entityManager.save(Booking, booking3);
const booking4 = new Booking();
booking4.room = room2;
booking4.user = user1;
booking4.startTime = new Date();
booking4.endTime = new Date(Date.now() + 1000 * 60 * 60);
await this.entityManager.save(Booking, booking4);
}先查询出 2 个 User ,2 个 MeetingRoom,然后创建 4 个 Booking。
用 save 把 4 条记录保存到数据库。
用 repl 的方式跑起来:
npm run repl调用 initData 方法:
await get(BookingService).initData()会有一系列 insert 语句:
在数据库的 booking 表可以看到插入了 4 条记录:
然后来写 booking 模块的接口:
首先是 list 接口。
它支持根据条件搜索,并且可以分页:
来写一下:
@Get('list')
async list(
@Query('pageNo', new DefaultValuePipe(1), generateParseIntPipe('pageNo')) pageNo: number,
@Query('pageSize', new DefaultValuePipe(10), generateParseIntPipe('pageSize')) pageSize: number,
@Query('username') username: string,
@Query('meetingRoomName') meetingRoomName: string,
@Query('meetingRoomPosition') meetingRoomPosition: string,
@Query('bookingTimeRangeStart') bookingTimeRangeStart: number,
@Query('bookingTimeRangeEnd') bookingTimeRangeEnd: number,
) {
return this.bookingService.find(pageNo, pageSize, username, meetingRoomName, meetingRoomPosition, bookingTimeRangeStart, bookingTimeRangeEnd);
}这种列表接口写过,就是传入分页参数、搜索参数,然后在 service 里把它们查出来返回。
这里的时间用 number 来接收。
去 BookingService 里实现下这个方法:
async find(pageNo: number, pageSize: number, username: string, meetingRoomName: string, meetingRoomPosition: string, bookingTimeRangeStart: number, bookingTimeRangeEnd: number ) {
const skipCount = (pageNo - 1) * pageSize;
const [bookings, totalCount] = await this.entityManager.findAndCount(Booking, {
where: {
user: {
username: Like(`%${username}%`)
},
room: {
name: Like(`%${meetingRoomName}%`),
location: Like(`%${meetingRoomPosition}%`)
},
startTime: Between(new Date(bookingTimeRangeStart), new Date(bookingTimeRangeEnd))
},
relations: {
user: true,
room: true,
},
skip: skipCount,
take: pageSize
});
return {
bookings,
totalCount
}
}很容易看懂,就是接个 where 条件,还有分页。
要注意下日期的范围查询使用 between and 语法,这里使用 Between 操作符。
先测试下。
停掉服务,用 repl 的方式测:
npm run repl先看下有啥数据:
然后测试下:
在 repl 拿到两个时间戳:
new Date('2023-09-29').getTime()new Date('2023-09-30').getTime()调用下这个方法:
await get(BookingService).find(1, 10, 'guang', '天王', '三层', 1695945600000, 1696032000000)打印了一堆 sql,下面有查询的结果:
查询出来的是 id 为 1 和 3 的记录。
因为条件是 user.name 包含 guang:
room.name 包含天王:
所以查出这两条是对的:
查询逻辑写完了,还得优化下。
因为这些参数是可选的,要处理下没有传入的情况:
async find(pageNo: number, pageSize: number, username: string, meetingRoomName: string, meetingRoomPosition: string, bookingTimeRangeStart: number, bookingTimeRangeEnd: number ) {
const skipCount = (pageNo - 1) * pageSize;
const condition: Record<string, any> = {};
if(username) {
condition.user = {
username: Like(`%${username}%`)
}
}
if(meetingRoomName) {
condition.room = {
name: Like(`%${meetingRoomName}%`)
}
}
if(meetingRoomPosition) {
if (!condition.room) {
condition.room = {}
}
condition.room.location = Like(`%${meetingRoomPosition}%`)
}
if(bookingTimeRangeStart) {
if(!bookingTimeRangeEnd) {
bookingTimeRangeEnd = bookingTimeRangeStart + 60 * 60 * 1000
}
condition.startTime = Between(new Date(bookingTimeRangeStart), new Date(bookingTimeRangeEnd))
}
const [bookings, totalCount] = await this.entityManager.findAndCount(Booking, {
where: condition,
relations: {
user: true,
room: true,
},
skip: skipCount,
take: pageSize
});
return {
bookings,
totalCount
}
}就是如果传入了,就加到 condition 上。
其中,如果 endTime 没传入,那就用 startTime + 一小时 来搜索。
此外,这里查询出来 user 信息是包含密码的,其实应该把它去掉:
可以在这里指定 select 的字段:
但这样有点麻烦。
直接查出来之后把它删掉就好了:
bookings.map(item => {
delete item.user.password;
return item;
})把 repl 停掉,把服务跑起来:
npm run start:dev在 postman 里测试下:
http://localhost:3005/booking/list?meetingRoomName=天王&username=guang没啥问题。
接下来是申请预订的接口
| 接口路径 | 请求方式 | 描述 |
|---|---|---|
| /booking/list | GET | 预订列表 |
| /booking/add | POST | 申请预订 |
在 BookingController 添加一个接口:
@Post('add')
@RequireLogin()
async add(@Body() booking: CreateBookingDto, @UserInfo('userId') userId: number) {
await this.bookingService.add(booking, userId);
return 'success'
}这里需要用 @UserInfo 拿到 userId。
从 request.user 拿到 userId 的信息,需要登录,所以添加 @RequireLogin 装饰器。
因为在 LoginGuard 里做了判断,只有有这个装饰器的 handler 才会从 header 中解析出用户信息放在 request.user 上:
创建用到的 dto:
import { IsNotEmpty, IsNumber } from "class-validator";
export class CreateBookingDto {
@IsNotEmpty({ message: '会议室名称不能为空'})
@IsNumber()
meetingRoomId: number;
@IsNotEmpty({ message: '开始时间不能为空' })
@IsNumber()
startTime: number;
@IsNotEmpty({ message: '结束时间不能为空' })
@IsNumber()
endTime: number;
note: string;
}然后在 BookingService 实现下 add 方法:
async add(bookingDto: CreateBookingDto, userId: number) {
const meetingRoom = await this.entityManager.findOneBy(MeetingRoom, {
id: bookingDto.meetingRoomId
});
if(!meetingRoom) {
throw new BadRequestException('会议室不存在');
}
const user = await this.entityManager.findOneBy(User, {
id: userId
});
const booking = new Booking();
booking.room = meetingRoom;
booking.user = user;
booking.startTime = new Date(bookingDto.startTime);
booking.endTime = new Date(bookingDto.endTime);
await this.entityManager.save(Booking, booking);
}就是根据 id 查询出 meetingRoom 和 user,然后创建 booking,保存。
测试下:
先登录拿到 token:
带在 Authorization 的 header 上访问 add 接口:
{
"meetingRoomId": 3,
"startTime": 1703986859333,
"endTime": 1703987859333
}在 mysql workbench 查询下,可以看到记录成功插入了:
当然,现在的接口还是有问题的,得限制下,同一个会议室一段时间内只能被预订一次。
那怎么保证预订的时间不会冲突呢?
其实一般的会议室预订系统都是这样做的:
在右边列出来会议室在一天之内哪些时间可用,哪些时间被预订了。
然后只能在没有被预订的时间内选择。
这里就简化一下,查询下已经预订的记录里有没有包含这段时间的就好了。
async add(bookingDto: CreateBookingDto, userId: number) {
const meetingRoom = await this.entityManager.findOneBy(MeetingRoom, {
id: bookingDto.meetingRoomId
});
if(!meetingRoom) {
throw new BadRequestException('会议室不存在');
}
const user = await this.entityManager.findOneBy(User, {
id: userId
});
const booking = new Booking();
booking.room = meetingRoom;
booking.user = user;
booking.startTime = new Date(bookingDto.startTime);
booking.endTime = new Date(bookingDto.endTime);
const res = await this.entityManager.findOneBy(Booking, {
room: {
id: meetingRoom.id
},
startTime: LessThanOrEqual(booking.startTime),
endTime: MoreThanOrEqual(booking.endTime)
});
if(res) {
throw new BadRequestException('该时间段已被预订');
}
await this.entityManager.save(Booking, booking);
}测试下:
{
"meetingRoomId": 3,
"startTime": 1703986959333,
"endTime": 1703986859333
}当预订一个已经被预订的时间段时,会提示已被预订。
然后继续写后面接口:
| 接口路径 | 请求方式 | 描述 |
|---|---|---|
| /booking/list | GET | 预订列表 |
| /booking/add | POST | 申请预订 |
| /booking/apply/:id | GET | 通过预订 |
| /booking/reject/:id | GET | 取消预订 |
| /booking/unbind/:id | GET | 解除预订 |
接下来写修改预订状态的这三个接口。
状态有这 4 种:
在 BookingController 添加三个路由:
@Get("apply/:id")
async apply(@Param('id') id: number) {
return this.bookingService.apply(id);
}
@Get("reject/:id")
async reject(@Param('id') id: number) {
return this.bookingService.reject(id);
}
@Get("unbind/:id")
async unbind(@Param('id') id: number) {
return this.bookingService.unbind(id);
}然后在 BookingService 里实现这三个方法:
async apply(id: number) {
await this.entityManager.update(Booking, {
id
}, {
status: '审批通过'
});
return 'success'
}
async reject(id: number) {
await this.entityManager.update(Booking, {
id
}, {
status: '审批驳回'
});
return 'success'
}
async unbind(id: number) {
await this.entityManager.update(Booking, {
id
}, {
status: '已解除'
});
return 'success'
}postman 里测试下:
http://localhost:3005/booking/apply/1http://localhost:3005/booking/reject/2http://localhost:3005/booking/reject/3在 mysql workbench 里可以看到状态成功被修改了:
接下来是催办的接口:
| 接口路径 | 请求方式 | 描述 |
|---|---|---|
| /booking/list | GET | 预订列表 |
| /booking/apply/:id | GET | 通过预订 |
| /booking/reject/:id | GET | 取消预订 |
| /booking/unbind/:id | GET | 解除预订 |
| /booking/urge | GET | 催办 |
按当时的设计,催办要发送邮件和短信,不过没买短信服务,这里就发邮件好了。
但也不是每次催办都会发邮件,在 redis 里加个标识,半小时内只发一次邮件。
在 BookingController 添加一个 urge 接口:
@Get('urge/:id')
async urge(@Param('id') id: number) {
return this.bookingService.urge(id);
}然后在 BookingService 添加实现逻辑:
@Inject(RedisService)
private redisService: RedisService;
@Inject(EmailService)
private emailService: EmailService;
async urge(id: number) {
const flag = await this.redisService.get('urge_' + id);
if(flag) {
return '半小时内只能催办一次,请耐心等待';
}
let email = await this.redisService.get('admin_email');
if(!email) {
const admin = await this.entityManager.findOne(User, {
select: {
email: true
},
where: {
isAdmin: true
}
});
email = admin.email
this.redisService.set('admin_email', admin.email);
}
this.emailService.sendMail({
to: email,
subject: '预订申请催办提醒',
html: `id 为 ${id} 的预订申请正在等待审批`
});
this.redisService.set('urge_' + id, 1, 60 * 30);
}注入了 EmailService 和 RedisService。
先用 redisService 查询 flag,查到的话就提醒半小时内只能催办一次。
然后用 redisService 查询 admin 的邮箱,没查到的话到数据库查,然后存到 redis。
之后发催办邮件,并且在 redis 里存一个 30 分钟的 flag。
测试下:
第一次催办,管理员会收到邮件:
第二次催办,会提示半小时只能催办一次:
在 RedisInsight 里可以看到这两个 key:
这样,催办接口就完成了。
当然,这里最好是在邮件里带一个具体的链接,点击可以直接打开对应的页面来处理申请。
等后面写完这个页面再改。
案例代码上传了
这节完成了预定管理模块的后端代码,包括列表、添加预订、审批、催办等。
后端代码完成了,下节来写前端部分的代码。
这节写下预定管理模块管理端的前端部分:
对应的路由前面写过了:
这节来填充下内容。
首先是 table 部分:
import { Badge, Button, DatePicker, Form, Input, Popconfirm, Table, TimePicker, message } from "antd";
import { useEffect, useState } from "react";
import { ColumnsType } from "antd/es/table";
import { UserSearchResult } from "../UserManage/UserManage";
import { MeetingRoomSearchResult } from "../MeetingRoomManage/MeetingRoomManage";
import dayjs from "dayjs";
interface BookingSearchResult {
id: number;
startTime: string;
endTime: string;
status: string;
note: string;
createTime: string;
updateTime: string;
user: UserSearchResult,
room: MeetingRoomSearchResult
}
export function BookingManage() {
const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [bookingSearchResult] = useState<Array<BookingSearchResult>>([]);
const columns: ColumnsType<BookingSearchResult> = [
{
title: '会议室名称',
dataIndex: 'room',
render(_, record) {
return record.room.name
}
},
{
title: '会议室位置',
dataIndex: 'room',
render(_, record) {
return record.room.location
}
},
{
title: '预订人',
dataIndex: 'user',
render(_, record) {
return record.user.username
}
},
{
title: '开始时间',
dataIndex: 'startTime',
render(_, record) {
return dayjs(new Date(record.startTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '结束时间',
dataIndex: 'endTime',
render(_, record) {
return dayjs(new Date(record.endTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '审批状态',
dataIndex: 'status'
},
{
title: '预订时间',
dataIndex: 'createTime',
render(_, record) {
return dayjs(new Date(record.createTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '备注',
dataIndex: 'note'
},
{
title: '描述',
dataIndex: 'description'
},
{
title: '操作',
render: (_, record) => (
<div>
</div>
)
}
];
const changePage = function(pageNo: number, pageSize: number) {
setPageNo(pageNo);
setPageSize(pageSize);
}
return <div id="bookingManage-container">
<div className="bookingManage-table">
<Table columns={columns} dataSource={bookingSearchResult} pagination={ {
current: pageNo,
pageSize: pageSize,
onChange: changePage
}}/>
</div>
</div>
}列表接口是这样的:
根据这个指定表格列的定义,并且添加分页的处理。
其中涉及到的 User 和 Room 的类型从其他页面导入:
这里用到 dayjs 来格式化日期,安装下:
npm install dayjs然后加上上面的表单:
import { Button, DatePicker, Form, Input, Popconfirm, Table, TimePicker, message } from "antd";
import { useEffect, useState } from "react";
import { ColumnsType } from "antd/es/table";
import { useForm } from "antd/es/form/Form";
import './booking_manage.css';
import { UserSearchResult } from "../UserManage/UserManage";
import { MeetingRoomSearchResult } from "../MeetingRoomManage/MeetingRoomManage";
import dayjs from "dayjs";
export interface SearchBooking {
username: string;
meetingRoomName: string;
meetingRoomPosition: string;
rangeStartDate: Date;
rangeStartTime: Date;
rangeEndDate: Date;
rangeEndTime: Date;
}
interface BookingSearchResult {
id: number;
startTime: string;
endTime: string;
status: string;
note: string;
createTime: string;
updateTime: string;
user: UserSearchResult,
room: MeetingRoomSearchResult
}
export function BookingManage() {
const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [bookingSearchResult, setBookingSearchResult] = useState<Array<BookingSearchResult>>([]);
const [num, setNum] = useState(0);
const columns: ColumnsType<BookingSearchResult> = [
{
title: '会议室名称',
dataIndex: 'room',
render(_, record) {
return record.room.name
}
},
{
title: '会议室位置',
dataIndex: 'room',
render(_, record) {
return record.room.location
}
},
{
title: '预订人',
dataIndex: 'user',
render(_, record) {
return record.user.username
}
},
{
title: '开始时间',
dataIndex: 'startTime',
render(_, record) {
return dayjs(new Date(record.startTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '结束时间',
dataIndex: 'endTime',
render(_, record) {
return dayjs(new Date(record.endTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '审批状态',
dataIndex: 'status'
},
{
title: '预订时间',
dataIndex: 'createTime',
render(_, record) {
return dayjs(new Date(record.createTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '备注',
dataIndex: 'note'
},
{
title: '描述',
dataIndex: 'description'
},
{
title: '操作',
render: (_, record) => (
<div>
</div>
)
}
];
const searchBooking = async (values: SearchBooking) => {
}
const [form ] = useForm();
useEffect(() => {
searchBooking({
username: form.getFieldValue('username'),
meetingRoomName: form.getFieldValue('meetingRoomName'),
meetingRoomPosition: form.getFieldValue('meetingRoomPosition'),
rangeStartDate: form.getFieldValue('rangeStartDate'),
rangeStartTime: form.getFieldValue('rangeStartTime'),
rangeEndDate: form.getFieldValue('rangeEndDate'),
rangeEndTime: form.getFieldValue('rangeEndTime')
});
}, [pageNo, pageSize, num]);
const changePage = function(pageNo: number, pageSize: number) {
setPageNo(pageNo);
setPageSize(pageSize);
}
return <div id="bookingManage-container">
<div className="bookingManage-form">
<Form
form={form}
onFinish={searchBooking}
name="search"
layout='inline'
colon={false}
>
<Form.Item label="预订人" name="username">
<Input />
</Form.Item>
<Form.Item label="会议室名称" name="meetingRoomName">
<Input />
</Form.Item>
<Form.Item label="预订开始日期" name="rangeStartDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订开始时间" name="rangeStartTime">
<TimePicker/>
</Form.Item>
<Form.Item label="预订结束日期" name="rangeEndDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订结束时间" name="rangeEndTime">
<TimePicker/>
</Form.Item>
<Form.Item label="位置" name="meetingRoomPosition">
<Input />
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索预订申请
</Button>
</Form.Item>
</Form>
</div>
<div className="bookingManage-table">
<Table columns={columns} dataSource={bookingSearchResult} pagination={ {
current: pageNo,
pageSize: pageSize,
onChange: changePage
}}/>
</div>
</div>
}涉及到的 css 如下:
#bookingManage-container {
padding: 20px;
}
#bookingManage-container .bookingManage-form {
margin-bottom: 40px;
}
#bookingManage-container .ant-form-item {
margin: 10px;
}
渲染出来是这样的:
这里要注意的是日期和时间分别要用 DatePicker 和 TimePicker,所以分为 2 个字段。
接下来实现下用到的接口,改下 interfaces.ts
export async function bookingList(searchBooking: SearchBooking, pageNo: number, pageSize: number) {
let bookingTimeRangeStart;
let bookingTimeRangeEnd;
if(searchBooking.rangeStartDate && searchBooking.rangeStartTime) {
const rangeStartDateStr = dayjs(searchBooking.rangeStartDate).format('YYYY-MM-DD');
const rangeStartTimeStr = dayjs(searchBooking.rangeStartTime).format('HH:mm');
bookingTimeRangeStart = dayjs(rangeStartDateStr + ' ' + rangeStartTimeStr).valueOf()
}
if(searchBooking.rangeEndDate && searchBooking.rangeEndTime) {
const rangeEndDateStr = dayjs(searchBooking.rangeEndDate).format('YYYY-MM-DD');
const rangeEndTimeStr = dayjs(searchBooking.rangeEndTime).format('HH:mm');
bookingTimeRangeEnd = dayjs(rangeEndDateStr + ' ' + rangeEndTimeStr).valueOf()
}
return await axiosInstance.get('/booking/list', {
params: {
username: searchBooking.username,
meetingRoomName: searchBooking.meetingRoomName,
meetingRoomPosition: searchBooking.meetingRoomPosition,
bookingTimeRangeStart,
bookingTimeRangeEnd,
pageNo: pageNo,
pageSize: pageSize
}
});
}
export async function apply(id: number) {
return await axiosInstance.get('/booking/apply/' + id);
}
export async function reject(id: number) {
return await axiosInstance.get('/booking/reject/' + id);
}
export async function unbind(id: number) {
return await axiosInstance.get('/booking/unbind/' + id);
}apply、reject、unbind 接口比较简单,列表接口相对麻烦一些。
因为现在日期和时间分为了 2 个字段,而接口只接收一个字段,所以要把它们合并。
用 dayjs 分别把日期和时间 format 成 YYYY-MM-DD 和 HH:mm 的格式。
然后拼接成一个字符串之后,再创建 dayjs 实例,这样时间就合并成一个了。
在页面里调用下列表接口:
import { Button, DatePicker, Form, Input, Popconfirm, Table, TimePicker, message } from "antd";
import { useEffect, useState } from "react";
import { ColumnsType } from "antd/es/table";
import { useForm } from "antd/es/form/Form";
import { apply, bookingList, reject, unbind } from "../../interfaces/interfaces";
import './booking_manage.css';
import { UserSearchResult } from "../UserManage/UserManage";
import { MeetingRoomSearchResult } from "../MeetingRoomManage/MeetingRoomManage";
import dayjs from "dayjs";
export interface SearchBooking {
username: string;
meetingRoomName: string;
meetingRoomPosition: string;
rangeStartDate: Date;
rangeStartTime: Date;
rangeEndDate: Date;
rangeEndTime: Date;
}
interface BookingSearchResult {
id: number;
startTime: string;
endTime: string;
status: string;
note: string;
createTime: string;
updateTime: string;
user: UserSearchResult,
room: MeetingRoomSearchResult
}
export function BookingManage() {
const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [bookingSearchResult, setBookingSearchResult] = useState<Array<BookingSearchResult>>([]);
const [num, setNum] = useState(0);
const columns: ColumnsType<BookingSearchResult> = [
{
title: '会议室名称',
dataIndex: 'room',
render(_, record) {
return record.room.name
}
},
{
title: '会议室位置',
dataIndex: 'room',
render(_, record) {
return record.room.location
}
},
{
title: '预订人',
dataIndex: 'user',
render(_, record) {
return record.user.username
}
},
{
title: '开始时间',
dataIndex: 'startTime',
render(_, record) {
return dayjs(new Date(record.startTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '结束时间',
dataIndex: 'endTime',
render(_, record) {
return dayjs(new Date(record.endTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '审批状态',
dataIndex: 'status',
},
{
title: '预订时间',
dataIndex: 'createTime',
render(_, record) {
return dayjs(new Date(record.createTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '备注',
dataIndex: 'note'
},
{
title: '描述',
dataIndex: 'description'
},
{
title: '操作',
render: (_, record) => (
<div>
</div>
)
}
];
const searchBooking = async (values: SearchBooking) => {
const res = await bookingList(values, pageNo, pageSize);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
setBookingSearchResult(data.bookings.map((item: BookingSearchResult) => {
return {
key: item.id,
...item
}
}))
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}
const [form ] = useForm();
useEffect(() => {
searchBooking({
username: form.getFieldValue('username'),
meetingRoomName: form.getFieldValue('meetingRoomName'),
meetingRoomPosition: form.getFieldValue('meetingRoomPosition'),
rangeStartDate: form.getFieldValue('rangeStartDate'),
rangeStartTime: form.getFieldValue('rangeStartTime'),
rangeEndDate: form.getFieldValue('rangeEndDate'),
rangeEndTime: form.getFieldValue('rangeEndTime')
});
}, [pageNo, pageSize, num]);
const changePage = function(pageNo: number, pageSize: number) {
setPageNo(pageNo);
setPageSize(pageSize);
}
return <div id="bookingManage-container">
<div className="bookingManage-form">
<Form
form={form}
onFinish={searchBooking}
name="search"
layout='inline'
colon={false}
>
<Form.Item label="预订人" name="username">
<Input />
</Form.Item>
<Form.Item label="会议室名称" name="meetingRoomName">
<Input />
</Form.Item>
<Form.Item label="预订开始日期" name="rangeStartDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订开始时间" name="rangeStartTime">
<TimePicker/>
</Form.Item>
<Form.Item label="预订结束日期" name="rangeEndDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订结束时间" name="rangeEndTime">
<TimePicker/>
</Form.Item>
<Form.Item label="位置" name="meetingRoomPosition">
<Input />
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索预订申请
</Button>
</Form.Item>
</Form>
</div>
<div className="bookingManage-table">
<Table columns={columns} dataSource={bookingSearchResult} pagination={ {
current: pageNo,
pageSize: pageSize,
onChange: changePage
}}/>
</div>
</div>
}没带参数的搜索没问题:
数据库里就这 4 条记录:
然后带上参数搜索下:
开始时间在 2023-9-29 的 10 点到 11 点的预订有 3 条:
11 点到 12 点的有 1 条:
这样,列表功能就完成了。
可以再加上个按照状态过滤,这个是 antd 的功能:
{
title: '审批状态',
dataIndex: 'status',
onFilter: (value, record) => record.status.startsWith(value as string),
filters: [
{
text: '审批通过',
value: '审批通过',
},
{
text: '审批驳回',
value: '审批驳回',
},
{
text: '申请中',
value: '申请中',
},
{
text: '已解除',
value: '已解除'
},
],
},然后加上右边的按钮:
{
title: '操作',
render: (_, record) => (
<div>
<Popconfirm
title="通过申请"
description="确认通过吗?"
onConfirm={() => changeStatus(record.id, 'apply')}
okText="Yes"
cancelText="No"
>
<a href="#">通过</a>
</Popconfirm>
<br/>
<Popconfirm
title="驳回申请"
description="确认驳回吗?"
onConfirm={() => changeStatus(record.id, 'reject')}
okText="Yes"
cancelText="No"
>
<a href="#">驳回</a>
</Popconfirm>
<br/>
<Popconfirm
title="解除申请"
description="确认解除吗?"
onConfirm={() => changeStatus(record.id, 'unbind')}
okText="Yes"
cancelText="No"
>
<a href="#">解除</a>
</Popconfirm>
<br/>
</div>
)
}async function changeStatus(id: number, status: 'apply' | 'reject' | 'unbind') {
const methods = {
apply,
reject,
unbind
}
const res = await methods[status](id);
if(res.status === 201 || res.status === 200) {
message.success('状态更新成功');
setNum(Math.random());
} else {
message.error(res.data.data);
}
}更新完状态之后要触发列表的重新渲染,所以这里用 setNum 触发:
这样,预订管理的功能就完成了。
案例代码上传了
这节实现了预订管理的管理端的前端页面,主要是列表和修改状态的接口。
要注意的是时间相关的处理,antd 只有 DatePicker 和 TimePicker,要添加 2 个字段接收,然后调用接口的时候把它们合并成一个字段。
下节来写用户端的部分。
这节来写下预订管理模块的用户端。
把用户端代码跑起来,首先写下预订历史页面:
这个页面就是一个列表:
先写上面的 form:
import { Button, DatePicker, Form, Input, TimePicker } from "antd";
import { useEffect } from "react";
import { useForm } from "antd/es/form/Form";
import './booking_history.css';
export interface SearchBooking {
username: string;
meetingRoomName: string;
meetingRoomPosition: string;
rangeStartDate: Date;
rangeStartTime: Date;
rangeEndDate: Date;
rangeEndTime: Date;
}
export function BookingHistory() {
const searchBooking = async (values: SearchBooking) => {
}
const [form ] = useForm();
useEffect(() => {
searchBooking({
username: '',
meetingRoomName: form.getFieldValue('meetingRoomName'),
meetingRoomPosition: form.getFieldValue('meetingRoomPosition'),
rangeStartDate: form.getFieldValue('rangeStartDate'),
rangeStartTime: form.getFieldValue('rangeStartTime'),
rangeEndDate: form.getFieldValue('rangeEndDate'),
rangeEndTime: form.getFieldValue('rangeEndTime')
});
}, []);
return <div id="bookingHistory-container">
<div className="bookingHistory-form">
<Form
form={form}
onFinish={searchBooking}
name="search"
layout='inline'
colon={false}
>
<Form.Item label="会议室名称" name="meetingRoomName">
<Input />
</Form.Item>
<Form.Item label="预订开始日期" name="rangeStartDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订开始时间" name="rangeStartTime">
<TimePicker/>
</Form.Item>
<Form.Item label="预订结束日期" name="rangeEndDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订结束时间" name="rangeEndTime">
<TimePicker/>
</Form.Item>
<Form.Item label="位置" name="meetingRoomPosition">
<Input />
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索预订历史
</Button>
</Form.Item>
</Form>
</div>
</div>
}#bookingHistory-container {
padding: 20px;
}
#bookingHistory-container .bookingHistory-form {
margin-bottom: 40px;
}
#bookingHistory-container .ant-form-item {
margin: 10px;
}
和后台管理的页面差不多,只不过这里没有 user 的搜索。
登录的时候把它放到了 localStorage 里,所以这里从 localStorage 取就行:
function getUserInfo() {
const userInfoStr = localStorage.getItem('user_info');
if(userInfoStr) {
return JSON.parse(userInfoStr);
}
}然后在 interfaces.ts 写下用到的接口:
export async function bookingList(searchBooking: SearchBooking, pageNo: number, pageSize: number) {
let bookingTimeRangeStart;
let bookingTimeRangeEnd;
if(searchBooking.rangeStartDate && searchBooking.rangeStartTime) {
const rangeStartDateStr = dayjs(searchBooking.rangeStartDate).format('YYYY-MM-DD');
const rangeStartTimeStr = dayjs(searchBooking.rangeStartTime).format('HH:mm');
bookingTimeRangeStart = dayjs(rangeStartDateStr + ' ' + rangeStartTimeStr).valueOf()
}
if(searchBooking.rangeEndDate && searchBooking.rangeEndTime) {
const rangeEndDateStr = dayjs(searchBooking.rangeEndDate).format('YYYY-MM-DD');
const rangeEndTimeStr = dayjs(searchBooking.rangeEndTime).format('HH:mm');
bookingTimeRangeEnd = dayjs(rangeEndDateStr + ' ' + rangeEndTimeStr).valueOf()
}
return await axiosInstance.get('/booking/list', {
params: {
username: searchBooking.username,
meetingRoomName: searchBooking.meetingRoomName,
meetingRoomPosition: searchBooking.meetingRoomPosition,
bookingTimeRangeStart,
bookingTimeRangeEnd,
pageNo: pageNo,
pageSize: pageSize
}
});
}这个就是当时后台管理的接口,没啥区别。
然后在页面调用下:
interface BookingSearchResult {
id: number;
startTime: string;
endTime: string;
status: string;
note: string;
createTime: string;
updateTime: string;
room: MeetingRoomSearchResult
}const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [bookingSearchResult, setBookingSearchResult] = useState<Array<BookingSearchResult>>([]);
const searchBooking = async (values: SearchBooking) => {
const res = await bookingList({
...values,
username: getUserInfo().username
}, pageNo, pageSize);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
setBookingSearchResult(data.bookings.map((item: BookingSearchResult) => {
return {
key: item.id,
...item
}
}))
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}接口调用没啥问题:
然后加上下面的表格:
这些和后台管理一样,直接复制过来就行:
import { Button, DatePicker, Form, Input, Table, TimePicker, message } from "antd";
import { useEffect, useState } from "react";
import { useForm } from "antd/es/form/Form";
import './booking_history.css';
import { bookingList } from "../../interfaces/interfaces";
import { MeetingRoomSearchResult } from "../meeting_room_list/MeetingRoomList";
import { ColumnsType } from "antd/es/table";
import dayjs from 'dayjs';
export interface SearchBooking {
username: string;
meetingRoomName: string;
meetingRoomPosition: string;
rangeStartDate: Date;
rangeStartTime: Date;
rangeEndDate: Date;
rangeEndTime: Date;
}
interface BookingSearchResult {
id: number;
startTime: string;
endTime: string;
status: string;
note: string;
createTime: string;
updateTime: string;
room: MeetingRoomSearchResult
}
function getUserInfo() {
const userInfoStr = localStorage.getItem('user_info');
if(userInfoStr) {
return JSON.parse(userInfoStr);
}
}
export function BookingHistory() {
const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [bookingSearchResult, setBookingSearchResult] = useState<Array<BookingSearchResult>>([]);
const searchBooking = async (values: SearchBooking) => {
const res = await bookingList(values, pageNo, pageSize);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
setBookingSearchResult(data.bookings.map((item: BookingSearchResult) => {
return {
key: item.id,
...item
}
}))
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}
const [form ] = useForm();
const changePage = function(pageNo: number, pageSize: number) {
setPageNo(pageNo);
setPageSize(pageSize);
}
useEffect(() => {
searchBooking({
username: getUserInfo().username,
meetingRoomName: form.getFieldValue('meetingRoomName'),
meetingRoomPosition: form.getFieldValue('meetingRoomPosition'),
rangeStartDate: form.getFieldValue('rangeStartDate'),
rangeStartTime: form.getFieldValue('rangeStartTime'),
rangeEndDate: form.getFieldValue('rangeEndDate'),
rangeEndTime: form.getFieldValue('rangeEndTime')
});
}, [pageNo, pageSize]);
const columns: ColumnsType<BookingSearchResult> = [
{
title: '会议室名称',
dataIndex: 'room',
render(_, record) {
return record.room.name
}
},
{
title: '开始时间',
dataIndex: 'startTime',
render(_, record) {
return dayjs(new Date(record.startTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '结束时间',
dataIndex: 'endTime',
render(_, record) {
return dayjs(new Date(record.endTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '审批状态',
dataIndex: 'status',
onFilter: (value, record) => record.status.startsWith(value as string),
filters: [
{
text: '审批通过',
value: '审批通过',
},
{
text: '审批驳回',
value: '审批驳回',
},
{
text: '申请中',
value: '申请中',
},
{
text: '已解除',
value: '已解除'
},
],
},
{
title: '预订时间',
dataIndex: 'createTime',
render(_, record) {
return dayjs(new Date(record.createTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '备注',
dataIndex: 'note'
},
{
title: '描述',
dataIndex: 'description'
},
{
title: '操作',
render: () => (
<div>
</div>
)
}
];
return <div id="bookingHistory-container">
<div className="bookingHistory-form">
<Form
form={form}
onFinish={searchBooking}
name="search"
layout='inline'
colon={false}
>
<Form.Item label="会议室名称" name="meetingRoomName">
<Input />
</Form.Item>
<Form.Item label="预订开始日期" name="rangeStartDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订开始时间" name="rangeStartTime">
<TimePicker/>
</Form.Item>
<Form.Item label="预订结束日期" name="rangeEndDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订结束时间" name="rangeEndTime">
<TimePicker/>
</Form.Item>
<Form.Item label="位置" name="meetingRoomPosition">
<Input />
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索预订历史
</Button>
</Form.Item>
</Form>
</div>
<div className="bookingHistory-table">
<Table columns={columns} dataSource={bookingSearchResult} pagination={ {
current: pageNo,
pageSize: pageSize,
onChange: changePage
}}/>
</div>
</div>
}这样,列表就完成了:
然后实现解除预订功能:
在 interfaces.ts 添加 unbind 接口:
export async function unbind(id: number) {
return await axiosInstance.get('/booking/unbind/' + id);
}然后在页面调用下:
import { Button, DatePicker, Form, Input, Popconfirm, Table, TimePicker, message } from "antd";
import { useEffect, useState } from "react";
import { useForm } from "antd/es/form/Form";
import './booking_history.css';
import { bookingList, unbind } from "../../interfaces/interfaces";
import { MeetingRoomSearchResult } from "../meeting_room_list/MeetingRoomList";
import { ColumnsType } from "antd/es/table";
import dayjs from 'dayjs';
export interface SearchBooking {
username: string;
meetingRoomName: string;
meetingRoomPosition: string;
rangeStartDate: Date;
rangeStartTime: Date;
rangeEndDate: Date;
rangeEndTime: Date;
}
interface BookingSearchResult {
id: number;
startTime: string;
endTime: string;
status: string;
note: string;
createTime: string;
updateTime: string;
room: MeetingRoomSearchResult
}
function getUserInfo() {
const userInfoStr = localStorage.getItem('user_info');
if(userInfoStr) {
return JSON.parse(userInfoStr);
}
}
export function BookingHistory() {
const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [bookingSearchResult, setBookingSearchResult] = useState<Array<BookingSearchResult>>([]);
const [num, setNum] = useState(0);
const searchBooking = async (values: SearchBooking) => {
const res = await bookingList(values, pageNo, pageSize);
const { data } = res.data;
if(res.status === 201 || res.status === 200) {
setBookingSearchResult(data.bookings.map((item: BookingSearchResult) => {
return {
key: item.id,
...item
}
}))
} else {
message.error(data || '系统繁忙,请稍后再试');
}
}
const [form ] = useForm();
const changePage = function(pageNo: number, pageSize: number) {
setPageNo(pageNo);
setPageSize(pageSize);
}
useEffect(() => {
searchBooking({
username: getUserInfo().username,
meetingRoomName: form.getFieldValue('meetingRoomName'),
meetingRoomPosition: form.getFieldValue('meetingRoomPosition'),
rangeStartDate: form.getFieldValue('rangeStartDate'),
rangeStartTime: form.getFieldValue('rangeStartTime'),
rangeEndDate: form.getFieldValue('rangeEndDate'),
rangeEndTime: form.getFieldValue('rangeEndTime')
});
}, [pageNo, pageSize, num]);
async function changeStatus(id: number) {
const res = await unbind(id);
if(res.status === 201 || res.status === 200) {
message.success('状态更新成功');
setNum(Math.random());
} else {
message.error(res.data.data);
}
}
const columns: ColumnsType<BookingSearchResult> = [
{
title: '会议室名称',
dataIndex: 'room',
render(_, record) {
return record.room.name
}
},
{
title: '开始时间',
dataIndex: 'startTime',
render(_, record) {
return dayjs(new Date(record.startTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '结束时间',
dataIndex: 'endTime',
render(_, record) {
return dayjs(new Date(record.endTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '审批状态',
dataIndex: 'status',
onFilter: (value, record) => record.status.startsWith(value as string),
filters: [
{
text: '审批通过',
value: '审批通过',
},
{
text: '审批驳回',
value: '审批驳回',
},
{
text: '申请中',
value: '申请中',
},
{
text: '已解除',
value: '已解除'
},
],
},
{
title: '预订时间',
dataIndex: 'createTime',
render(_, record) {
return dayjs(new Date(record.createTime)).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: '备注',
dataIndex: 'note'
},
{
title: '描述',
dataIndex: 'description'
},
{
title: '操作',
render: (_, record) => (
record.status === '申请中' ? <div>
<Popconfirm
title="解除申请"
description="确认解除吗?"
onConfirm={() => changeStatus(record.id)}
okText="Yes"
cancelText="No"
>
<a href="#">解除预订</a>
</Popconfirm>
</div> : null
)
}
];
return <div id="bookingHistory-container">
<div className="bookingHistory-form">
<Form
form={form}
onFinish={searchBooking}
name="search"
layout='inline'
colon={false}
>
<Form.Item label="会议室名称" name="meetingRoomName">
<Input />
</Form.Item>
<Form.Item label="预订开始日期" name="rangeStartDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订开始时间" name="rangeStartTime">
<TimePicker/>
</Form.Item>
<Form.Item label="预订结束日期" name="rangeEndDate">
<DatePicker/>
</Form.Item>
<Form.Item label="预订结束时间" name="rangeEndTime">
<TimePicker/>
</Form.Item>
<Form.Item label="位置" name="meetingRoomPosition">
<Input />
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索预订历史
</Button>
</Form.Item>
</Form>
</div>
<div className="bookingHistory-table">
<Table columns={columns} dataSource={bookingSearchResult} pagination={ {
current: pageNo,
pageSize: pageSize,
onChange: changePage
}}/>
</div>
</div>
}测试下:
没啥问题。
如果没有合适的数据,就手动去数据库里改一下:
接下来,还有一个添加预订的功能:
当点击会议室列表的预订按钮的时候,会弹出这个窗口。
添加 src/meeting_room_list/CreateBookingModal.tsx
import { DatePicker, Form, Input, InputNumber, Modal, Select, TimePicker, message } from "antd";
import { useForm } from "antd/es/form/Form";
import { bookingAdd } from "../../interfaces/interfaces";
import { MeetingRoomSearchResult } from "./MeetingRoomList";
interface CreateBookingModalProps {
isOpen: boolean;
handleClose: Function;
meetingRoom: MeetingRoomSearchResult;
}
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 18 }
}
export interface CreateBooking {
meetingRoomId: number;
rangeStartDate: Date;
rangeStartTime: Date;
rangeEndDate: Date;
rangeEndTime: Date;
note: string;
}
export function CreateBookingModal(props: CreateBookingModalProps) {
const [form] = useForm<CreateBooking>();
const handleOk = async function() {
}
return <Modal title="预订会议室" open={props.isOpen} onOk={handleOk} onCancel={() => props.handleClose()} okText={'创建'}>
<Form
form={form}
colon={false}
{...layout}
>
<Form.Item
label="会议室名称"
name="meetingRoomId"
>
{props.meetingRoom.name}
</Form.Item>
<Form.Item
label="预订开始日期"
name="rangeStartDate"
rules={[
{ required: true, message: '请输入预订开始日期!' },
]}
>
<DatePicker/>
</Form.Item>
<Form.Item
label="预订开始时间"
name="rangeStartTime"
rules={[
{ required: true, message: '请输入预订开始时间!' },
]}
>
<TimePicker/>
</Form.Item>
<Form.Item
label="预订结束日期"
name="rangeEndDate"
rules={[
{ required: true, message: '请输入预订结束日期!' },
]}
>
<DatePicker/>
</Form.Item>
<Form.Item
label="预订结束时间"
name="rangeEndTime"
rules={[
{ required: true, message: '请输入预订结束时间!' },
]}
>
<TimePicker/>
</Form.Item>
<Form.Item
label="备注"
name="note"
>
<Input />
</Form.Item>
</Form>
</Modal>
}然后点击预订按钮的时候,显示这个弹窗:
添加 isCreateModalOpen 的 state 来标识弹窗是否打开,并且记录点击的是哪个会议室:
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [currentMeetingRoom, setCurrentMeetingRoom] = useState<MeetingRoomSearchResult>();点击预订按钮的时候,打开弹窗,记录当前会议室:
{
title: '操作',
render: (_, record) => (
<div>
<a href="#" onClick={() => {
setIsCreateModalOpen(true);
setCurrentMeetingRoom(record);
}}>预订</a>
</div>
)
}然后弹窗传入当前的会议室,并且点击关闭的时候关闭弹窗:
{
currentMeetingRoom ?
<CreateBookingModal meetingRoom={currentMeetingRoom} isOpen={isCreateModalOpen} handleClose={() => {
setIsCreateModalOpen(false);
}}></CreateBookingModal>
: null
}没啥问题:
然后在 interfaces.ts 添加用到的接口:
export async function bookingAdd(booking: CreateBooking) {
const rangeStartDateStr = dayjs(booking.rangeStartDate).format('YYYY-MM-DD');
const rangeStartTimeStr = dayjs(booking.rangeStartTime).format('HH:mm');
const startTime = dayjs(rangeStartDateStr + ' ' + rangeStartTimeStr).valueOf()
const rangeEndDateStr = dayjs(booking.rangeEndDate).format('YYYY-MM-DD');
const rangeEndTimeStr = dayjs(booking.rangeEndTime).format('HH:mm');
const endTime = dayjs(rangeEndDateStr + ' ' + rangeEndTimeStr).valueOf()
return await axiosInstance.post('/booking/add', {
meetingRoomId: booking.meetingRoomId,
startTime,
endTime,
note: booking.note
});
}这里需要把日期时间做合并。
然后在组件里调用下:
const handleOk = async function() {
const values = form.getFieldsValue();
values.meetingRoomId = props.meetingRoom.id;
const res = await bookingAdd(values);
if(res.status === 201 || res.status === 200) {
message.success('预订成功');
form.resetFields();
props.handleClose();
} else {
message.error(res.data.data);
}
}没啥问题:
这样,预订、预订历史、取消预订就都完成了。
案例代码上传了
这节完成了预订历史、添加预订、取消预订的功能。
就是涉及到时间日期需要两个表单做合并处理,其余的倒是没啥难度。
至此,预订管理模块就完成了。