Skip to content

这节我们来写下预订管理模块的用户端。

把用户端代码跑起来,首先写下预订历史页面:

这个页面就是一个列表:

我们先写上面的 form:

javascript
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>
}
css
#bookingHistory-container {
    padding: 20px;
}
#bookingHistory-container .bookingHistory-form {
    margin-bottom: 40px;
}
#bookingHistory-container .ant-form-item {
    margin: 10px;
}

和后台管理的页面差不多,只不过这里没有 user 的搜索。

我们登录的时候把它放到了 localStorage 里,所以这里从 localStorage 取就行:

javascript
function getUserInfo() {
    const userInfoStr = localStorage.getItem('user_info');

    if(userInfoStr) {
        return JSON.parse(userInfoStr);
    }
}

然后在 interface.ts 写下用到的接口:

javascript
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
        }
    });
}

这个就是当时后台管理的接口,没啥区别。

然后在页面调用下:

javascript
interface BookingSearchResult {
    id: number;
    startTime: string;
    endTime: string;
    status: string;
    note: string;
    createTime: string;
    updateTime: string;
    room: MeetingRoomSearchResult
}
javascript
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 || '系统繁忙,请稍后再试');
    }
}

接口调用没啥问题:

然后加上下面的表格:

这些和后台管理一样,直接复制过来就行:

javascript
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 "../../interface/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>
}

这样,列表就完成了:

然后实现解除预定功能:

在 interface.ts 添加 unbind 接口:

javascript
export async function unbind(id: number) {
    return await axiosInstance.get('/booking/unbind/' + id);
}

然后在页面调用下:

javascript
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 "../../interface/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

javascript
import { DatePicker, Form, Input, InputNumber, Modal, Select, TimePicker, message } from "antd";
import { useForm } from "antd/es/form/Form";
import { bookingAdd } from "../../interface/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 来标识弹窗是否打开,并且记录点击的是哪个会议室:

javascript
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [currentMeetingRoom, setCurrentMeetingRoom] =  useState<MeetingRoomSearchResult>();

点击预定按钮的时候,打开弹窗,记录当前会议室:

javascript
{
    title: '操作',
    render: (_, record) => (
        <div>
            <a href="#" onClick={() => {
                setIsCreateModalOpen(true);
                setCurrentMeetingRoom(record);
            }}>预定</a>
        </div>
    )
}

然后弹窗传入当前的会议室,并且点击关闭的时候关闭弹窗:

javascript
{
    currentMeetingRoom ? 
        <CreateBookingModal meetingRoom={currentMeetingRoom} isOpen={isCreateModalOpen} handleClose={() => {
            setIsCreateModalOpen(false);
        }}></CreateBookingModal>
    : null
}

没啥问题:

然后在 interface.ts 添加用到的接口:

javascript
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            
    });
}

这里需要把日期时间做合并。

然后在组件里调用下:

javascript
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);
    }
}

没啥问题:

这样,预定、预订历史、取消预订就都完成了。

案例代码上传了小册仓库

总结

这节我们完成了预订历史、添加预定、取消预订的功能。

就是涉及到时间日期需要两个表单做合并处理,其余的倒是没啥难度。

至此,预订管理模块就完成了。