761 lines
28 KiB
JavaScript
761 lines
28 KiB
JavaScript
|
|
import { useEffect, useState } from "react";
|
|||
|
|
import {
|
|||
|
|
Button,
|
|||
|
|
Descriptions,
|
|||
|
|
Empty,
|
|||
|
|
Form,
|
|||
|
|
Input,
|
|||
|
|
InputNumber,
|
|||
|
|
Modal,
|
|||
|
|
Radio,
|
|||
|
|
Select,
|
|||
|
|
Space,
|
|||
|
|
Table,
|
|||
|
|
message,
|
|||
|
|
} from "antd";
|
|||
|
|
import { DownloadOutlined, PlusOutlined } from "@ant-design/icons";
|
|||
|
|
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
|
|||
|
|
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
|||
|
|
import { NS_COURSEWARE, NS_PAPER } from "~/enumerate/namespace";
|
|||
|
|
import {
|
|||
|
|
CLASS_PAPER_TYPE_MAP,
|
|||
|
|
CLASS_PAPER_TYPE_OPTIONS,
|
|||
|
|
QUESTION_TYPE_MAP,
|
|||
|
|
QUESTION_TYPE_OPTIONS,
|
|||
|
|
} from "~/enumerate/constant";
|
|||
|
|
|
|||
|
|
const API_HOST = window.process?.env?.app?.API_HOST || "";
|
|||
|
|
|
|||
|
|
/** 下载试卷 PDF(文件流,文件名取自响应头) */
|
|||
|
|
const downloadPaperPdf = async (paperId) => {
|
|||
|
|
const res = await fetch(
|
|||
|
|
`${API_HOST}/safetyEval/paper/paper/exam/download?paperId=${paperId}`,
|
|||
|
|
{ headers: { token: sessionStorage.getItem("token") || "" } },
|
|||
|
|
);
|
|||
|
|
const contentType = res.headers.get("content-type") || "";
|
|||
|
|
if (contentType.includes("application/json")) {
|
|||
|
|
const json = await res.json();
|
|||
|
|
throw new Error(json.errMessage || json.message || "下载失败");
|
|||
|
|
}
|
|||
|
|
if (!res.ok) throw new Error(`下载失败(${res.status})`);
|
|||
|
|
const blob = await res.blob();
|
|||
|
|
let fileName = "试卷.pdf";
|
|||
|
|
const disposition = res.headers.get("content-disposition") || "";
|
|||
|
|
const matched = disposition.match(/filename\*?=(?:UTF-8''|")?([^";]+)/i);
|
|||
|
|
if (matched?.[1]) {
|
|||
|
|
try {
|
|||
|
|
fileName = decodeURIComponent(matched[1].replace(/"/g, ""));
|
|||
|
|
} catch {
|
|||
|
|
fileName = matched[1].replace(/"/g, "");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const blobUrl = window.URL.createObjectURL(blob);
|
|||
|
|
const link = document.createElement("a");
|
|||
|
|
link.href = blobUrl;
|
|||
|
|
link.download = fileName;
|
|||
|
|
link.style.display = "none";
|
|||
|
|
document.body.appendChild(link);
|
|||
|
|
link.click();
|
|||
|
|
document.body.removeChild(link);
|
|||
|
|
window.URL.revokeObjectURL(blobUrl);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** 班级课程 Tab — 配置试卷 / 试卷详情入口 */
|
|||
|
|
function ClassPaperConfig(props) {
|
|||
|
|
const { classId } = props;
|
|||
|
|
/** 班级已绑定的试卷 id(来自班级详情) */
|
|||
|
|
const [paperId, setPaperId] = useState("");
|
|||
|
|
const [configOpen, setConfigOpen] = useState(false);
|
|||
|
|
/** 试卷详情/预览弹窗展示的试卷 id */
|
|||
|
|
const [previewPaperId, setPreviewPaperId] = useState("");
|
|||
|
|
|
|||
|
|
/** 查询班级详情,取绑定试卷 */
|
|||
|
|
const loadClassPaper = async () => {
|
|||
|
|
const res = await props.classDetail({ id: classId });
|
|||
|
|
if (res?.success !== false) setPaperId(res?.data?.paperId || "");
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (classId) loadClassPaper();
|
|||
|
|
}, [classId]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<>
|
|||
|
|
<Space>
|
|||
|
|
<Button onClick={() => setConfigOpen(true)}>配置试卷</Button>
|
|||
|
|
<Button
|
|||
|
|
disabled={!paperId}
|
|||
|
|
onClick={() => setPreviewPaperId(paperId)}
|
|||
|
|
>
|
|||
|
|
试卷详情
|
|||
|
|
</Button>
|
|||
|
|
</Space>
|
|||
|
|
|
|||
|
|
<PaperConfigModal
|
|||
|
|
open={configOpen}
|
|||
|
|
classId={classId}
|
|||
|
|
paperId={paperId}
|
|||
|
|
paper={props.paper}
|
|||
|
|
courseware={props.courseware}
|
|||
|
|
basicLoading={props.paper?.paperBasicLoading}
|
|||
|
|
fetchBasicInfo={props.paperBasicInfo}
|
|||
|
|
paperPage={props.paperPage}
|
|||
|
|
paperRuleGenerate={props.paperRuleGenerate}
|
|||
|
|
coursewarePage={props.coursewarePage}
|
|||
|
|
classBindPaper={props.classBindPaper}
|
|||
|
|
onPreview={(id) => setPreviewPaperId(id)}
|
|||
|
|
onBound={loadClassPaper}
|
|||
|
|
resetModelState={props.resetModelState}
|
|||
|
|
onClose={() => setConfigOpen(false)}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<PaperExamPreviewModal
|
|||
|
|
open={!!previewPaperId}
|
|||
|
|
paperId={previewPaperId}
|
|||
|
|
title="试卷详情"
|
|||
|
|
loading={props.paper?.paperExamLoading}
|
|||
|
|
fetchExamInfo={props.paperExamInfo}
|
|||
|
|
resetModelState={props.resetModelState}
|
|||
|
|
onClose={() => setPreviewPaperId("")}
|
|||
|
|
/>
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 配置试卷弹窗:已绑定时展示试卷信息(查看/编辑/下载),未绑定时提供新增 */
|
|||
|
|
function PaperConfigModal({
|
|||
|
|
open,
|
|||
|
|
classId,
|
|||
|
|
paperId,
|
|||
|
|
paper,
|
|||
|
|
courseware,
|
|||
|
|
basicLoading,
|
|||
|
|
fetchBasicInfo,
|
|||
|
|
paperPage,
|
|||
|
|
paperRuleGenerate,
|
|||
|
|
coursewarePage,
|
|||
|
|
classBindPaper,
|
|||
|
|
onPreview,
|
|||
|
|
onBound,
|
|||
|
|
resetModelState,
|
|||
|
|
onClose,
|
|||
|
|
}) {
|
|||
|
|
const [basicInfo, setBasicInfo] = useState(null);
|
|||
|
|
const [addOpen, setAddOpen] = useState(false);
|
|||
|
|
|
|||
|
|
// 打开且有绑定试卷时才查询基本信息;paperId 变化(换绑)后重新查询
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!open) {
|
|||
|
|
setAddOpen(false);
|
|||
|
|
resetModelState?.(NS_PAPER, { paperBasicLoading: false });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setBasicInfo(null);
|
|||
|
|
if (paperId) {
|
|||
|
|
fetchBasicInfo({ paperId }).then((res) => {
|
|||
|
|
if (res?.success !== false) setBasicInfo(res?.data || null);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, [open, paperId]);
|
|||
|
|
|
|||
|
|
const columns = [
|
|||
|
|
{
|
|||
|
|
title: "试卷类型",
|
|||
|
|
dataIndex: "paperType",
|
|||
|
|
width: 140,
|
|||
|
|
render: (v) => CLASS_PAPER_TYPE_MAP[v] ?? (v || "-"),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "考试时间(分钟)",
|
|||
|
|
dataIndex: "examTime",
|
|||
|
|
width: 140,
|
|||
|
|
render: (v) => (v == null ? "-" : v),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "操作",
|
|||
|
|
render: () => (
|
|||
|
|
<TableAction>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
onClick={() => onPreview(paperId)}
|
|||
|
|
>
|
|||
|
|
查看
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
onClick={() => setAddOpen(true)}
|
|||
|
|
>
|
|||
|
|
编辑
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
icon={<DownloadOutlined />}
|
|||
|
|
onClick={async () => {
|
|||
|
|
try {
|
|||
|
|
await downloadPaperPdf(paperId);
|
|||
|
|
} catch (e) {
|
|||
|
|
message.error(e?.message || "下载失败");
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
下载
|
|||
|
|
</Button>
|
|||
|
|
</TableAction>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Modal
|
|||
|
|
open={open}
|
|||
|
|
title="配置试卷"
|
|||
|
|
width={640}
|
|||
|
|
footer={null}
|
|||
|
|
destroyOnHidden
|
|||
|
|
onCancel={onClose}
|
|||
|
|
>
|
|||
|
|
{!paperId && (
|
|||
|
|
<Button
|
|||
|
|
type="primary"
|
|||
|
|
icon={<PlusOutlined />}
|
|||
|
|
style={{ marginBottom: 12 }}
|
|||
|
|
onClick={() => setAddOpen(true)}
|
|||
|
|
>
|
|||
|
|
新增
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
<Table
|
|||
|
|
rowKey="id"
|
|||
|
|
size="small"
|
|||
|
|
columns={columns}
|
|||
|
|
dataSource={paperId && basicInfo ? [basicInfo] : []}
|
|||
|
|
loading={basicLoading}
|
|||
|
|
pagination={false}
|
|||
|
|
locale={{
|
|||
|
|
emptyText: (
|
|||
|
|
<Empty
|
|||
|
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|||
|
|
description="暂未配置试卷"
|
|||
|
|
/>
|
|||
|
|
),
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<PaperAddModal
|
|||
|
|
open={addOpen}
|
|||
|
|
mode={paperId ? "edit" : "add"}
|
|||
|
|
classId={classId}
|
|||
|
|
paper={paper}
|
|||
|
|
courseware={courseware}
|
|||
|
|
paperPage={paperPage}
|
|||
|
|
paperRuleGenerate={paperRuleGenerate}
|
|||
|
|
coursewarePage={coursewarePage}
|
|||
|
|
classBindPaper={classBindPaper}
|
|||
|
|
onPreview={onPreview}
|
|||
|
|
resetModelState={resetModelState}
|
|||
|
|
onCancel={() => setAddOpen(false)}
|
|||
|
|
onSuccess={() => {
|
|||
|
|
setAddOpen(false);
|
|||
|
|
onBound();
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Modal>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 新增/编辑试卷弹窗:平台试卷列表选用 或 自动生成试卷(规则表单) */
|
|||
|
|
function PaperAddModal({
|
|||
|
|
open,
|
|||
|
|
mode,
|
|||
|
|
classId,
|
|||
|
|
paper,
|
|||
|
|
courseware,
|
|||
|
|
paperPage,
|
|||
|
|
paperRuleGenerate,
|
|||
|
|
coursewarePage,
|
|||
|
|
classBindPaper,
|
|||
|
|
onPreview,
|
|||
|
|
resetModelState,
|
|||
|
|
onCancel,
|
|||
|
|
onSuccess,
|
|||
|
|
}) {
|
|||
|
|
const [form] = Form.useForm();
|
|||
|
|
const [paperType, setPaperType] = useState("fixed");
|
|||
|
|
const [query, setQuery] = useState({ current: 1, size: 10 });
|
|||
|
|
const [keyword, setKeyword] = useState("");
|
|||
|
|
const isFixed = paperType === "fixed";
|
|||
|
|
const coursewareOptions = (courseware?.coursewareList || []).map((c) => ({
|
|||
|
|
label: c.coursewareName,
|
|||
|
|
value: c.id,
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 切换类型/打开时加载数据:平台试卷查分页,自动生成查课件下拉
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!open) {
|
|||
|
|
resetModelState?.(NS_PAPER, {
|
|||
|
|
paperLoading: false,
|
|||
|
|
paperRuleLoading: false,
|
|||
|
|
});
|
|||
|
|
resetModelState?.(NS_COURSEWARE, { classBindPaperLoading: false });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
// 规则表单挂载后才可重置,避免 useForm 未关联告警
|
|||
|
|
if (!isFixed) form.resetFields();
|
|||
|
|
setKeyword("");
|
|||
|
|
if (isFixed) {
|
|||
|
|
setQuery({ current: 1, size: 10 });
|
|||
|
|
paperPage({ current: 1, size: 10, paperType: "fixed" });
|
|||
|
|
} else {
|
|||
|
|
coursewarePage({ current: 1, size: 200 });
|
|||
|
|
}
|
|||
|
|
}, [open, paperType]);
|
|||
|
|
|
|||
|
|
const handleSearch = (params) => {
|
|||
|
|
setQuery(params);
|
|||
|
|
paperPage({ paperType: "fixed", ...params });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** 使用平台试卷:绑定到班级 */
|
|||
|
|
const handleUse = async (record) => {
|
|||
|
|
const res = await classBindPaper({
|
|||
|
|
id: classId,
|
|||
|
|
paperId: record.id,
|
|||
|
|
paperType: "fixed",
|
|||
|
|
examMinutes: record.examTime,
|
|||
|
|
});
|
|||
|
|
if (res?.success === false) return;
|
|||
|
|
message.success("绑定成功");
|
|||
|
|
onSuccess();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/** 自动生成试卷并绑定 */
|
|||
|
|
const handleRuleOk = async () => {
|
|||
|
|
try {
|
|||
|
|
const values = await form.validateFields();
|
|||
|
|
const res = await paperRuleGenerate(values);
|
|||
|
|
if (res?.success === false) return;
|
|||
|
|
const basic = res?.data || {};
|
|||
|
|
const bindRes = await classBindPaper({
|
|||
|
|
id: classId,
|
|||
|
|
paperId: basic.id,
|
|||
|
|
paperType: "config_rule",
|
|||
|
|
examMinutes: basic.examTime,
|
|||
|
|
});
|
|||
|
|
if (bindRes?.success === false) return;
|
|||
|
|
message.success("自动生成试卷并绑定成功");
|
|||
|
|
onSuccess();
|
|||
|
|
} catch {
|
|||
|
|
// 表单校验失败,由 antd 提示
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const columns = [
|
|||
|
|
{ title: "试卷名称", dataIndex: "paperName", ellipsis: true },
|
|||
|
|
{ title: "试卷分数", dataIndex: "paperTotalScore", width: 90 },
|
|||
|
|
{ title: "合格分数", dataIndex: "paperPassScore", width: 90 },
|
|||
|
|
{
|
|||
|
|
title: "考试时长(分)",
|
|||
|
|
dataIndex: "examTime",
|
|||
|
|
width: 110,
|
|||
|
|
render: (v) => (v == null ? "-" : v),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "操作",
|
|||
|
|
width: 130,
|
|||
|
|
render: (_, record) => (
|
|||
|
|
<TableAction>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
onClick={() => onPreview(record.id)}
|
|||
|
|
>
|
|||
|
|
预览
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
onClick={() => handleUse(record)}
|
|||
|
|
>
|
|||
|
|
使用
|
|||
|
|
</Button>
|
|||
|
|
</TableAction>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Modal
|
|||
|
|
open={open}
|
|||
|
|
title={mode === "edit" ? "编辑试卷" : "新增试卷"}
|
|||
|
|
width={860}
|
|||
|
|
destroyOnHidden
|
|||
|
|
okText="生成并绑定"
|
|||
|
|
confirmLoading={
|
|||
|
|
!isFixed &&
|
|||
|
|
(paper?.paperRuleLoading || courseware?.classBindPaperLoading)
|
|||
|
|
}
|
|||
|
|
footer={isFixed ? null : undefined}
|
|||
|
|
onOk={handleRuleOk}
|
|||
|
|
onCancel={onCancel}
|
|||
|
|
>
|
|||
|
|
<Form.Item label="试卷类型" style={{ marginBottom: 16 }}>
|
|||
|
|
<Radio.Group
|
|||
|
|
value={paperType}
|
|||
|
|
onChange={(e) => setPaperType(e.target.value)}
|
|||
|
|
options={CLASS_PAPER_TYPE_OPTIONS}
|
|||
|
|
optionType="button"
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
{isFixed ? (
|
|||
|
|
<>
|
|||
|
|
<Input.Search
|
|||
|
|
placeholder="请输入试卷名称"
|
|||
|
|
allowClear
|
|||
|
|
style={{ width: 280, marginBottom: 12 }}
|
|||
|
|
value={keyword}
|
|||
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|||
|
|
onSearch={(value) =>
|
|||
|
|
handleSearch({
|
|||
|
|
current: 1,
|
|||
|
|
size: query.size,
|
|||
|
|
paperName: value,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
<Table
|
|||
|
|
rowKey="id"
|
|||
|
|
size="small"
|
|||
|
|
columns={columns}
|
|||
|
|
dataSource={
|
|||
|
|
Array.isArray(paper?.paperList)
|
|||
|
|
? paper.paperList
|
|||
|
|
: []
|
|||
|
|
}
|
|||
|
|
loading={paper?.paperLoading}
|
|||
|
|
pagination={{
|
|||
|
|
total: paper?.paperTotal,
|
|||
|
|
size: "small",
|
|||
|
|
showTotal: (t) => `共 ${t} 条`,
|
|||
|
|
current: Number(query.current) || 1,
|
|||
|
|
pageSize: Number(query.size) || 10,
|
|||
|
|
onChange: (page, pageSize) =>
|
|||
|
|
handleSearch({
|
|||
|
|
current: page,
|
|||
|
|
size: pageSize,
|
|||
|
|
paperName: query.paperName,
|
|||
|
|
}),
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<Form form={form} layout="vertical" scrollToFirstError>
|
|||
|
|
<Form.Item
|
|||
|
|
name="paperName"
|
|||
|
|
label="试卷名称"
|
|||
|
|
rules={[{ required: true, message: "请输入试卷名称" }]}
|
|||
|
|
>
|
|||
|
|
<Input
|
|||
|
|
placeholder="请输入试卷名称"
|
|||
|
|
maxLength={128}
|
|||
|
|
allowClear
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
<Space style={{ display: "flex" }} align="start">
|
|||
|
|
<Form.Item
|
|||
|
|
name="paperTotalScore"
|
|||
|
|
label="试卷总分"
|
|||
|
|
rules={[{ required: true, message: "请输入试卷总分" }]}
|
|||
|
|
style={{ width: 180 }}
|
|||
|
|
>
|
|||
|
|
<InputNumber
|
|||
|
|
style={{ width: "100%" }}
|
|||
|
|
min={0.1}
|
|||
|
|
max={9999.9}
|
|||
|
|
placeholder="0.1-9999.9"
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
<Form.Item
|
|||
|
|
name="paperPassScore"
|
|||
|
|
label="合格分数"
|
|||
|
|
rules={[{ required: true, message: "请输入合格分数" }]}
|
|||
|
|
style={{ width: 180 }}
|
|||
|
|
>
|
|||
|
|
<InputNumber
|
|||
|
|
style={{ width: "100%" }}
|
|||
|
|
min={0}
|
|||
|
|
max={9999.9}
|
|||
|
|
placeholder="0-9999.9"
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
<Form.Item
|
|||
|
|
name="examTime"
|
|||
|
|
label="考试时长(分钟)"
|
|||
|
|
rules={[
|
|||
|
|
{ required: true, message: "请输入考试时长" },
|
|||
|
|
]}
|
|||
|
|
style={{ width: 180 }}
|
|||
|
|
>
|
|||
|
|
<InputNumber
|
|||
|
|
style={{ width: "100%" }}
|
|||
|
|
min={1}
|
|||
|
|
max={3600}
|
|||
|
|
precision={0}
|
|||
|
|
placeholder="1-3600"
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
</Space>
|
|||
|
|
<Form.Item
|
|||
|
|
name="coursewareManagementIdList"
|
|||
|
|
label="课程课件"
|
|||
|
|
rules={[
|
|||
|
|
{
|
|||
|
|
required: true,
|
|||
|
|
message: "请选择课程课件",
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<Select
|
|||
|
|
mode="multiple"
|
|||
|
|
placeholder="请选择课程课件(按抽题范围)"
|
|||
|
|
showSearch
|
|||
|
|
optionFilterProp="label"
|
|||
|
|
options={coursewareOptions}
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
<Form.Item
|
|||
|
|
label="题型规则(试题数量与每题分数)"
|
|||
|
|
required
|
|||
|
|
style={{ marginBottom: 0 }}
|
|||
|
|
>
|
|||
|
|
<Form.List
|
|||
|
|
name="questionTypeRuleList"
|
|||
|
|
rules={[
|
|||
|
|
{
|
|||
|
|
validator: (_, value) =>
|
|||
|
|
value?.length >= 1
|
|||
|
|
? 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,
|
|||
|
|
"questionType",
|
|||
|
|
]}
|
|||
|
|
rules={[
|
|||
|
|
{
|
|||
|
|
required: true,
|
|||
|
|
message: "请选择题型",
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<Select
|
|||
|
|
placeholder="题型"
|
|||
|
|
options={
|
|||
|
|
QUESTION_TYPE_OPTIONS
|
|||
|
|
}
|
|||
|
|
style={{ width: 110 }}
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
<Form.Item
|
|||
|
|
noStyle
|
|||
|
|
name={[field.name, "num"]}
|
|||
|
|
rules={[
|
|||
|
|
{
|
|||
|
|
required: true,
|
|||
|
|
message: "请输入试题数量",
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<InputNumber
|
|||
|
|
placeholder="试题数量"
|
|||
|
|
min={1}
|
|||
|
|
max={1024}
|
|||
|
|
precision={0}
|
|||
|
|
style={{ width: 130 }}
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
<Form.Item
|
|||
|
|
noStyle
|
|||
|
|
name={[
|
|||
|
|
field.name,
|
|||
|
|
"score",
|
|||
|
|
]}
|
|||
|
|
rules={[
|
|||
|
|
{
|
|||
|
|
required: true,
|
|||
|
|
message: "请输入每题分数",
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<InputNumber
|
|||
|
|
placeholder="每题分数"
|
|||
|
|
min={0.1}
|
|||
|
|
max={999.9}
|
|||
|
|
style={{ width: 130 }}
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
danger
|
|||
|
|
disabled={
|
|||
|
|
fields.length <= 1
|
|||
|
|
}
|
|||
|
|
onClick={() =>
|
|||
|
|
remove(field.name)
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
删除
|
|||
|
|
</Button>
|
|||
|
|
</Space>
|
|||
|
|
</Form.Item>
|
|||
|
|
))}
|
|||
|
|
<Form.Item style={{ marginBottom: 0 }}>
|
|||
|
|
<Button
|
|||
|
|
type="dashed"
|
|||
|
|
block
|
|||
|
|
icon={<PlusOutlined />}
|
|||
|
|
onClick={() =>
|
|||
|
|
add({
|
|||
|
|
questionType: "single",
|
|||
|
|
num: 1,
|
|||
|
|
score: 1,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
添加规则
|
|||
|
|
</Button>
|
|||
|
|
<Form.ErrorList errors={errors} />
|
|||
|
|
</Form.Item>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</Form.List>
|
|||
|
|
</Form.Item>
|
|||
|
|
</Form>
|
|||
|
|
)}
|
|||
|
|
</Modal>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 试卷考试信息预览弹窗:基本信息 + 试题列表(试卷详情/查看/预览共用) */
|
|||
|
|
function PaperExamPreviewModal({
|
|||
|
|
open,
|
|||
|
|
paperId,
|
|||
|
|
title,
|
|||
|
|
loading,
|
|||
|
|
fetchExamInfo,
|
|||
|
|
resetModelState,
|
|||
|
|
onClose,
|
|||
|
|
}) {
|
|||
|
|
const [info, setInfo] = useState(null);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!open) {
|
|||
|
|
resetModelState?.(NS_PAPER, { paperExamLoading: false });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!paperId) return;
|
|||
|
|
setInfo(null);
|
|||
|
|
fetchExamInfo({ paperId }).then((res) => {
|
|||
|
|
if (res?.success !== false) setInfo(res?.data || null);
|
|||
|
|
});
|
|||
|
|
}, [open, paperId]);
|
|||
|
|
|
|||
|
|
const basic = info?.paperBasicInfoVO || {};
|
|||
|
|
|
|||
|
|
const columns = [
|
|||
|
|
{
|
|||
|
|
title: "序号",
|
|||
|
|
width: 60,
|
|||
|
|
render: (_, __, index) => index + 1,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "题型",
|
|||
|
|
dataIndex: "questionType",
|
|||
|
|
width: 70,
|
|||
|
|
render: (v) => QUESTION_TYPE_MAP[v] ?? (v || "-"),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "题干",
|
|||
|
|
dataIndex: "title",
|
|||
|
|
render: (v, record) => (
|
|||
|
|
<>
|
|||
|
|
<div>{v || "-"}</div>
|
|||
|
|
{(record.questionOptionVOList || []).map((o) => (
|
|||
|
|
<div key={o.id}>
|
|||
|
|
{o.optionKey}. {o.optionText}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{ title: "分值", dataIndex: "score", width: 70 },
|
|||
|
|
{ title: "答案", dataIndex: "answer", width: 90 },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Modal
|
|||
|
|
open={open}
|
|||
|
|
title={title}
|
|||
|
|
width={760}
|
|||
|
|
footer={null}
|
|||
|
|
destroyOnHidden
|
|||
|
|
loading={loading}
|
|||
|
|
onCancel={onClose}
|
|||
|
|
>
|
|||
|
|
<Descriptions
|
|||
|
|
size="small"
|
|||
|
|
bordered
|
|||
|
|
column={2}
|
|||
|
|
style={{ marginBottom: 16 }}
|
|||
|
|
>
|
|||
|
|
<Descriptions.Item label="试卷名称">
|
|||
|
|
{basic.paperName || "-"}
|
|||
|
|
</Descriptions.Item>
|
|||
|
|
<Descriptions.Item label="试卷类型">
|
|||
|
|
{CLASS_PAPER_TYPE_MAP[basic.paperType] ??
|
|||
|
|
(basic.paperType || "-")}
|
|||
|
|
</Descriptions.Item>
|
|||
|
|
<Descriptions.Item label="试卷总分">
|
|||
|
|
{basic.paperTotalScore ?? "-"}
|
|||
|
|
</Descriptions.Item>
|
|||
|
|
<Descriptions.Item label="合格分数">
|
|||
|
|
{basic.paperPassScore ?? "-"}
|
|||
|
|
</Descriptions.Item>
|
|||
|
|
<Descriptions.Item label="考试时长(分钟)">
|
|||
|
|
{basic.examTime ?? "-"}
|
|||
|
|
</Descriptions.Item>
|
|||
|
|
</Descriptions>
|
|||
|
|
<Table
|
|||
|
|
rowKey="id"
|
|||
|
|
size="small"
|
|||
|
|
columns={columns}
|
|||
|
|
dataSource={Array.isArray(info?.questionVOList) ? info.questionVOList : []}
|
|||
|
|
pagination={false}
|
|||
|
|
scroll={{ y: 360 }}
|
|||
|
|
/>
|
|||
|
|
</Modal>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default Connect([NS_COURSEWARE, NS_PAPER], true)(ClassPaperConfig);
|