这节我们来写图书新增、修改、删除、详情功能:



我们创建一个新的组件:
BookManage/CreateBookModal.tsx
import { Button, Form, Input, Modal, message, TextArea } from "antd";
import { useForm } from "antd/es/form/Form";
import TextArea from "antd/es/input/TextArea";
interface CreateBookModalProps {
isOpen: boolean;
handleClose: Function;
}
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 18 },
};
export interface CreateBook {
name: string;
author: string;
description: string;
cover: string;
}
export function CreateBookModal(props: CreateBookModalProps) {
const [form] = useForm<CreateBook>();
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="name"
rules={[{ required: true, message: "请输入图书名称!" }]}>
<Input />
</Form.Item>
<Form.Item
label="作者"
name="author"
rules={[{ required: true, message: "请输入图书作者!" }]}>
<Input />
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: true, message: "请输入图书描述!" }]}>
<TextArea />
</Form.Item>
<Form.Item
label="封面"
name="cover"
rules={[{ required: true, message: "请上传图书封面!" }]}>
<Input />
</Form.Item>
</Form>
</Modal>
);
}
创建一个 Modal,包含一个 Form,有 4 个字段:name、author、description、cover。
传入 isOpen 来控制 Modal 开启,handleClose 处理关闭的回调。
在 BookManage/index.tsx 调用下:

const [isCreateBookModalOpen, setCraeteBookModalOpen] = useState(false);
<CreateBookModal
isOpen={isCreateBookModalOpen}
handleClose={() => {
setCraeteBookModalOpen(false);
}}></CreateBookModal>

接下来在 interfaces/index.ts 里添加 /book/create 接口:

export async function create(book: CreateBook) {
return await axiosInstance.post("/book/create", {
name: book.name,
author: book.author,
description: book.description,
cover: book.cover,
});
}
然后在 CreateBookModal 组件调用下:

const handleOk = async function () {
await form.validateFields();
const values = form.getFieldsValue();
try {
const res = await create(values);
if (res.status === 201 || res.status === 200) {
message.success("创建成功");
form.resetFields();
props.handleClose();
}
} catch (e: any) {
message.error(e.response.data.message);
}
};
测试下:

添加成功,刷新页面就可以看到新的图书了。
只是这个封面的路径还没上传文件。
我们在 handleClose 的回调里调用下 setState 触发刷新:

这样创建成功就会自动刷新列表:

然后我们来做一下文件上传:
新建 BookManage/Coverupload.tsx
import { InboxOutlined } from "@ant-design/icons";
import { message } from "antd";
import Dragger, { DraggerProps } from "antd/es/upload/Dragger";
interface CoverUploadProps {
value?: string;
onChange?: Function;
}
let onChange: Function;
const props: DraggerProps = {
name: "file",
action: "http://localhost:3000/book/upload",
method: "post",
onChange(info) {
const { status } = info.file;
if (status === "done") {
onChange(info.file.response);
message.success(`${info.file.name} 文件上传成功`);
} else if (status === "error") {
message.error(`${info.file.name} 文件上传失败`);
}
},
};
const dragger = (
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">点击或拖拽文件到这个区域来上传</p>
</Dragger>
);
export function CoverUpload(props: CoverUploadProps) {
onChange = props.onChange!;
return props?.value ? (
<div>
<img
src={"http://localhost:3000/" + props.value}
alt="封面"
width="100"
height="100"
/>
{dragger}
</div>
) : (
<div>{dragger}</div>
);
}
用 antd 的 Dragger 组件做拖拽上传。
当拖拽文件过去的时候,会自动上传,然后在 onChange 方法里拿到上传状态来做提示。
我们在 CreateBookModal 里用一下:

试下效果:

封面上传成功。
在服务器 uploads 目录下也可以看到这个文件:

这样,新增图书功能就完成了。
我们继续来做下修改:
和新增差不多,创建一个 BookManage/UpdateBookModal.tsx 组件:
import { Button, Form, Input, Modal, message } from "antd";
import { useForm } from "antd/es/form/Form";
import TextArea from "antd/es/input/TextArea";
import { CoverUpload } from "./CoverUpload";
interface UpdateBookModalProps {
id: number;
isOpen: boolean;
handleClose: Function;
}
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 18 },
};
export interface UpdateBook {
id: number;
name: string;
author: string;
description: string;
cover: string;
}
export function UpdateBookModal(props: UpdateBookModalProps) {
const [form] = useForm<UpdateBook>();
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="name"
rules={[{ required: true, message: "请输入图书名称!" }]}>
<Input />
</Form.Item>
<Form.Item
label="作者"
name="author"
rules={[{ required: true, message: "请输入图书作者!" }]}>
<Input />
</Form.Item>
<Form.Item
label="描述"
name="description"
rules={[{ required: true, message: "请输入图书描述!" }]}>
<TextArea />
</Form.Item>
<Form.Item
label="封面"
name="cover"
rules={[{ required: true, message: "请上传图书封面!" }]}>
<CoverUpload></CoverUpload>
</Form.Item>
</Form>
</Modal>
);
}
和 CreateBookModal 的区别是参数多了个 id。
在页面里引入下:


就是声明一个 isUpdateModalOpen 的 state 来控制弹窗的显示隐藏。
然后声明一个 updateId 的 state 来记录当前更新的 id。
点击图书的编辑按钮的时候,设置 updateId,并打开弹窗。
const [isUpdateBookModalOpen, setUpdateBookModalOpen] = useState(false);
const [updateId, setUpdateId] = useState(0);
<UpdateBookModal
id={updateId}
isOpen={isUpdateBookModalOpen}
handleClose={() => {
setUpdateBookModalOpen(false);
setName("");
}}></UpdateBookModal>
<a
href="#"
onClick={() => {
setUpdateId(book.id);
setUpdateBookModalOpen(true);
}}>
编辑
</a>

然后在弹窗里根据 id 请求下数据:
在 interfaces/index.ts 里加一下接口:

export async function detail(id: number) {
return await axiosInstance.get(`/book/${id}`);
}

async function query() {
if (!props.id) {
return;
}
try {
const res = await detail(props.id);
const { data } = res;
debugger;
if (res.status === 200 || res.status === 201) {
form.setFieldValue("id", data.id);
form.setFieldValue("name", data.name);
form.setFieldValue("author", data.author);
form.setFieldValue("description", data.description);
form.setFieldValue("cover", data.cover);
}
} catch (e: any) {
message.error(e.response.data.message);
}
}
useEffect(() => {
query();
}, [props.id]);
试一下:

然后点击更新按钮时候调用下更新接口:
改下 interfaces/index.ts

export async function update(book: UpdateBook) {
return await axiosInstance.put("/book/update", {
id: book.id,
name: book.name,
author: book.author,
description: book.description,
cover: book.cover,
});
}
组件里调用下:

const handleOk = async function () {
await form.validateFields();
const values = form.getFieldsValue();
try {
const res = await update({ ...values, id: props.id });
if (res.status === 201 || res.status === 200) {
message.success("更新成功");
props.handleClose();
}
} catch (e: any) {
message.error(e.response.data.message);
}
};
试下效果:

更新成功。
但是我们要手动刷新下页面,因为 name 没变,没触发重新请求:

改一下:

加一个随机数的状态,当这个状态变了就重新请求。

这样就好了。
更新功能完成了。
详情也是一样的弹窗,比较简单,和更新差不多,我们就不实现了。
再来实现下删除:

点击按钮按钮的有个二次确认的弹窗,确认后执行删除逻辑:
<Popconfirm
title="图书删除"
description="确认删除吗?"
onConfirm={() => handleDelete(book.id)}
okText="Yes"
cancelText="No">
<a href="#">删除</a>
</Popconfirm>
在 interfaces/index.ts 加一下删除接口:

export async function deleteBook(id: number) {
return await axiosInstance.delete(`/book/delete/${id}`);
}
在组件里调用下:

async function handleDelete(id: number) {
try {
await deleteBook(id);
message.success("删除成功");
setNum(Math.random());
} catch (e: any) {
message.error(e.response.data.message);
}
}

删除功能完成。
案例代码上传了小册仓库
总结
这节我们实现了图书的新增、编辑、删除功能。
用 antd 的 Modal 创建弹窗,在父组件里通过 isOpen 控制显示隐藏,并做 onClose 时的处理。
然后用 antd 的 Dragger 实现了封面图片的上传。
创建图书的弹窗就是输入内容后调用 /book/create 接口。
更新图书的弹窗就是打开弹窗的时候根据 id 查询内容,点更新的时候调用 /book/udpate 接口。
删除的时候用 Popconfirm 组件做下二次确认。
这样,图书模块的前端部分就完成了。
