tangjie 2026-08-21 11:17:30 +08:00
commit ca38ee44ae
8 changed files with 1185 additions and 13093 deletions

File diff suppressed because it is too large Load Diff

82
src/api/paper/index.js Normal file
View File

@ -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 || ""',
);
/**
* 新建试卷-导入试题multipartdeclareRequest 仅支持 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",
);

View File

@ -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 }),
{},
);

View File

@ -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");

View File

@ -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),
);

View File

@ -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),
);

View File

@ -0,0 +1,6 @@
import React from "react";
/** 试卷管理路由出口List 列表 / Detail 试卷试题下钻页) */
export default function PaperManage(props) {
return props.children;
}

View File

@ -299,6 +299,11 @@ const menuItems = [
label: "培训课程管理",
icon: <FileTextOutlined />,
},
{
key: "/safetyEval/container/EduTraining/PaperManage/List",
label: "试卷管理",
icon: <FileTextOutlined />,
},
],
},
{