feat: 综合大题改造(替代 Word 上传)+ MockExam 按组分支

中职/高职组以「综合大题」替代原 Word 文件上传:管理员在后台录入题干
(wangEditor 富文本,含表格),系统按组随机抽题,考生在 3 个小问内作答,
每小题 10 分、大题合计 30 分。

- api/admin.js / api/exam.js / stores/exam.js:大题题库 CRUD、抽题、作答与评分链路
- Competition.vue:Word 上传区块替换为大题作答区(3 小问 + 字数上限 + 富文本题干渲染)
- Admin.vue:大题题库管理、考生作答查看与人工评分
- MockExam.vue:按组分支——中职/高职显示综合大题,其余组保持 Word 答题;侧栏与提交提示同步
- RichEditor.vue + utils/richtext.js:题干富文本渲染前做白名单消毒(零依赖,纵深防御)

说明:big_questions / big_question_answers 建表迁移 023/024 随后端仓库提交。
This commit is contained in:
2026-09-21 11:17:59 +08:00
parent e8f0195b61
commit 1bfe007149
8 changed files with 1729 additions and 470 deletions

View File

@@ -22,15 +22,6 @@ export function getQuestions(group) {
return request.get(`/exam/${group}/questions`)
}
/**
* 获取 Word 答题模板(返回文件流,前端通过 window.open 直接下载)
* @param {string} group - 组别标识
* @returns {Promise} 文件流
*/
export function getWordTemplate(group) {
return request.get(`/exam/${group}/word-template`)
}
/**
* 提交单个答案
* @param {string} group - 组别标识
@@ -41,25 +32,41 @@ export function submitAnswers(group, answers) {
return request.post(`/exam/${group}/submit-answers`, { answers }, { silent: true })
}
// 注:原 getWordTemplate / uploadWord / deleteWord 三个封装已随「中职/高职取消 Word
// 上传题」一并移除(2026-09-21)。后端对应路由亦已对中职/高职封堵;本科/教师组的
// 文档上传走分片通道 @/api/upload,不经此处。
// ==================== 综合大题(中职/高职) ====================
/**
* 上传 Word 答题文件
* @param {string} group - 组别标识
* @param {FormData} formData - 包含 Word 文件的 FormData
* @returns {Promise}
* 获取综合大题(首次调用即抽题并落库快照,后续调用复用同一题)
*
* 服务端保证「一考生一行、一题一快照」:抽题幂等(reused=true 表示复用),
* 题干以抽题时刻的快照为准(管理员后续改题干不影响已抽考生,防纠纷)。
* 响应体不含任何分数字段(成绩发布门控)。
* @param {string} group - 组别标识(仅 zhongzhi / gaozhi)
* @returns {Promise} {
* question: { big_question_uid, title, description, sub_prompts, score_per_sub, total_score, snapshot_at },
* answers: { answer_1, answer_2, answer_3 },
* submitTime, maxChars, subCount, reused
* }
*/
export function uploadWord(group, formData) {
return request.post(`/exam/${group}/upload-word`, formData, {
timeout: 60000, // 上传超时 60 秒
})
export function getBigQuestion(group) {
// silent:本接口的失败(如 4004 该组暂无大题)由答题区自行展示 + 提供重试,
// 避免与页面其他提示重复弹窗。
return request.get(`/exam/${group}/big-question`, { silent: true })
}
/**
* 删除已上传的 Word 答题文件(中职/高职组)
* 保存综合大题三小问答案(整卷覆盖保存)
*
* 超长由前端 maxlength 与服务端双重拦截;服务端超限返回 400 且整批拒绝(不静默截断)。
* @param {string} group - 组别标识
* @returns {Promise}
* @param {{answer_1: string, answer_2: string, answer_3: string, forceSubmit?: boolean}} data
* @returns {Promise} { filled, subCount, maxChars, savedAt }
*/
export function deleteWord(group) {
return request.delete(`/exam/${group}/word`, { timeout: 60000 })
export function saveBigAnswer(group, data) {
return request.post(`/exam/${group}/big-answer`, data, { silent: true })
}
/**