diff --git a/src/api/courseware/index.js b/src/api/courseware/index.js index 9d48b39..20e1842 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: () => ( + + + + + + ), + }, + ]; + + return ( + + {!paperId && ( + + )} +
+ ), + }} + /> + + 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, + }), + }} + /> + + ) : ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + + + + + + )} + + + + )} + + ); +} + +/** 试卷考试信息预览弹窗:基本信息 + 试题列表(试卷详情/查看/预览共用) */ +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);