paper
parent
03332b3d05
commit
293a8050cd
|
|
@ -89,6 +89,12 @@ export const classRemove = declareRequest(
|
|||
"Post > @/safetyEval/classManagement/delete",
|
||||
);
|
||||
|
||||
/** 绑定试卷(请求体:{ id, paperId, paperType, examMinutes }) */
|
||||
export const classBindPaper = declareRequest(
|
||||
"classBindPaperLoading",
|
||||
"Post > @/safetyEval/classManagement/bindPaper",
|
||||
);
|
||||
|
||||
/** 分页查询班级学员 */
|
||||
export const classStudentPage = declareRequest(
|
||||
"classStudentLoading",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,24 @@ export const paperPage = declareRequest(
|
|||
"paperList: [] | res.data || [] & paperTotal: 0 | res.total || 0",
|
||||
);
|
||||
|
||||
/** 试卷基本信息查询(不含试题列表) */
|
||||
export const paperBasicInfo = declareRequest(
|
||||
"paperBasicLoading",
|
||||
"Get > /safetyEval/paper/paper/basic/info",
|
||||
);
|
||||
|
||||
/** 试卷考试信息查询(基本信息 + 试题列表) */
|
||||
export const paperExamInfo = declareRequest(
|
||||
"paperExamLoading",
|
||||
"Get > /safetyEval/paper/paper/exam/info",
|
||||
);
|
||||
|
||||
/** 自动生成试卷(按课件与题型规则抽题组卷) */
|
||||
export const paperRuleGenerate = declareRequest(
|
||||
"paperRuleLoading",
|
||||
"Post > @/safetyEval/paper/courseware/question/rule",
|
||||
);
|
||||
|
||||
/** 修改试卷基本信息 */
|
||||
export const paperUpdateBasic = declareRequest(
|
||||
"paperUpdateLoading",
|
||||
|
|
|
|||
|
|
@ -738,6 +738,17 @@ export const PAPER_TYPE_MAP = PAPER_TYPE_OPTIONS.reduce(
|
|||
{},
|
||||
);
|
||||
|
||||
/** 班级配置试卷类型(fixed平台试卷/config_rule自动生成试卷) */
|
||||
export const CLASS_PAPER_TYPE_OPTIONS = [
|
||||
{ label: "平台试卷", value: "fixed" },
|
||||
{ label: "自动生成试卷", value: "config_rule" },
|
||||
];
|
||||
|
||||
export const CLASS_PAPER_TYPE_MAP = CLASS_PAPER_TYPE_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.label }),
|
||||
{},
|
||||
);
|
||||
|
||||
/** 题型(single单选/multiple多选/judge判断) */
|
||||
export const QUESTION_TYPE_OPTIONS = [
|
||||
{ label: "单选", value: "single" },
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Badge, Button, Form, Input, Modal, Table, message } from "antd";
|
||||
import { Badge, Button, Form, Input, Modal, Space, Table, message } from "antd";
|
||||
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
|
||||
import SearchForm from "~/components/SearchForm";
|
||||
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
|
||||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||
import { NS_COURSEWARE } from "~/enumerate/namespace";
|
||||
import { COURSEWARE_STATUS_MAP } from "~/enumerate/constant";
|
||||
import ClassPaperConfig from "./PaperConfig";
|
||||
|
||||
/** 视频累计时长(小时)→ “X分钟Y秒” */
|
||||
const formatVideoDuration = (hours) => {
|
||||
|
|
@ -144,13 +145,12 @@ function ClassCourseTab(props) {
|
|||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ marginBottom: 12 }}
|
||||
onClick={() => setSelectOpen(true)}
|
||||
>
|
||||
选择课程
|
||||
</Button>
|
||||
<Space style={{ marginBottom: 12 }}>
|
||||
<Button type="primary" onClick={() => setSelectOpen(true)}>
|
||||
选择课程
|
||||
</Button>
|
||||
<ClassPaperConfig classId={classId} />
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,760 @@
|
|||
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);
|
||||
Loading…
Reference in New Issue