refactor(admin): 评分入口收敛 + 大题小问得分可见(R30)

- 文案:「考生大题作答查证」提示由「作品评分」改为「大题评分」
  (保存后将写入该考生「大题评分」,总分 = 客观题得分 + 大题评分)
- 删除「考生答题记录」的评分操作:操作列(评分按钮)+ 评分弹窗 +
  examScoreDialog* / openExamScoreDialog / saveExamScore + .exam-score-form 样式
  ⇒ 大题评分统一由「考生大题作答查证」按小问给分,word_score 不再有两条口径
  ⛔ 刻意保留:adminApi.setScores(作品管理 batchSaveWorkScores 仍在用)
     与后端 PUT /api/admin/scores/:group(10+ 门禁脚本依赖:verify_full_e2e /
     exam_regression_test / verify_large_scale_17 / verify_agg_key_mismatch 等)
- 新增「小问得分」列(「大题得分」之后):后端 listAnswersForAdmin 本就返回
  score1/2/3 ⇒ 纯前端改动、未动后端。新增 subScoresOf / hasSubScores / fmtSubScores;
  hasSubScores 用于区分「未评分」与「三问都给 0 分」(二者不可都显示成 0)

验证:
- TDZ 顺序扫描:src 38 文件 + dist 打包产物 26 文件 全 PASS
- 真 chromium(playwright)ui-runtime-check.mjs:16/16 断言全过 / 致命错误 0 / 告警 0
  (含 expectText "10 / 8 / 9" 直证小问得分列渲染;DOM dump 证实答题记录表头
   已无「操作」列、大题查证表头含「小问得分」)
- dist 重建 built in 13.00s / VITE_EXIT=0;产物「作品评分」= 0,「小问得分」「大题评分」在
This commit is contained in:
2026-09-28 17:44:55 +08:00
parent e5579f034e
commit 2ca82d0da3

View File

@@ -828,11 +828,6 @@
<span style="margin-left:10px;color:#1a3a5c;font-weight:600">总分:{{ row.totalScore ?? 0 }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template #default="{ row }">
<el-button text type="warning" size="small" @click="openExamScoreDialog(row)">评分</el-button>
</template>
</el-table-column>
<el-table-column prop="submitTime" label="提交时间" width="160" align="center">
<template #default="{ row }">{{ row.submitTime ? fmtDate(row.submitTime) : '--' }}</template>
</el-table-column>
@@ -848,29 +843,12 @@
/>
</div>
<!-- 考生大题评分弹窗(客观分只读,大题得分可改) -->
<!-- R29:标签改「左对齐 + 定宽 120px」,修复「客观分(卷面)」被折行成
「客观分(卷」/「面)」造成的文字错位(测试人员反馈)。 -->
<el-dialog v-model="examScoreDialogVisible" title="评分" width="520px">
<el-form
v-if="examScoreDialogRow"
class="exam-score-form"
label-width="120px"
label-position="left"
>
<el-form-item label="选手">{{ examScoreDialogRow.userName }}</el-form-item>
<el-form-item label="客观分(卷面)">
<span class="exam-score-readonly">{{ examScoreDialogRow.examScore ?? 0 }} 分(系统自动评分,不可修改)</span>
</el-form-item>
<el-form-item label="大题分">
<el-input-number v-model="examScoreDialogWord" :min="0" :max="100" :step="0.25" :precision="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="examScoreDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="examScoreSaving" v-guard="() => saveExamScore()">保存评分</el-button>
</template>
</el-dialog>
<!-- R30:原「考生答题记录 · 评分」弹窗已删除。
原因:大题评分统一在下方「考生大题作答查证」按小问给分
(PUT /api/admin/big-answers/:group/:uid/scores),
避免同一份 word_score 存在两条入口、出现口径不一致。
⛔ 后端 PUT /api/admin/scores/:group 与 adminApi.setScores 均**保留**:
前者被 10+ 个门禁脚本依赖,后者作品管理页(本科/教师)仍在使用。 -->
<!-- ========== 考生大题作答查证 ========== -->
<el-divider />
@@ -943,6 +921,14 @@
<span v-else style="color:#909399">未评分</span>
</template>
</el-table-column>
<!-- R30:三小问得分逐问展示(数据源 listAnswersForAdmin 的 score1/2/3,
故本列不需要任何后端改动)。按 bigSubCount 顺序以「/」分隔。 -->
<el-table-column label="小问得分" width="150" align="center">
<template #default="{ row }">
<span v-if="hasSubScores(row)" class="big-sub-scores">{{ fmtSubScores(row) }}</span>
<span v-else style="color:#909399">未评分</span>
</template>
</el-table-column>
<el-table-column label="提交时间" width="170">
<template #default="{ row }">
<span v-if="row.submitTime">{{ formatDateTime(row.submitTime) }}</span>
@@ -1031,7 +1017,7 @@
<div class="big-answer-total">
本大题合计:
<strong>{{ fmtScore(bigScoreTotal) }}</strong> 分(满分 {{ bigScorePerSub * bigSubCount }} 分)
<span class="big-answer-tip">保存后将写入该考生「作品评分」,总分 = 客观题得分 + 作品评分</span>
<span class="big-answer-tip">保存后将写入该考生「大题评分」,总分 = 客观题得分 + 大题评分</span>
</div>
</template>
<el-alert v-else type="info" :closable="false" show-icon title="该考生尚未抽取综合大题(可能未进入考试)" />
@@ -1921,6 +1907,31 @@ function fmtScore(v) {
return Number.isInteger(n) ? String(n) : String(Math.round(n * 100) / 100)
}
/**
* R30:该考生各小问得分数组(按 bigSubCount 顺序)。
*
* 数据源 = 后端 listAnswersForAdmin 返回的 score1 / score2 / score3(未评分时为 null)。
* ⇒ 仅是「把已有数据显示出来」,**不需要任何后端改动**。
*/
function subScoresOf(row) {
const out = []
for (let i = 1; i <= bigSubCount.value; i++) out.push(row ? row[`score${i}`] : null)
return out
}
/**
* 是否已有任一小问得分。
* 用于区分「尚未评分」(false) 与「三小问都给 0 分」(true) —— 二者不能都显示成 0 分。
*/
function hasSubScores(row) {
return subScoresOf(row).some(v => v !== null && v !== undefined && v !== '')
}
/** 「8 / 9.5 / 9.5」形式的小问得分展示(某一小问未给分则该项显示 '--') */
function fmtSubScores(row) {
return subScoresOf(row).map(v => fmtScore(v)).join(' / ')
}
const bigScoreTotal = computed(() => {
let sum = 0
for (let i = 1; i <= bigSubCount.value; i++) {
@@ -2517,35 +2528,12 @@ function onExamSubsFilterChange() {
examSubmissionsPage.value = 1
}
// 考生答题评分弹窗
const examScoreDialogVisible = ref(false)
const examScoreDialogRow = ref(null)
const examScoreDialogWord = ref(0)
const examScoreSaving = ref(false)
function openExamScoreDialog(row) {
examScoreDialogRow.value = row
examScoreDialogWord.value = row.wordScore ?? 0
examScoreDialogVisible.value = true
}
async function saveExamScore() {
if (!examScoreDialogRow.value) return
examScoreSaving.value = true
try {
// R29:组别跟随「成绩管理」页
await adminApi.setScores(scoreExportGroup.value, {
scores: [{
contestantId: examScoreDialogRow.value.contestantId,
// 客观分回传弹窗所见值,避免后端按答题记录重算导致与所见不一致
examScore: Number(examScoreDialogRow.value.examScore ?? 0),
wordScore: Number(examScoreDialogWord.value),
}],
})
ElMessage.success('评分已保存')
examScoreDialogVisible.value = false
// 客观分 / 大题分同时影响成绩明细表与考生答题记录,必须一起刷新
await refreshScorePageData(true)
} catch (e) { ElMessage.error('保存失败') } finally { examScoreSaving.value = false }
}
// R30:原「考生答题评分弹窗」的 state 与 openExamScoreDialog / saveExamScore 已整体移除。
// 大题评分改由「考生大题作答查证」按小问给分(adminApi.setBigAnswerScores
// → service.bigQuestion.setSubScores → service.admin.updateScores),
// 使 word_score 的**写入入口收敛为唯一一条**,不再出现两个口径。
// ⚠️ adminApi.setScores 本身**保留**:作品管理页 batchSaveWorkScores(本科/教师)仍在使用;
// 后端 PUT /api/admin/scores/:group 亦须保留(10+ 个门禁脚本依赖)。
// 组别切换 → 刷新该组全部相关数据(唯一刷新入口)
// 2026-09-23 修复:原仅刷新 fetchExamSubmissions(),题库 / 大题目录 / 大题作答
@@ -3550,14 +3538,7 @@ onMounted(() => {
align-items: center;
}
/* R29:评分弹窗标签左对齐且禁止折行(「客观分(卷面)」曾折行成两行导致错位) */
.exam-score-form .el-form-item__label {
text-align: left;
white-space: nowrap;
}
.exam-score-form .exam-score-readonly {
color: #606266;
}
/* R30:`.exam-score-form` 相关样式已随「考生答题记录 · 评分」弹窗一并删除 */
.search-input { width: 220px; }
.filter-select { width: 140px; }
@@ -4119,6 +4100,13 @@ onMounted(() => {
color: #909399;
}
/* R30:「考生大题作答查证」列表的「小问得分」列(如 8 / 9.5 / 9.5)。
使用等宽数字,避免切组/刷新时列宽抖动。 */
.big-sub-scores {
font-variant-numeric: tabular-nums;
color: #303133;
}
/* ==================== 评分输入 ==================== */
.score-input {
width: 80px;