后端接口写完了,这节我们来实现下前端页面。
先写管理端的:
把管理端项目跑起来:
npm run start
我们已经添加了对应的路由,但是还没做点击菜单时的切换。
加一下:
const handleMenuItemClick: MenuClickEventHandler = (info) => {
let path = '';
switch(info.key) {
case '1':
path = '/meeting_room_manage';
break;
case '2':
path = '/booking_manage';
break;
case '3':
path = '/user_manage';
break;
case '4':
path = '/statistics';
break;
}
router.navigate(path);
}
然后写下这 3 个路由的组件:
src/pages/MeetingRoomManage/MeetingRoomManage.tsx
export function MeetingRoomManage() {
return <div>MeetingRoomManage</div>
}
src/pages/BookingManage/BookingManage.tsx
export function BookingManage() {
return <div>BookingManage</div>
}
src/pages/Statistics/Statistics.tsx
export function Statistics() {
return <div>Statistics</div>
}
注册这三个组件对应的路由:
{
path: '/',
element: <MeetingRoomManage/>
},
{
path: 'user_manage',
element: <UserManage/>
},
{
path: 'meeting_room_manage',
element: <MeetingRoomManage/>
},
{
path: 'booking_manage',
element: <BookingManage/>
},
{
path: 'statistics',
element: <Statistics/>
}
测试下:
然后还要加上页面刷新时选中对应菜单项的逻辑:
const location = useLocation();
function getSelectedKeys() {
if(location.pathname === '/user_manage') {
return ['3']
} else if(location.pathname === '/booking_manage') {
return ['2']
} else if(location.pathname === '/meeting_room_manage') {
return ['1']
} else if(location.pathname === '/statistics') {
return ['4']
} else {
return ['1']
}
}
这样,刷新后也会选中对应的菜单项:
然后来实现会议室管理页面:
和我们前面写过的用户列表差不多:
import { Badge, Button, Form, Image, Input, Table, message } from "antd";
import { useCallback, useEffect, useMemo, useState } from "react";
import './meeting_room_manage.css';
import { ColumnsType } from "antd/es/table";
import { useForm } from "antd/es/form/Form";
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 MeetingRoomManage() {
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) => (
<a href="#" onClick={() => {}}>删除</a>
)
}
], []);
const searchMeetingRoom = useCallback(async (values: SearchMeetingRoom) => {
}, []);
const [form ] = useForm();
const changePage = useCallback(function(pageNo: number, pageSize: number) {
setPageNo(pageNo);
setPageSize(pageSize);
}, []);
return <div id="meetingRoomManage-container">
<div className="meetingRoomManage-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="location">
<Input/>
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
搜索会议室
</Button>
<Button type="primary" style={{background: 'green'}}>
添加会议室
</Button>
</Form.Item>
</Form>
</div>
<div className="meetingRoomManage-table">
<Table columns={columns} dataSource={meetingRoomResult} pagination={ {
current: pageNo,
pageSize: pageSize,
onChange: changePage
}}/>
</div>
</div>
}
css 部分如下:
#meetingRoomManage-container {
padding: 20px;
}
#meetingRoomManage-container .meetingRoomManage-form {
margin-bottom: 40px;
}
然后我们在 interfaces.ts 添加 list 接口:
export async function meetingRoomList(name: string, capacity: number, equipment: string, pageNo: number, pageSize: number) {
return await axiosInstance.get('/meeting-room/list', {
params: {
name,
capacity,
equipment,
pageNo,
pageSize
}
});
}
在页面调用下:
const searchMeetingRoom = useCallback(async (values: SearchMeetingRoom) => {
const res = await meetingRoomList(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 || '系统繁忙,请稍后再试');
}
}, []);
按名称搜索: 按容量搜索: 按设备搜索:
然后,最开始进入页面的时候也得搜索一次:
useEffect(() => {
searchMeetingRoom({
name: form.getFieldValue('name'),
capacity: form.getFieldValue('capacity'),
equipment: form.getFieldValue('equipment')
});
}, [pageNo, pageSize]);
最开始搜索一次,并且分页信息变了也重新搜索。
这样,刚进入页面就会触发一次搜索。
然后我们处理删除:
在 interfaces.ts 里添加 delete 接口:
export async function deleteMeetingRoom(id: number) {
return await axiosInstance.delete('/meeting-room/' + id);
}
然后添加删除按钮的处理逻辑:
<a href="#" onClick={() => handleDelete(record.id)}>删除</a>
const handleDelete = useCallback(async (id: number) => {
try {
await deleteMeetingRoom(id);
message.success('删除成功');
} catch(e) {
console.log(e);
message.error('删除失败');
}
}, []);
提示删除成功,刷新后也确实没有了。
不过应该是删除后自动刷新的。
我们添加一个状态,删除后设置一个随机数,然后把它作为 useEffect 的依赖,这样就能触发重新搜索。
const [num, setNum] = useState<number>();
setNum(Math.random());
不过,删除操作最好加上个二次确认。
这个把按钮抱一下就好了:
{
title: '操作',
render: (_, record) => (
<Popconfirm
title="会议室删除"
description="确认删除吗?"
onConfirm={() => handleDelete(record.id)}
okText="Yes"
cancelText="No"
>
<a href="#">删除</a>
</Popconfirm>
)
}
这样,点击后就会出现一个确认框,确认后才会删除:
然后实现添加会议室:
我们在 MeetingRoomManage 的同级添加一个 CreateMeetingRoomModal 组件:
import { Modal } from "antd";
import { useCallback } from "react";
interface CreateMeetingRoomModalProps {
isOpen: boolean;
handleClose: Function
}
export function CreateMeetingRoomModal(props: CreateMeetingRoomModalProps) {
const handleOk = useCallback(async function() {
props.handleClose();
}, []);
return <Modal title="创建会议室" open={props.isOpen} onOk={handleOk} onCancel={() => props.handleClose()}>
<p>xxxx</p>
</Modal>
}
组件里有一个 Modal,通过参数 isOpen 控制是否显示。
当点击取消的时候,或者确认的时候,都会调用 props.handleClose 方法。
然后在 MeetingRoomManage 引入它:
先添加一个状态代表 modal 是否打开:
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
然后添加 Modal 组件,并且点击按钮的时候 open:
<Button type="primary" style={{background: 'green'}} onClick={
() => setIsCreateModalOpen(true)
}>
添加会议室
</Button>
<CreateMeetingRoomModal isOpen={isCreateModalOpen} handleClose={() => {
setIsCreateModalOpen(false);
}}></CreateMeetingRoomModal>
这样,modal 就添加成功了:
然后实现 modal 的具体逻辑,创建会议室:
import { Button, Form, Input,InputNumber, Modal } from "antd";
import { useForm } from "antd/es/form/Form";
import TextArea from "antd/es/input/TextArea";
import { useCallback } from "react";
interface CreateMeetingRoomModalProps {
isOpen: boolean;
handleClose: Function
}
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 18 }
}
export interface CreateMeetingRoom {
name: string;
capacity: number;
location: string;
equipment: string;
description: string;
}
export function CreateMeetingRoomModal(props: CreateMeetingRoomModalProps) {
const [form] = useForm();
const handleOk = useCallback(async function() {
const values = form.getFieldsValue();
console.log(values);
props.handleClose();
}, []);
return <Modal title="创建会议室" open={props.isOpen} onOk={handleOk} onCancel={() => props.handleClose()} okText={'创建'}>
<Form
form={form}
colon={false}
{...layout}
>
<Form.Item
label="会议室名称"
name="name"
rules={[
{ required: true, message: '请输入会议室名称!' },
]}
>
<Input />
</Form.Item>
<Form.Item
label="位置"
name="location"
rules={[
{ required: true, message: '请输入会议室位置!' },
]}
>
<Input />
</Form.Item>
<Form.Item
label="容纳人数"
name="capacity"
rules={[
{ required: true, message: '请输入会议室容量!' },
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label="设备"
name="equipment"
>
<Input />
</Form.Item>
<Form.Item
label="描述"
name="description"
>
<TextArea />
</Form.Item>
</Form>
</Modal>
}
在 modal 里添加一个表单,点击创建按钮的时候打印表单值。
我们在 interfaces.ts 添加创建会议室的接口:
export async function createMeetingRoom(meetingRoom: CreateMeetingRoom) {
return await axiosInstance.post('/meeting-room/create', meetingRoom);
}
在组件里调用下:
const [form] = useForm<CreateMeetingRoom>();
const handleOk = useCallback(async function() {
const values = form.getFieldsValue();
values.description = values.description || '';
values.equipment = values.equipment || '';
const res = await createMeetingRoom(values);
if(res.status === 201 || res.status === 200) {
message.success('创建成功');
form.resetFields();
props.handleClose();
} else {
message.error(res.data.data);
}
}, []);
如果没有填 description 或者 equipment 就设置个空字符串。
测试下:
创建失败时:
创建成功时:
创建成功后,手动刷新页面,就看到了新的会议室。
然后我们在关掉弹窗的时候设置下 num。
这样就会触发列表数据的刷新:
测试下:
最后,加上更新会议室的功能:
创建 UpdateMeetingRoom.tsx
内容和 create 的基本一样:
import { Button, Form, Input, InputNumber, Modal, message } from "antd";
import { useForm } from "antd/es/form/Form";
import TextArea from "antd/es/input/TextArea";
import { useCallback } from "react";
import { updateMeetingRoom } from "../../interfaces/interfaces";
interface UpdateMeetingRoomModalProps {
isOpen: boolean;
handleClose: Function
}
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 18 }
}
export interface UpdateMeetingRoom {
name: string;
capacity: number;
location: string;
equipment: string;
description: string;
}
export function UpdateMeetingRoomModal(props: UpdateMeetingRoomModalProps) {
const [form] = useForm<UpdateMeetingRoom>();
const handleOk = useCallback(async function() {
props.handleClose();
}, []);
return <Modal title="更新会议室" open={props.isOpen} onOk={handleOk} onCancel={() => props.handleClose()} okText={'更新'}>
<Form
form={form}
colon={false}
{...layout}
>
<Form.Item
label="会议室名称"
name="name"
rules={[
{ required: true, message: '请输入会议室名称!' },
]}
>
<Input />
</Form.Item>
<Form.Item
label="位置"
name="location"
rules={[
{ required: true, message: '请输入会议室位置!' },
]}
>
<Input />
</Form.Item>
<Form.Item
label="容纳人数"
name="capacity"
rules={[
{ required: true, message: '请输入会议室容量!' },
]}
>
<InputNumber />
</Form.Item>
<Form.Item
label="设备"
name="equipment"
>
<Input />
</Form.Item>
<Form.Item
label="描述"
name="description"
>
<TextArea/>
</Form.Item>
</Form>
</Modal>
}
在 interfaces.ts里创建会用到的接口:
export async function updateMeetingRoom(meetingRoom: UpdateMeetingRoom) {
return await axiosInstance.put('/meeting-room/update', meetingRoom);
}
export async function findMeetingRoom(id: number) {
return await axiosInstance.get('/meeting-room/' + id);
}
然后在 MeetingRoomManage 组件引入:
先创建两个 state:
const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false);
const [updateId, setUpdateId] = useState<number>();
一个是 update 弹窗是否打开,一个是当前的 id。
然后添加一个更新按钮,点击的时候打开弹出弹窗,设置 id:
{
title: '操作',
render: (_, record) => (
<div>
<Popconfirm
title="会议室删除"
description="确认删除吗?"
onConfirm={() => handleDelete(record.id)}
okText="Yes"
cancelText="No"
>
<a href="#">删除</a>
</Popconfirm>
<br/>
<a href="#" onClick={() => {
setIsUpdateModalOpen(true);
setUpdateId(record.id);
}}>更新</a>
</div>
)
}
在下面加上弹窗:
<UpdateMeetingRoomModal isOpen={isUpdateModalOpen} handleClose={() => {
setIsUpdateModalOpen(false);
setNum(Math.random());
}}></UpdateMeetingRoomModal>
这样更新弹窗就加上了:
然后我们要把 id 传过去:
updateId 的默认值是 undefined,可能为空,加上 ! 代表非空。
然后在组件里添加这个参数:
并且调用查询接口,查询 id 对应的数据来回显:
useEffect(() => {
async function query() {
if(!props.id) {
return;
}
const res = await findMeetingRoom(props.id);
const { data } = res;
if(res.status === 200 || res.status === 201) {
form.setFieldValue('id', data.data.id);
form.setFieldValue('name', data.data.name);
form.setFieldValue('location', data.data.location);
form.setFieldValue('capacity', data.data.capacity);
form.setFieldValue('equipment', data.data.equipment);
form.setFieldValue('description', data.data.description);
} else {
message.error(res.data.data);
}
}
query();
}, [props.id]);
现在就能回显数据了:
然后再加上更新数据的接口:
const handleOk = useCallback(async function() {
const values = form.getFieldsValue();
values.description = values.description || '';
values.equipment = values.equipment || '';
const res = await updateMeetingRoom({
...values,
id: form.getFieldValue('id')
});
if(res.status === 201 || res.status === 200) {
message.success('更新成功');
props.handleClose();
} else {
message.error(res.data.data);
}
}, []);
这里要的参数要额外带上 id。
测试下:
更新成功了。
这样,会议室管理的页面就完成了。
案例代码上传了小册仓库。
总结
这节我们实现了会议室管理的前端页面。
实现了列表、分页和搜索,添加会议室、更新会议室、删除会议室。
其中添加和更新会议室需要创建 Modal,我们把它拆分成了单独的组件。
更新会议室的时候,传入 id,根据 id 回显数据,然后修改完以后再更新数据。
至此,会议室管理的后端和前端代码就都完成了。