feat(admin): 后台新增「成绩复核」——检索考生 + 全卷逐题明细弹窗(R26)

- api/admin.js: 新增 reviewSearch(姓名/身份证/电话任一,可选限定组别)与 getContestantScore(group, id)
- Admin.vue: 菜单新增「成绩复核」(Search 图标);检索区(关键词 + 可选组别下拉)+ 结果列表 + 全卷明细弹窗
- 明细含客观题逐题:题号 / 题型 / 题干 / 选项 / 考生答案 / 正确答案 / 判定 / 得分/满分
- 异常标记:题干缺失(questionMissing)、答案与判定矛盾(answerMatches≠isCorrect)
- 判断题答案 'true'/'false' 显示为「正确/错误」;未作答显示「未作答」且不判错
- 综合大题展示三小问作答与得分;本科/教师组展示作品文件清单并支持下载(不回传 file_path)
- 弹窗用考生自身组别请求接口,避免前端传错组照错人(后端 409 兜底)
- 回传正确答案仅管理员可见,与后端 adminAuth 门禁配套

构建验证:vite build 通过(✓ built in 14.80s,退出码 0)
This commit is contained in:
2026-09-28 10:15:50 +08:00
parent bdfa4a9618
commit 37d2641e49
2 changed files with 516 additions and 1 deletions

View File

@@ -435,4 +435,39 @@ export function setBigAnswerScores(group, uid, scores) {
return request.put(`/admin/big-answers/${group}/${uid}/scores`, scores)
}
// ==================== 成绩复核 ====================
// 场景:考生对成绩有异议 → 按「姓名 / 身份证号 / 电话」任一检索 → 核对全卷题目与作答。
/**
* 检索考生(姓名 / 身份证号 / 电话 任一,模糊匹配)
*
* 不传 group 则全组别检索 —— 复核时通常只拿到一个身份证号,不应强迫先猜组别。
* @param {string} keyword - 检索词
* @param {string} [group] - 可选:限定组别
* @returns {Promise} { list: [{ contestantId, name, idNumber, phone, group, objectiveApplicable }], total, truncated }
*/
export function reviewSearch(keyword, group) {
const params = { keyword }
if (group) params.group = group
return request.get('/admin/review/search', { params })
}
/**
* 考生全卷明细(复核)
*
* 返回客观题逐题(题干 / 选项 / 考生答案 / **正确答案** / 对错 / 得分 / 满分)
* + 综合大题(题干快照 + 三小问作答与得分)+ 本科/教师组作品文件清单。
* ⚠️ 含正确答案,仅管理员可用。
* @param {string} group - 组别标识(须与考生实际组别一致)
* @param {number|string} contestantId - 考生 ID(contestants.id)
* @returns {Promise} { name, idNumber, phone, group, objectiveApplicable, scorePublished,
* objective: [{ seq, questionType, questionUid, question, options, correctAnswer, maxScore,
* answer, answered, isCorrect, score, answerMatches, questionMissing }],
* summary: { answered, total, correctCount, autoScore, wordScore, totalScore },
* bigQuestion, works, warnings, matchedKeys, questionOrderFixed }
*/
export function getContestantScore(group, contestantId) {
return request.get(`/admin/review/score/${group}/${contestantId}`)
}