Merge branch 'dev-tmp1' of http://47.92.113.182:3000/cq_anquan/safety-eval-service-frontend into dev-tmp1
commit
ca38ee44ae
13093
pnpm-lock.yaml
13093
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,82 @@
|
|||
import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
|
||||
|
||||
/** 教育培训 — 试卷管理 */
|
||||
|
||||
/** 分页查询试卷 */
|
||||
export const paperPage = declareRequest(
|
||||
"paperLoading",
|
||||
"Get > /safetyEval/paper/page",
|
||||
"paperList: [] | res.data || [] & paperTotal: 0 | res.total || 0",
|
||||
);
|
||||
|
||||
/** 修改试卷基本信息 */
|
||||
export const paperUpdateBasic = declareRequest(
|
||||
"paperUpdateLoading",
|
||||
"Put > @/safetyEval/paper/update/basic",
|
||||
);
|
||||
|
||||
/** 删除试卷 */
|
||||
export const paperDelete = declareRequest(
|
||||
"paperLoading",
|
||||
"Delete > @/safetyEval/paper/{id}",
|
||||
);
|
||||
|
||||
/** 复制试卷 */
|
||||
export const paperCopy = declareRequest(
|
||||
"paperCopyLoading",
|
||||
"Post > @/safetyEval/paper/copy",
|
||||
);
|
||||
|
||||
/** 导入试题模板地址 */
|
||||
export const paperImportTemplate = declareRequest(
|
||||
"paperTemplateLoading",
|
||||
"Get > /safetyEval/paper/import/template",
|
||||
'paperTemplateUrl: "" | res.data || ""',
|
||||
);
|
||||
|
||||
/**
|
||||
* 新建试卷-导入试题(multipart,declareRequest 仅支持 json,手动 fetch)
|
||||
* @param {FormData} payload paperName/paperTotalScore/paperPassScore/examTime/file
|
||||
*/
|
||||
export async function paperImport(payload) {
|
||||
const res = await fetch(
|
||||
`${window.process.env.app.API_HOST}/safetyEval/paper/import`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { token: sessionStorage.getItem("token") },
|
||||
body: payload,
|
||||
},
|
||||
);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** 试卷试题分页查询 */
|
||||
export const paperQuestionPage = declareRequest(
|
||||
"paperQuestionLoading",
|
||||
"Get > /safetyEval/paper/paper/question/page",
|
||||
"paperQuestionList: [] | res.data || [] & paperQuestionTotal: 0 | res.total || 0",
|
||||
);
|
||||
|
||||
/** 删除试卷习题 */
|
||||
export const paperQuestionDelete = declareRequest(
|
||||
"paperQuestionLoading",
|
||||
"Post > @/safetyEval/paperQuestionRel/delete",
|
||||
);
|
||||
|
||||
/** 新增试卷试题(习题) */
|
||||
export const paperQuestionSave = declareRequest(
|
||||
"paperQuestionSaveLoading",
|
||||
"Post > @/safetyEval/paperQuestionRel/save",
|
||||
);
|
||||
|
||||
/** 查询试卷习题详情(分值取自试卷试题关系) */
|
||||
export const paperQuestionFind = declareRequest(
|
||||
"questionFindLoading",
|
||||
"Get > /safetyEval/paperQuestionRel/find",
|
||||
);
|
||||
|
||||
/** 编辑试卷习题 */
|
||||
export const paperQuestionModify = declareRequest(
|
||||
"questionModifyLoading",
|
||||
"Post > @/safetyEval/paperQuestionRel/modify",
|
||||
);
|
||||
|
|
@ -661,3 +661,26 @@ export const COURSEWARE_TYPE_MAP = COURSEWARE_TYPE_OPTIONS.reduce(
|
|||
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
|
||||
{},
|
||||
);
|
||||
|
||||
/** 试卷类型(fixed平台试卷/config_rule规则试卷) */
|
||||
export const PAPER_TYPE_OPTIONS = [
|
||||
{ label: "平台试卷", value: "fixed" },
|
||||
{ label: "规则试卷", value: "config_rule" },
|
||||
];
|
||||
|
||||
export const PAPER_TYPE_MAP = PAPER_TYPE_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
|
||||
{},
|
||||
);
|
||||
|
||||
/** 题型(single单选/multiple多选/judge判断) */
|
||||
export const QUESTION_TYPE_OPTIONS = [
|
||||
{ label: "单选", value: "single" },
|
||||
{ label: "多选", value: "multiple" },
|
||||
{ label: "判断", value: "judge" },
|
||||
];
|
||||
|
||||
export const QUESTION_TYPE_MAP = QUESTION_TYPE_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
|
||||
{},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const NS_PERSNONEL_CERTFICATE = defineNamespace("personnelCertificate");
|
|||
export const NS_CORP_CERTIFICATE = defineNamespace("corpCertificate");
|
||||
export const NS_USER_CERTIFICATE = defineNamespace("userCertificate");
|
||||
export const NS_COURSEWARE = defineNamespace("courseware");
|
||||
export const NS_PAPER = defineNamespace("paper");
|
||||
export const NS_ORG_INFO = defineNamespace("orgInfo");
|
||||
export const NS_ORG_QUALIFICATION_CERT = defineNamespace("orgQualificationCert");
|
||||
export const NS_ORG_DEPARTMENT = defineNamespace("orgDepartment");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,538 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
message,
|
||||
} from "antd";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
|
||||
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
|
||||
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
|
||||
import SearchForm from "~/components/SearchForm";
|
||||
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
|
||||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||
import { tools } from "@cqsjjb/jjb-common-lib";
|
||||
import { NS_PAPER, NS_COURSEWARE } from "~/enumerate/namespace";
|
||||
import { QUESTION_TYPE_MAP, QUESTION_TYPE_OPTIONS } from "~/enumerate/constant";
|
||||
|
||||
const { router } = tools;
|
||||
|
||||
/** 按题型生成默认选项(判断题固定正确/错误两项) */
|
||||
const buildDefaultOptions = (questionType) =>
|
||||
questionType === "judge"
|
||||
? [
|
||||
{ optionKey: "A", optionText: "正确", isCorrect: false },
|
||||
{ optionKey: "B", optionText: "错误", isCorrect: false },
|
||||
]
|
||||
: ["A", "B", "C", "D"].map((key) => ({
|
||||
optionKey: key,
|
||||
optionText: "",
|
||||
isCorrect: false,
|
||||
}));
|
||||
|
||||
/** 生成下一个选项标识:按 26 个大写字母顺序 */
|
||||
const nextOptionKey = (list = []) =>
|
||||
String.fromCharCode(65 + list.length) || "A";
|
||||
|
||||
/** 试卷试题管理 — 详情页(由试卷列表「查看」进入) */
|
||||
function PaperQuestionPage(props) {
|
||||
const [searchForm] = Form.useForm();
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [editQuestion, setEditQuestion] = useState(null);
|
||||
const paperId = router.query?.paperId;
|
||||
const paperName = router.query?.paperName;
|
||||
const { paper, courseware } = props;
|
||||
const {
|
||||
paperQuestionList: dataSource,
|
||||
paperQuestionTotal: total,
|
||||
paperQuestionLoading: loading,
|
||||
} = paper || {};
|
||||
const coursewareOptions = (courseware?.coursewareList || []).map((c) => ({
|
||||
label: c.coursewareName,
|
||||
value: c.id,
|
||||
}));
|
||||
|
||||
const handleSearch = () => {
|
||||
props.paperQuestionPage(router.query);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
searchForm.setFieldsValue({
|
||||
title: router.query.title,
|
||||
questionType: router.query.questionType,
|
||||
});
|
||||
handleSearch();
|
||||
// 关联课件下拉数据(新增/编辑试题时选择)
|
||||
props.coursewarePage({ current: 1, size: 200 });
|
||||
}, []);
|
||||
|
||||
const onDelete = (record) => {
|
||||
Modal.confirm({
|
||||
title: "提示",
|
||||
content: "数据将会删除,您是否确认删除?",
|
||||
okText: "是",
|
||||
cancelText: "否",
|
||||
onOk: async () => {
|
||||
const res = await props.paperQuestionDelete({
|
||||
paperId,
|
||||
questionId: record.id,
|
||||
});
|
||||
if (res?.success !== false) {
|
||||
message.success("删除成功");
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "序号",
|
||||
width: 70,
|
||||
fixed: "left",
|
||||
render: (_, __, index) => {
|
||||
const current = Number(router.query.current) || 1;
|
||||
const size = Number(router.query.size) || 10;
|
||||
return (current - 1) * size + index + 1;
|
||||
},
|
||||
},
|
||||
{ title: "试题号", dataIndex: "questionNum", width: 80 },
|
||||
{
|
||||
title: "题干",
|
||||
dataIndex: "title",
|
||||
width: 280,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "题型",
|
||||
dataIndex: "questionType",
|
||||
width: 80,
|
||||
render: (v) => QUESTION_TYPE_MAP[v] ?? (v || "-"),
|
||||
},
|
||||
{ title: "分值", dataIndex: "score", width: 80 },
|
||||
{ title: "答案", dataIndex: "answer", width: 90 },
|
||||
{
|
||||
title: "关联课件",
|
||||
dataIndex: "coursewareName",
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "答案解析",
|
||||
dataIndex: "analysis",
|
||||
width: 220,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 120,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<TableAction>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => setEditQuestion(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => onDelete(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</TableAction>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title={`试卷试题管理${paperName ? ` - ${paperName}` : ""}`}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setAddOpen(true)}
|
||||
>
|
||||
新增试题
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<SearchForm
|
||||
style={{ marginBottom: 24 }}
|
||||
form={searchForm}
|
||||
loading={loading}
|
||||
formLine={[
|
||||
<Form.Item key="title" name="title">
|
||||
<ControlWrapper.Input
|
||||
label="题干"
|
||||
placeholder="请输入题干关键字"
|
||||
allowClear
|
||||
maxLength={50}
|
||||
/>
|
||||
</Form.Item>,
|
||||
<Form.Item key="questionType" name="questionType">
|
||||
<ControlWrapper.Select
|
||||
label="题型"
|
||||
placeholder="请选择"
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
options={QUESTION_TYPE_OPTIONS}
|
||||
/>
|
||||
</Form.Item>,
|
||||
]}
|
||||
onReset={(value) => {
|
||||
router.query = { ...value, paperId, current: 1, size: 10 };
|
||||
handleSearch();
|
||||
}}
|
||||
onFinish={(value) => {
|
||||
router.query = { ...value, paperId, current: 1, size: 10 };
|
||||
handleSearch();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={Array.isArray(dataSource) ? dataSource : []}
|
||||
scroll={{ x: 1300, y: props.scrollY }}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
current: Number(router.query.current) || 1,
|
||||
pageSize: Number(router.query.size) || 10,
|
||||
onChange: (page, pageSize) => {
|
||||
router.query = { ...router.query, current: page, size: pageSize };
|
||||
handleSearch();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<QuestionFormModal
|
||||
open={addOpen}
|
||||
mode="add"
|
||||
coursewareOptions={coursewareOptions}
|
||||
confirmLoading={paper?.paperQuestionSaveLoading}
|
||||
onCancel={() => {
|
||||
setAddOpen(false);
|
||||
props.resetModelState(NS_PAPER, { paperQuestionSaveLoading: false });
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
const res = await props.paperQuestionSave({ paperId, ...values });
|
||||
if (res?.success !== false) {
|
||||
message.success("新增成功");
|
||||
setAddOpen(false);
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<QuestionFormModal
|
||||
open={!!editQuestion}
|
||||
mode="edit"
|
||||
paperId={paperId}
|
||||
questionId={editQuestion?.id}
|
||||
coursewareOptions={coursewareOptions}
|
||||
findLoading={paper?.questionFindLoading}
|
||||
confirmLoading={paper?.questionModifyLoading}
|
||||
questionFind={props.paperQuestionFind}
|
||||
onCancel={() => {
|
||||
setEditQuestion(null);
|
||||
props.resetModelState(NS_PAPER, {
|
||||
questionFindLoading: false,
|
||||
questionModifyLoading: false,
|
||||
});
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
const res = await props.paperQuestionModify({
|
||||
id: editQuestion?.id,
|
||||
paperId,
|
||||
...values,
|
||||
});
|
||||
if (res?.success !== false) {
|
||||
message.success("修改成功");
|
||||
setEditQuestion(null);
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增/编辑试题弹窗(编辑时先调查询接口回填) */
|
||||
function QuestionFormModal({
|
||||
open,
|
||||
mode,
|
||||
paperId,
|
||||
questionId,
|
||||
coursewareOptions,
|
||||
findLoading,
|
||||
confirmLoading,
|
||||
questionFind,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const isEdit = mode === "edit";
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.resetFields();
|
||||
return;
|
||||
}
|
||||
if (isEdit) {
|
||||
// 编辑:查试卷习题详情(分值取自试卷试题关系)
|
||||
questionFind({ paperId, questionId }).then((res) => {
|
||||
if (res?.success === false) return;
|
||||
const detail = res?.data || {};
|
||||
form.setFieldsValue({
|
||||
questionType: detail.questionType,
|
||||
title: detail.title,
|
||||
score: detail.score,
|
||||
coursewareManagementId: detail.coursewareManagementId,
|
||||
tagType: detail.tagType,
|
||||
analysis: detail.analysis,
|
||||
questionOptionList: (detail.questionOptionVOList || []).map((o) => ({
|
||||
id: o.id,
|
||||
optionKey: o.optionKey,
|
||||
optionText: o.optionText,
|
||||
isCorrect: !!o.isCorrect,
|
||||
})),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
questionType: "single",
|
||||
questionOptionList: buildDefaultOptions("single"),
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
/** 题型切换:重建默认选项(仅新增) */
|
||||
const onQuestionTypeChange = (value) => {
|
||||
form.setFieldsValue({ questionOptionList: buildDefaultOptions(value) });
|
||||
};
|
||||
|
||||
/** 正确答案联动:单选/判断仅允许一个正确答案 */
|
||||
const onCorrectChange = (index, e) => {
|
||||
const questionType = form.getFieldValue("questionType");
|
||||
if (e.target.checked && questionType !== "multiple") {
|
||||
const list = (form.getFieldValue("questionOptionList") || []).map(
|
||||
(item, i) => (i === index ? item : { ...item, isCorrect: false }),
|
||||
);
|
||||
form.setFieldValue("questionOptionList", list);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const list = values.questionOptionList || [];
|
||||
if (!list.some((o) => o.isCorrect)) {
|
||||
message.warning("请至少设置一个正确答案");
|
||||
return;
|
||||
}
|
||||
const keys = list.map((o) => o.optionKey?.trim());
|
||||
if (new Set(keys).size !== keys.length) {
|
||||
message.warning("选项标识不能重复");
|
||||
return;
|
||||
}
|
||||
await onSubmit(values);
|
||||
} catch {
|
||||
// validation error
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
destroyOnHidden
|
||||
title={isEdit ? "编辑试题" : "新增试题"}
|
||||
width={680}
|
||||
confirmLoading={confirmLoading}
|
||||
onOk={handleOk}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
scrollToFirstError
|
||||
disabled={findLoading}
|
||||
>
|
||||
<Space style={{ display: "flex" }} align="start">
|
||||
<Form.Item
|
||||
name="questionType"
|
||||
label="题型"
|
||||
rules={[{ required: true, message: "请选择题型" }]}
|
||||
style={{ width: 140, marginBottom: 16 }}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择"
|
||||
disabled={isEdit}
|
||||
options={QUESTION_TYPE_OPTIONS}
|
||||
onChange={onQuestionTypeChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="score"
|
||||
label="分值"
|
||||
rules={[{ required: true, message: "请输入分值" }]}
|
||||
style={{ width: 140, marginBottom: 16 }}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: "100%" }}
|
||||
min={0.1}
|
||||
max={999.9}
|
||||
step={0.5}
|
||||
placeholder="0.1-999.9"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="coursewareManagementId"
|
||||
label="关联课件"
|
||||
rules={[{ required: true, message: "请选择关联课件" }]}
|
||||
style={{ width: 300, marginBottom: 16 }}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择关联课件"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={coursewareOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="题干"
|
||||
rules={[{ required: true, message: "请输入题干" }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
placeholder="请输入题干"
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="tagType" label="标签类型">
|
||||
<Input placeholder="请输入标签类型" maxLength={50} allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="试题选项(勾选正确答案;选项标识如 A、B、C)"
|
||||
required
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Form.List
|
||||
name="questionOptionList"
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) =>
|
||||
value?.length >= 2
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error("请至少填写两个选项")),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
{fields.map((field) => (
|
||||
<Form.Item key={field.key} style={{ marginBottom: 8 }}>
|
||||
<Space align="center">
|
||||
<Form.Item
|
||||
noStyle
|
||||
name={[field.name, "id"]}
|
||||
/>
|
||||
<Form.Item
|
||||
noStyle
|
||||
name={[field.name, "optionKey"]}
|
||||
rules={[{ required: true, message: "请输入标识" }]}
|
||||
>
|
||||
<Input placeholder="标识" style={{ width: 80 }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
name={[field.name, "optionText"]}
|
||||
rules={[{ required: true, message: "请输入选项内容" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入选项内容"
|
||||
style={{ width: 360 }}
|
||||
maxLength={200}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
name={[field.name, "isCorrect"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox
|
||||
onChange={(e) => onCorrectChange(field.name, e)}
|
||||
>
|
||||
正确答案
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
disabled={fields.length <= 2}
|
||||
onClick={() => remove(field.name)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
add({
|
||||
optionKey: nextOptionKey(
|
||||
form.getFieldValue("questionOptionList"),
|
||||
),
|
||||
optionText: "",
|
||||
isCorrect: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
添加选项
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
<Form.Item name="analysis" label="答案解析">
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="请输入答案解析"
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default Connect([NS_PAPER, NS_COURSEWARE], true)(
|
||||
AntdTableFuncControl(PaperQuestionPage),
|
||||
);
|
||||
|
|
@ -0,0 +1,530 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Table,
|
||||
Upload,
|
||||
message,
|
||||
} from "antd";
|
||||
import {
|
||||
DownloadOutlined,
|
||||
PlusOutlined,
|
||||
UploadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
|
||||
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
|
||||
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
|
||||
import SearchForm from "~/components/SearchForm";
|
||||
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
|
||||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||
import { tools } from "@cqsjjb/jjb-common-lib";
|
||||
import { NS_PAPER } from "~/enumerate/namespace";
|
||||
import { PAPER_TYPE_MAP, PAPER_TYPE_OPTIONS } from "~/enumerate/constant";
|
||||
import { paperImport } from "~/api/paper";
|
||||
|
||||
const { router } = tools;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
/** 搜索表单值 → 查询参数:时间范围拆为后端字段(yyyy-MM-dd) */
|
||||
const buildQuery = (value) => {
|
||||
const { createTimeRange, ...rest } = value || {};
|
||||
return {
|
||||
...rest,
|
||||
...(createTimeRange?.[0]
|
||||
? { createTimeStart: createTimeRange[0].format("YYYY-MM-DD") }
|
||||
: {}),
|
||||
...(createTimeRange?.[1]
|
||||
? { createTimeEnd: createTimeRange[1].format("YYYY-MM-DD") }
|
||||
: {}),
|
||||
current: 1,
|
||||
size: 10,
|
||||
};
|
||||
};
|
||||
|
||||
/** 试卷管理 — 列表 */
|
||||
function PaperManagePage(props) {
|
||||
const [searchForm] = Form.useForm();
|
||||
const [editRecord, setEditRecord] = useState(null);
|
||||
const [copyRecord, setCopyRecord] = useState(null);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const { paper } = props;
|
||||
const { paperList: dataSource, paperTotal: total, paperLoading: loading } =
|
||||
paper || {};
|
||||
|
||||
const handleSearch = () => {
|
||||
props.paperPage(router.query);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { createTimeStart, createTimeEnd, ...rest } = router.query;
|
||||
searchForm.setFieldsValue({
|
||||
...rest,
|
||||
...(createTimeStart && createTimeEnd
|
||||
? {
|
||||
createTimeRange: [dayjs(createTimeStart), dayjs(createTimeEnd)],
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
handleSearch();
|
||||
}, []);
|
||||
|
||||
/** 下载导入模板 */
|
||||
const onDownloadTemplate = async () => {
|
||||
const res = await props.paperImportTemplate();
|
||||
const url = res?.paperTemplateUrl || res?.data;
|
||||
if (url) {
|
||||
window.open(url);
|
||||
} else {
|
||||
message.warning("模板地址未配置");
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = (record) => {
|
||||
Modal.confirm({
|
||||
title: "提示",
|
||||
content: "数据将会删除,您是否确认删除?",
|
||||
okText: "是",
|
||||
cancelText: "否",
|
||||
onOk: async () => {
|
||||
const res = await props.paperDelete({ id: record.id });
|
||||
if (res?.success !== false) {
|
||||
message.success("删除成功");
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "序号",
|
||||
width: 70,
|
||||
fixed: "left",
|
||||
render: (_, __, index) => {
|
||||
const current = Number(router.query.current) || 1;
|
||||
const size = Number(router.query.size) || 10;
|
||||
return (current - 1) * size + index + 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "试卷名称",
|
||||
dataIndex: "paperName",
|
||||
width: 260,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: "试卷类型",
|
||||
dataIndex: "paperType",
|
||||
width: 110,
|
||||
render: (v) => PAPER_TYPE_MAP[v] ?? (v || "-"),
|
||||
},
|
||||
{ title: "试卷总分", dataIndex: "paperTotalScore", width: 100 },
|
||||
{ title: "合格分数", dataIndex: "paperPassScore", width: 100 },
|
||||
{
|
||||
title: "考试时长(分钟)",
|
||||
dataIndex: "examTime",
|
||||
width: 130,
|
||||
render: (v) => v ?? "-",
|
||||
},
|
||||
{ title: "创建时间", dataIndex: "createTime", width: 170 },
|
||||
{
|
||||
title: "操作",
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
render: (_, record) => (
|
||||
<TableAction>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() =>
|
||||
props.history.push(
|
||||
`Detail?paperId=${record.id}&paperName=${encodeURIComponent(
|
||||
record.paperName || "",
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => setEditRecord(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => setCopyRecord(record)}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => onDelete(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</TableAction>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title="试卷管理"
|
||||
extra={
|
||||
<>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
loading={paper?.paperTemplateLoading}
|
||||
onClick={onDownloadTemplate}
|
||||
>
|
||||
下载导入模板
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setImportOpen(true)}
|
||||
>
|
||||
导入试题新建试卷
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SearchForm
|
||||
style={{ marginBottom: 24 }}
|
||||
form={searchForm}
|
||||
loading={loading}
|
||||
formLine={[
|
||||
<Form.Item key="paperName" name="paperName">
|
||||
<ControlWrapper.Input
|
||||
label="试卷名称"
|
||||
placeholder="请输入名称"
|
||||
allowClear
|
||||
maxLength={50}
|
||||
/>
|
||||
</Form.Item>,
|
||||
<Form.Item key="paperType" name="paperType">
|
||||
<ControlWrapper.Select
|
||||
label="试卷类型"
|
||||
placeholder="请选择"
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
options={PAPER_TYPE_OPTIONS}
|
||||
/>
|
||||
</Form.Item>,
|
||||
<Form.Item key="createTimeRange" name="createTimeRange">
|
||||
<ControlWrapper.DatePicker.RangePicker label="创建时间" />
|
||||
</Form.Item>,
|
||||
]}
|
||||
onReset={(value) => {
|
||||
router.query = buildQuery(value);
|
||||
handleSearch();
|
||||
}}
|
||||
onFinish={(value) => {
|
||||
router.query = buildQuery(value);
|
||||
handleSearch();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={Array.isArray(dataSource) ? dataSource : []}
|
||||
scroll={{ x: 1200, y: props.scrollY }}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
current: Number(router.query.current) || 1,
|
||||
pageSize: Number(router.query.size) || 10,
|
||||
onChange: (page, pageSize) => {
|
||||
router.query = { ...router.query, current: page, size: pageSize };
|
||||
handleSearch();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<BasicInfoModal
|
||||
open={!!editRecord}
|
||||
title="编辑试卷"
|
||||
initial={editRecord}
|
||||
confirmLoading={paper?.paperUpdateLoading}
|
||||
onCancel={() => {
|
||||
setEditRecord(null);
|
||||
props.resetModelState(NS_PAPER, { paperUpdateLoading: false });
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
const res = await props.paperUpdateBasic({
|
||||
id: editRecord?.id,
|
||||
...values,
|
||||
});
|
||||
if (res?.success !== false) {
|
||||
message.success("修改成功");
|
||||
setEditRecord(null);
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<BasicInfoModal
|
||||
open={!!copyRecord}
|
||||
title="复制试卷"
|
||||
initial={
|
||||
copyRecord && { ...copyRecord, paperName: `${copyRecord.paperName}-副本` }
|
||||
}
|
||||
confirmLoading={paper?.paperCopyLoading}
|
||||
onCancel={() => {
|
||||
setCopyRecord(null);
|
||||
props.resetModelState(NS_PAPER, { paperCopyLoading: false });
|
||||
}}
|
||||
onSubmit={async (values) => {
|
||||
const res = await props.paperCopy({ id: copyRecord?.id, ...values });
|
||||
if (res?.success !== false) {
|
||||
message.success("复制成功");
|
||||
setCopyRecord(null);
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ImportPaperModal
|
||||
open={importOpen}
|
||||
onCancel={() => setImportOpen(false)}
|
||||
onSuccess={handleSearch}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
/** 编辑/复制试卷基本信息弹窗(后端仅更新名称/总分/合格分) */
|
||||
function BasicInfoModal({ open, title, initial, confirmLoading, onSubmit, onCancel }) {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (open && initial) {
|
||||
form.setFieldsValue({
|
||||
paperName: initial.paperName,
|
||||
paperTotalScore: initial.paperTotalScore,
|
||||
paperPassScore: initial.paperPassScore,
|
||||
});
|
||||
}
|
||||
if (!open) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
await onSubmit(values);
|
||||
} catch {
|
||||
// validation error
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
destroyOnHidden
|
||||
title={title}
|
||||
width={520}
|
||||
confirmLoading={confirmLoading}
|
||||
onOk={handleOk}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
<Form form={form} layout="vertical" scrollToFirstError>
|
||||
<Form.Item
|
||||
name="paperName"
|
||||
label="试卷名称"
|
||||
rules={[{ required: true, message: "请输入试卷名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入试卷名称" maxLength={128} allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="paperTotalScore"
|
||||
label="试卷总分"
|
||||
rules={[{ required: true, message: "请输入试卷总分" }]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: "100%" }}
|
||||
min={0.1}
|
||||
max={999.9}
|
||||
placeholder="0.1-999.9"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="paperPassScore"
|
||||
label="合格分数"
|
||||
rules={[{ required: true, message: "请输入合格分数" }]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: "100%" }}
|
||||
min={0}
|
||||
max={999.9}
|
||||
placeholder="0-999.9"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
/** 导入试题新建试卷弹窗,成功后展示导入结果 */
|
||||
function ImportPaperModal({ open, onCancel, onSuccess }) {
|
||||
const [form] = Form.useForm();
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [result, setResult] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.resetFields();
|
||||
setResult(null);
|
||||
setUploading(false);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleOk = async () => {
|
||||
// 已导入成功时确定按钮即关闭
|
||||
if (result) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const formData = new FormData();
|
||||
formData.append("paperName", values.paperName);
|
||||
formData.append("paperTotalScore", values.paperTotalScore);
|
||||
formData.append("paperPassScore", values.paperPassScore);
|
||||
formData.append("examTime", values.examTime);
|
||||
formData.append("file", values.file[0].originFileObj);
|
||||
setUploading(true);
|
||||
const res = await paperImport(formData);
|
||||
if (res?.success !== false) {
|
||||
setResult(res?.data || {});
|
||||
onSuccess();
|
||||
}
|
||||
} catch {
|
||||
// validation error
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const errorColumns = [
|
||||
{ title: "行号", dataIndex: "rowIndex", width: 70 },
|
||||
{ title: "sheet页", dataIndex: "sheetName", width: 120 },
|
||||
{ title: "异常原因", dataIndex: "errorMessage", ellipsis: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
destroyOnHidden
|
||||
title="导入试题新建试卷"
|
||||
width={560}
|
||||
okText={result ? "关闭" : "开始导入"}
|
||||
confirmLoading={uploading}
|
||||
onOk={handleOk}
|
||||
onCancel={onCancel}
|
||||
>
|
||||
{result ? (
|
||||
<>
|
||||
<Alert
|
||||
type={result.errors?.length ? "warning" : "success"}
|
||||
message={`成功导入 ${result.successCount ?? 0} 条试题${
|
||||
result.errors?.length ? `,失败 ${result.errors.length} 条` : ""
|
||||
}`}
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
{!!result.errors?.length && (
|
||||
<Table
|
||||
size="small"
|
||||
rowKey={(e) => `${e.rowIndex}-${e.sheetName}`}
|
||||
columns={errorColumns}
|
||||
dataSource={result.errors}
|
||||
pagination={false}
|
||||
scroll={{ y: 240 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Form form={form} layout="vertical" scrollToFirstError>
|
||||
<Form.Item
|
||||
name="paperName"
|
||||
label="试卷名称"
|
||||
rules={[{ required: true, message: "请输入试卷名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入试卷名称" maxLength={128} allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="paperTotalScore"
|
||||
label="试卷总分"
|
||||
rules={[{ required: true, message: "请输入试卷总分" }]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: "100%" }}
|
||||
min={0.1}
|
||||
max={999.9}
|
||||
placeholder="0.1-999.9"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="paperPassScore"
|
||||
label="合格分数"
|
||||
rules={[{ required: true, message: "请输入合格分数" }]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: "100%" }}
|
||||
min={0}
|
||||
max={999.9}
|
||||
placeholder="0-999.9"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="examTime"
|
||||
label="考试时长(分钟)"
|
||||
rules={[{ required: true, message: "请输入考试时长" }]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: "100%" }}
|
||||
min={1}
|
||||
max={3600}
|
||||
placeholder="1-3600"
|
||||
addonAfter="分钟"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="file"
|
||||
label="试题Excel文件"
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={(e) => (Array.isArray(e) ? e : e?.fileList)}
|
||||
rules={[{ required: true, message: "请上传试题Excel文件" }]}
|
||||
extra="第一个sheet单选题,第二个sheet多选题,第三个sheet判断题"
|
||||
>
|
||||
<Upload accept=".xls,.xlsx" maxCount={1} beforeUpload={() => false}>
|
||||
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default Connect([NS_PAPER], true)(
|
||||
AntdTableFuncControl(PaperManagePage),
|
||||
);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import React from "react";
|
||||
|
||||
/** 试卷管理路由出口(List 列表 / Detail 试卷试题下钻页) */
|
||||
export default function PaperManage(props) {
|
||||
return props.children;
|
||||
}
|
||||
|
|
@ -299,6 +299,11 @@ const menuItems = [
|
|||
label: "培训课程管理",
|
||||
icon: <FileTextOutlined />,
|
||||
},
|
||||
{
|
||||
key: "/safetyEval/container/EduTraining/PaperManage/List",
|
||||
label: "试卷管理",
|
||||
icon: <FileTextOutlined />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue