跳到主要内容

考试系统:新增试卷、回收站

这节我们实现新增试卷和回收站的功能。

先在 interfaces/index.tsx 加一下 add 接口:

export async function examAdd(values: ExamAdd) {
return await examServiceInstance.post("/exam/add", values);
}

然后写一下新增试卷的弹窗:

pages/ExamList/ExamAddModal.tsx

import { Button, Form, Input, Modal, message } from "antd";
import { useForm } from "antd/es/form/Form";
import TextArea from "antd/es/input/TextArea";
import { examAdd } from "../../interfaces";

interface ExamAddModalProps {
isOpen: boolean;
handleClose: Function;
}

const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 18 },
};

export interface ExamAdd {
name: string;
}

export function ExamAddModal(props: ExamAddModalProps) {
const [form] = useForm<ExamAdd>();

const handleOk = async function () {
await form.validateFields();

const values = form.getFieldsValue();

try {
const res = await examAdd(values);

if (res.status === 201 || res.status === 200) {
message.success("创建成功");
form.resetFields();
props.handleClose();
}
} catch (e: any) {
message.error(e.response?.data?.message || "系统繁忙,请稍后再试");
}
};

return (
<Modal
title="新增试卷"
open={props.isOpen}
onOk={handleOk}
onCancel={() => props.handleClose()}
okText={"创建"}
cancelText={"取消"}>
<Form form={form} colon={false} {...layout}>
<Form.Item
label="试卷名"
name="name"
rules={[{ required: true, message: "请输入试卷名!" }]}>
<Input />
</Form.Item>
</Form>
</Modal>
);
}

Modal 内是一个表单,点击创建会调用 examAdd 接口。

在 ExamList/index.tsx 里调用下:

image.png

image.png

import { Button, message } from "antd";
import "./index.scss";
import { useEffect, useState } from "react";
import { examList } from "../../interfaces";
import { ExamAddModal } from "./ExamAddModal";

interface Exam {
id: number;
name: string;
isPublish: boolean;
isDelete: boolean;
content: string;
}

export function ExamList() {
const [list, setList] = useState<Array<Exam>>();
const [isExamAddModalOpen, setIsExamAddModalOpen] = useState(false);

async function query() {
try {
const res = await examList();
if (res.status === 201 || res.status === 200) {
setList(res.data);
}
} catch (e: any) {
message.error(e.response?.data?.message || "系统繁忙,请稍后再试");
}
}

useEffect(() => {
query();
}, []);

return (
<div id="ExamList-container">
<div className="header">
<h1>考试系统</h1>
</div>
<div className="body">
<div className="operate">
<Button
type="primary"
onClick={() => {
setIsExamAddModalOpen(true);
}}>
新建试卷
</Button>
</div>
<div className="list">
{list?.map((item) => {
return (
<div className="item">
<p>{item.name}</p>
<div className="btns">
<Button
className="btn"
type="primary"
style={{ background: "darkblue" }}>
{item.isPublish ? "停止" : "发布"}
</Button>
<Button
className="btn"
type="primary"
style={{ background: "green" }}>
编辑
</Button>
<Button
className="btn"
type="primary"
style={{ background: "darkred" }}>
删除
</Button>
</div>
</div>
);
})}
</div>
</div>
<ExamAddModal
isOpen={isExamAddModalOpen}
handleClose={() => {
setIsExamAddModalOpen(false);
query();
}}
/>
</div>
);
}

测试下:

没啥问题。

然后实现发布状态的修改:

image.png

之前我们只实现了 publish 接口,没实现 unpublish 接口。

在后端代码加一下:

image.png

@Get('unpublish/:id')
@RequireLogin()
async unpublish(@UserInfo('userId') userId: number, @Param('id') id: string) {
return this.examService.unpublish(userId, +id);
}

image.png

async unpublish(userId: number, id: number) {
return this.prismaService.exam.update({
where: {
id,
createUserId: userId
},
data: {
isPublish: false
}
})
}

试下效果:

image.png

image.png

在前端代码的 interfaces/index.tsx 调用下:

export async function examPublish(id: number) {
return await examServiceInstance.get("/exam/publish/" + id);
}

export async function examUnpublish(id: number) {
return await examServiceInstance.get("/exam/unpublish/" + id);
}

然后在 ExamList/index.tsx 组件里用一下:

image.png

image.png

async function changePublishState(id: number, publish: boolean) {
try {
const res = publish ? await examUnpublish(id) : await examPublish(id);
if (res.status === 201 || res.status === 200) {
message.success(publish ? "已取消发布" : "已发布");
query();
}
} catch (e: any) {
message.error(e.response?.data?.message || "系统繁忙,请稍后再试");
}
}

试一下:

2024-08-26 13.08.07.gif

没啥问题。

然后再来实现删除功能。

在 interfaces/index.tsx 增加删除接口:

export async function examDelete(id: number) {
return await examServiceInstance.delete("/exam/delete/" + id);
}

点击删除按钮的时候调用下:

image.png

image.png

async function deleteExam(id: number) {
try {
const res = await examDelete(id);
if (res.status === 201 || res.status === 200) {
message.success("已删除");
query();
}
} catch (e: any) {
message.error(e.response?.data?.message || "系统繁忙,请稍后再试");
}
}

试一下:

2024-08-26 13.13.16.gif

这时候其实已经删除了,只不过我们用的逻辑删除。

在列表过滤下:

image.png

list?.filter((item) => {
return !item.isDelete;
});

2024-08-26 13.15.31.gif

然后支持下回收站的显示:

image.png

加一个状态表示是否在回收站,点击按钮切换状态。

显示的时候根据是否在回收站决定显示 isDelete 是 true 还是 false 的数据。

const [bin, setBin] = useState(false);
<Button
onClick={() => {
setBin((bin) => !bin);
}}>
{bin ? "退出回收站" : "打开回收站"}
</Button>
list?.filter((item) => {
return bin ? item.isDelete === true : item.isDelete === false;
});

2024-08-26 13.20.59.gif

这样,回收站功能就完成了。

我们再给删除加个二次确认弹窗:

image.png

<Popconfirm
title="试卷删除"
description="确认放入回收站吗?"
onConfirm={() => deleteExam(item.id)}
okText="Yes"
cancelText="No">
<Button className="btn" type="primary" style={{ background: "darkred" }}>
删除
</Button>
</Popconfirm>

2024-08-26 13.23.32.gif

整体测试下:

创建两个考试:

2024-08-26 13.24.19.gif

2024-08-26 13.24.52.gif

删除其中一个,然后进回收站看一下:

2024-08-26 13.25.29.gif

再试下发布功能:

2024-08-26 13.26.01.gif

没啥问题。

这样,试卷的新增、删除、回收站就都完成了。

案例代码在小册仓库:

前端代码

后端代码

总结

这节我们实现了试卷新增、发布试卷、回收站功能。

新增试卷会弹窗展示表单,输入信息后调用接口并刷新列表。

删除、发布后也会刷新列表。

回收站就是根据 isDelete 来过滤列表里的数据。

下节我们来做试卷编辑的功能。