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 }),
|
(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_CORP_CERTIFICATE = defineNamespace("corpCertificate");
|
||||||
export const NS_USER_CERTIFICATE = defineNamespace("userCertificate");
|
export const NS_USER_CERTIFICATE = defineNamespace("userCertificate");
|
||||||
export const NS_COURSEWARE = defineNamespace("courseware");
|
export const NS_COURSEWARE = defineNamespace("courseware");
|
||||||
|
export const NS_PAPER = defineNamespace("paper");
|
||||||
export const NS_ORG_INFO = defineNamespace("orgInfo");
|
export const NS_ORG_INFO = defineNamespace("orgInfo");
|
||||||
export const NS_ORG_QUALIFICATION_CERT = defineNamespace("orgQualificationCert");
|
export const NS_ORG_QUALIFICATION_CERT = defineNamespace("orgQualificationCert");
|
||||||
export const NS_ORG_DEPARTMENT = defineNamespace("orgDepartment");
|
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: "培训课程管理",
|
label: "培训课程管理",
|
||||||
icon: <FileTextOutlined />,
|
icon: <FileTextOutlined />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "/safetyEval/container/EduTraining/PaperManage/List",
|
||||||
|
label: "试卷管理",
|
||||||
|
icon: <FileTextOutlined />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue