这节写下预定管理模块管理端的前端部分:

对应的路由我们前面写过了:

这节来填充下内容。
首先是 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 个字段接收,然后调用接口的时候把它们合并成一个字段。
下节我们来写用户端的部分。
