diff --git a/src/api/courseware/index.js b/src/api/courseware/index.js
index 8185714..630241e 100644
--- a/src/api/courseware/index.js
+++ b/src/api/courseware/index.js
@@ -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",
diff --git a/src/api/paper/index.js b/src/api/paper/index.js
index e08827b..1c6bc7a 100644
--- a/src/api/paper/index.js
+++ b/src/api/paper/index.js
@@ -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",
diff --git a/src/enumerate/constant/index.js b/src/enumerate/constant/index.js
index 7e6dd6c..eda2728 100644
--- a/src/enumerate/constant/index.js
+++ b/src/enumerate/constant/index.js
@@ -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" },
diff --git a/src/pages/Container/EduTraining/ClassManage/Add/CourseTab.js b/src/pages/Container/EduTraining/ClassManage/Add/CourseTab.js
index f10b825..2c3fc62 100644
--- a/src/pages/Container/EduTraining/ClassManage/Add/CourseTab.js
+++ b/src/pages/Container/EduTraining/ClassManage/Add/CourseTab.js
@@ -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) {
}}
/>
-
+
+
+
+
{
+ 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 (
+ <>
+
+
+
+
+
+ setPreviewPaperId(id)}
+ onBound={loadClassPaper}
+ resetModelState={props.resetModelState}
+ onClose={() => setConfigOpen(false)}
+ />
+
+ 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: () => (
+
+
+
+ }
+ onClick={async () => {
+ try {
+ await downloadPaperPdf(paperId);
+ } catch (e) {
+ message.error(e?.message || "下载失败");
+ }
+ }}
+ >
+ 下载
+
+
+ ),
+ },
+ ];
+
+ return (
+
+ {!paperId && (
+ }
+ style={{ marginBottom: 12 }}
+ onClick={() => setAddOpen(true)}
+ >
+ 新增
+
+ )}
+
+ ),
+ }}
+ />
+
+ setAddOpen(false)}
+ onSuccess={() => {
+ setAddOpen(false);
+ onBound();
+ }}
+ />
+
+ );
+}
+
+/** 新增/编辑试卷弹窗:平台试卷列表选用 或 自动生成试卷(规则表单) */
+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) => (
+
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+ setPaperType(e.target.value)}
+ options={CLASS_PAPER_TYPE_OPTIONS}
+ optionType="button"
+ />
+
+
+ {isFixed ? (
+ <>
+ setKeyword(e.target.value)}
+ onSearch={(value) =>
+ handleSearch({
+ current: 1,
+ size: query.size,
+ paperName: value,
+ })
+ }
+ />
+ `共 ${t} 条`,
+ current: Number(query.current) || 1,
+ pageSize: Number(query.size) || 10,
+ onChange: (page, pageSize) =>
+ handleSearch({
+ current: page,
+ size: pageSize,
+ paperName: query.paperName,
+ }),
+ }}
+ />
+ >
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ value?.length >= 1
+ ? Promise.resolve()
+ : Promise.reject(
+ new Error(
+ "请至少配置一条题型规则",
+ ),
+ ),
+ },
+ ]}
+ >
+ {(fields, { add, remove }, { errors }) => (
+ <>
+ {fields.map((field) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ }
+ onClick={() =>
+ add({
+ questionType: "single",
+ num: 1,
+ score: 1,
+ })
+ }
+ >
+ 添加规则
+
+
+
+ >
+ )}
+
+
+
+ )}
+
+ );
+}
+
+/** 试卷考试信息预览弹窗:基本信息 + 试题列表(试卷详情/查看/预览共用) */
+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) => (
+ <>
+ {v || "-"}
+ {(record.questionOptionVOList || []).map((o) => (
+
+ {o.optionKey}. {o.optionText}
+
+ ))}
+ >
+ ),
+ },
+ { title: "分值", dataIndex: "score", width: 70 },
+ { title: "答案", dataIndex: "answer", width: 90 },
+ ];
+
+ return (
+
+
+
+ {basic.paperName || "-"}
+
+
+ {CLASS_PAPER_TYPE_MAP[basic.paperType] ??
+ (basic.paperType || "-")}
+
+
+ {basic.paperTotalScore ?? "-"}
+
+
+ {basic.paperPassScore ?? "-"}
+
+
+ {basic.examTime ?? "-"}
+
+
+
+
+ );
+}
+
+export default Connect([NS_COURSEWARE, NS_PAPER], true)(ClassPaperConfig);