R20:Admin.vue —— 页面状态刷新修复 + 大题导出 Word + 评分一位小数

C 页面状态刷新(修复测试人员反馈的「切组 / 评分后需手动刷新」):
- onQuestionTypeChange:移除 `bigQuestions.length === 0` 守卫(切组后旧数组仍非空
  ⇒ 永不重新拉取,界面停留在上一组数据)
- saveBigScores / saveExamScore:原来各只刷新一侧列表(评分后「查看」侧仍是旧值)
- 收敛为唯一刷新入口 refreshQuestionData(),onMenuSelect / watch(questionGroup) /
  评分收尾 / 切组 全部走它
- 新增 ensureBigData():按「数据属于哪个组」(bigDataGroup) 判定是否拉取
- fetchBigQuestions / fetchBigAnswers:固定发起时的组别,响应回来若组别已变则丢弃
  (防切组瞬间旧响应串组显示)

B 大题评分一位小数:
- 3 处 el-input-number 加 :step="0.5" :precision="1"
- 新增 fmtScore():整数不带小数点(27),非整数保留一位(27.4),空/非法回 '--'
- bigScoreTotal 收敛到一位小数(防 0.1+0.2 浮点脏值显示)

D 大题导出 Word:
- 工具栏新增「导出选中Word(N)」「导出全部Word」
- 大题表加多选(reserve-selection)+ 选中态跟踪
- exportBigQuestions():走 safeDownload(已含 JSON 错误分支,避免把错误信封存成
  .docx);过滤不属于当前组别的陈旧 ID

计划外修复:
- exportBigQuestions 边界:选中 ID 全失效时原会因 qs 为空而静默退化成「导出全部」,
  与用户意图相反;已改为提示而不导出

验证:VITE_EXIT=0(多次构建,Admin chunk hash 已更新)。
未 push:须先上线验证通过再单独授权。
This commit is contained in:
2026-09-23 18:30:01 +08:00
parent cddf91d13a
commit bdfa4a9618

View File

@@ -206,12 +206,7 @@
<template v-if="activeQuestionType !== 'big'"> <template v-if="activeQuestionType !== 'big'">
<el-input v-model="questionSearch" placeholder="搜索题目内容" clearable size="small" class="search-input" @input="onQuestionSearchChange" /> <el-input v-model="questionSearch" placeholder="搜索题目内容" clearable size="small" class="search-input" @input="onQuestionSearchChange" />
<el-button type="primary" @click="openQuestionDialog(null, activeQuestionType)">添加题目</el-button> <el-button type="primary" @click="openQuestionDialog(null, activeQuestionType)">添加题目</el-button>
<el-divider direction="vertical" /> <el-tag type="info" size="small" class="score-policy-tag">客观题分值口径:每小题固定 1 分</el-tag>
<span class="batch-score-label">批量设分:</span>
<span class="batch-score-item">选择题<el-input-number v-model="batchChoiceScore" :min="1" :max="100" size="small" controls-position="right" /></span>
<el-button size="small" v-guard="() => applyTypeScore('choice')">应用</el-button>
<span class="batch-score-item">判断题<el-input-number v-model="batchJudgeScore" :min="1" :max="100" size="small" controls-position="right" /></span>
<el-button size="small" v-guard="() => applyTypeScore('judge')">应用</el-button>
<el-divider direction="vertical" /> <el-divider direction="vertical" />
<el-button size="small" v-guard="() => downloadQuestionTpl()">下载Excel模板</el-button> <el-button size="small" v-guard="() => downloadQuestionTpl()">下载Excel模板</el-button>
<el-button size="small" type="success" v-guard="() => triggerExcelImport()">Excel批量导入</el-button> <el-button size="small" type="success" v-guard="() => triggerExcelImport()">Excel批量导入</el-button>
@@ -225,6 +220,14 @@
<el-input v-model="bigSearch" placeholder="搜索大题标题/题干" clearable size="small" class="search-input" /> <el-input v-model="bigSearch" placeholder="搜索大题标题/题干" clearable size="small" class="search-input" />
<el-button type="primary" v-guard="() => openBigDialog(null)">新增大题</el-button> <el-button type="primary" v-guard="() => openBigDialog(null)">新增大题</el-button>
<el-button size="small" :loading="bigQuestionsLoading" @click="fetchBigQuestions">刷新</el-button> <el-button size="small" :loading="bigQuestionsLoading" @click="fetchBigQuestions">刷新</el-button>
<el-button
size="small"
type="warning"
:disabled="bigSelectedIds.length === 0"
:loading="bigExporting"
v-guard="() => exportBigQuestions(bigSelectedIds)"
>导出选中Word({{ bigSelectedIds.length }})</el-button>
<el-button size="small" type="success" :loading="bigExporting" v-guard="() => exportBigQuestions()">导出全部Word</el-button>
<el-tag type="info" class="total-score-tag"> <el-tag type="info" class="total-score-tag">
共 {{ bigQuestions.length }} 道大题(每道 {{ bigSubCount }} 小问 × {{ bigScorePerSub }} 分 = {{ bigScorePerSub * bigSubCount }} 分) 共 {{ bigQuestions.length }} 道大题(每道 {{ bigSubCount }} 小问 × {{ bigScorePerSub }} 分 = {{ bigScorePerSub * bigSubCount }} 分)
</el-tag> </el-tag>
@@ -305,7 +308,17 @@
</div> </div>
<!-- 综合大题题库表格 --> <!-- 综合大题题库表格 -->
<el-table v-if="activeQuestionType === 'big'" :data="pagedBigQuestions" stripe v-loading="bigQuestionsLoading" class="admin-table"> <el-table
v-if="activeQuestionType === 'big'"
ref="bigTableRef"
:data="pagedBigQuestions"
row-key="id"
stripe
v-loading="bigQuestionsLoading"
class="admin-table"
@selection-change="onBigSelectionChange"
>
<el-table-column type="selection" width="45" align="center" :reserve-selection="true" />
<el-table-column type="index" width="50" label="#" :index="(idx) => (bigPage - 1) * bigPageSize + idx + 1" align="center" /> <el-table-column type="index" width="50" label="#" :index="(idx) => (bigPage - 1) * bigPageSize + idx + 1" align="center" />
<el-table-column prop="title" label="大题标题" min-width="180" show-overflow-tooltip /> <el-table-column prop="title" label="大题标题" min-width="180" show-overflow-tooltip />
<el-table-column label="题干(富文本,含表格)" min-width="260"> <el-table-column label="题干(富文本,含表格)" min-width="260">
@@ -396,7 +409,7 @@
</el-table-column> </el-table-column>
<el-table-column label="大题得分" width="110" align="center"> <el-table-column label="大题得分" width="110" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.bigScore !== null && row.bigScore !== undefined">{{ row.bigScore }} 分</span> <span v-if="row.bigScore !== null && row.bigScore !== undefined">{{ fmtScore(row.bigScore) }} 分</span>
<span v-else style="color:#909399">未评分</span> <span v-else style="color:#909399">未评分</span>
</template> </template>
</el-table-column> </el-table-column>
@@ -432,7 +445,7 @@
<el-input v-model="questionForm.content" type="textarea" :rows="3" /> <el-input v-model="questionForm.content" type="textarea" :rows="3" />
</el-form-item> </el-form-item>
<el-form-item label="分值"> <el-form-item label="分值">
<el-input-number v-model="questionForm.score" :min="1" :max="100" /> <el-input-number v-model="questionForm.score" :min="1" :max="1" disabled />
</el-form-item> </el-form-item>
<el-form-item label="选项"> <el-form-item label="选项">
<div class="choice-options"> <div class="choice-options">
@@ -465,7 +478,7 @@
<el-input v-model="questionForm.content" type="textarea" :rows="3" /> <el-input v-model="questionForm.content" type="textarea" :rows="3" />
</el-form-item> </el-form-item>
<el-form-item label="分值"> <el-form-item label="分值">
<el-input-number v-model="questionForm.score" :min="1" :max="100" /> <el-input-number v-model="questionForm.score" :min="1" :max="1" disabled />
</el-form-item> </el-form-item>
<el-form-item label="正确答案"> <el-form-item label="正确答案">
<el-radio-group v-model="questionForm.answer"> <el-radio-group v-model="questionForm.answer">
@@ -596,6 +609,8 @@
v-model="bigScoreForm[`score_${i}`]" v-model="bigScoreForm[`score_${i}`]"
:min="0" :min="0"
:max="bigScorePerSub" :max="bigScorePerSub"
:step="0.5"
:precision="1"
size="small" size="small"
controls-position="right" controls-position="right"
/> />
@@ -605,7 +620,7 @@
<div class="big-answer-total"> <div class="big-answer-total">
本大题合计: 本大题合计:
<strong>{{ bigScoreTotal }}</strong> 分(满分 {{ bigScorePerSub * bigSubCount }} 分) <strong>{{ fmtScore(bigScoreTotal) }}</strong> 分(满分 {{ bigScorePerSub * bigSubCount }} 分)
<span class="big-answer-tip">保存后将写入该考生「作品评分」,总分 = 客观题得分 + 作品评分</span> <span class="big-answer-tip">保存后将写入该考生「作品评分」,总分 = 客观题得分 + 作品评分</span>
</div> </div>
</template> </template>
@@ -686,7 +701,7 @@
<span>{{ examScoreDialogRow.examScore ?? 0 }} 分(系统自动评分,不可修改)</span> <span>{{ examScoreDialogRow.examScore ?? 0 }} 分(系统自动评分,不可修改)</span>
</el-form-item> </el-form-item>
<el-form-item label="大题分"> <el-form-item label="大题分">
<el-input-number v-model="examScoreDialogWord" :min="0" :max="100" /> <el-input-number v-model="examScoreDialogWord" :min="0" :max="100" :step="0.5" :precision="1" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -776,7 +791,7 @@
<el-button text type="primary" size="small" v-guard="() => downloadWork(row, f)">下载</el-button> <el-button text type="primary" size="small" v-guard="() => downloadWork(row, f)">下载</el-button>
<span class="score-inline"> <span class="score-inline">
评分 评分
<el-input-number v-model="f._score" :min="0" :max="100" size="small" controls-position="right" class="score-input" /> <el-input-number v-model="f._score" :min="0" :max="100" :step="0.5" :precision="1" size="small" controls-position="right" class="score-input" />
</span> </span>
</div> </div>
<span v-if="!row.files || row.files.length === 0" style="color:#909399">未提交</span> <span v-if="!row.files || row.files.length === 0" style="color:#909399">未提交</span>
@@ -1259,13 +1274,8 @@ function onMenuSelect(index) {
if (index === 'dashboard') fetchStats() if (index === 'dashboard') fetchStats()
if (index === 'accounts') fetchAccounts() if (index === 'accounts') fetchAccounts()
if (index === 'questions' && questionGroup.value) { if (index === 'questions' && questionGroup.value) {
fetchQuestions() // 单一刷新入口:题库 + 考生答题记录 +(大题库 / 大题作答)
fetchExamSubmissions() refreshQuestionData(true)
// 综合大题页签下同步拉取题库与作答查证数据
if (activeQuestionType.value === 'big') {
fetchBigQuestions()
fetchBigAnswers()
}
} }
if (index === 'materials') fetchAdminMaterials() if (index === 'materials') fetchAdminMaterials()
if (index === 'submissions' && submissionGroup.value) fetchSubmissions() if (index === 'submissions' && submissionGroup.value) fetchSubmissions()
@@ -1468,7 +1478,9 @@ const judgePageSize = ref(10)
const questionDialogVisible = ref(false) const questionDialogVisible = ref(false)
const questionSaving = ref(false) const questionSaving = ref(false)
const editingQuestion = ref(null) const editingQuestion = ref(null)
const questionForm = reactive({ group: 'zhongzhi', content: '', score: 5, optionA: '', optionB: '', optionC: '', optionD: '', answer: '', description: '' }) /** 客观题(选择/判断)每小题固定分值 —— 与后端 service/admin.js 的 OBJECTIVE_SCORE_PER_QUESTION 同源 */
const OBJECTIVE_SCORE = 1
const questionForm = reactive({ group: 'zhongzhi', content: '', score: OBJECTIVE_SCORE, optionA: '', optionB: '', optionC: '', optionD: '', answer: '', description: '' })
const questionTypeLabel = computed(() => { const questionTypeLabel = computed(() => {
const map = { choice: '选择题', judge: '判断题', big: '综合大题' } const map = { choice: '选择题', judge: '判断题', big: '综合大题' }
@@ -1583,12 +1595,24 @@ const bigRowScorePerSub = computed(() => {
return v > 0 ? v : bigScorePerSub.value return v > 0 ? v : bigScorePerSub.value
}) })
/**
* 分数显示格式化(2026-09-23):人工评分允许小数点后一位。
* 整数不带小数点(27 而非 27.0),非整数保留一位(27.4);空/非法值回 '--'。
*/
function fmtScore(v) {
if (v === null || v === undefined || v === '') return '--'
const n = Number(v)
if (!Number.isFinite(n)) return '--'
return Number.isInteger(n) ? String(n) : String(Math.round(n * 10) / 10)
}
const bigScoreTotal = computed(() => { const bigScoreTotal = computed(() => {
let sum = 0 let sum = 0
for (let i = 1; i <= bigSubCount.value; i++) { for (let i = 1; i <= bigSubCount.value; i++) {
sum += Number(bigScoreForm[`score_${i}`]) || 0 sum += Number(bigScoreForm[`score_${i}`]) || 0
} }
return sum // 浮点求和收敛到一位小数,避免 0.1+0.2=0.30000000000000004 之类的脏值显示
return Math.round(sum * 10) / 10
}) })
/** 剥标签取纯文本(列表内搜索用,不用于渲染) */ /** 剥标签取纯文本(列表内搜索用,不用于渲染) */
@@ -1611,10 +1635,13 @@ function formatDateTime(v) {
} }
async function fetchBigQuestions() { async function fetchBigQuestions() {
// 固定「发起请求时的组别」:切组瞬间的旧响应回来时不得写回,否则会串组显示别组题目
const g = questionGroup.value
bigQuestionsLoading.value = true bigQuestionsLoading.value = true
try { try {
const data = await adminApi.getBigQuestions(questionGroup.value) const data = await adminApi.getBigQuestions(g)
const result = data?.data || data const result = data?.data || data
if (questionGroup.value !== g) return
bigQuestions.value = result?.list || [] bigQuestions.value = result?.list || []
if (Number(result?.subCount) > 0) bigSubCount.value = Number(result.subCount) if (Number(result?.subCount) > 0) bigSubCount.value = Number(result.subCount)
if (Number(result?.scorePerSub) > 0) bigScorePerSub.value = Number(result.scorePerSub) if (Number(result?.scorePerSub) > 0) bigScorePerSub.value = Number(result.scorePerSub)
@@ -1623,7 +1650,7 @@ async function fetchBigQuestions() {
if (bigPage.value > maxPage) bigPage.value = maxPage if (bigPage.value > maxPage) bigPage.value = maxPage
} catch (e) { } catch (e) {
console.warn('获取综合大题失败', e) console.warn('获取综合大题失败', e)
bigQuestions.value = [] if (questionGroup.value === g) bigQuestions.value = []
} finally { } finally {
bigQuestionsLoading.value = false bigQuestionsLoading.value = false
} }
@@ -1705,6 +1732,8 @@ async function deleteBigQuestion(row) {
await adminApi.deleteBigQuestion(questionGroup.value, row.id) await adminApi.deleteBigQuestion(questionGroup.value, row.id)
ElMessage.success('大题已删除') ElMessage.success('大题已删除')
await fetchBigQuestions() await fetchBigQuestions()
// 已删除的行可能仍被勾选(reserve-selection 会保留),导出前清掉避免请求到已删 ID
clearBigSelection()
} catch (e) { } catch (e) {
// 409:该题已被抽取,服务端拒绝删除;给出可执行的处理建议 // 409:该题已被抽取,服务端拒绝删除;给出可执行的处理建议
ElMessage.error(e?.message || '删除失败') ElMessage.error(e?.message || '删除失败')
@@ -1719,16 +1748,20 @@ function openBigPreview(row) {
} }
async function fetchBigAnswers() { async function fetchBigAnswers() {
// 同 fetchBigQuestions:固定发起时的组别,防止切组竞态把别组数据写回
const g = questionGroup.value
bigAnswersLoading.value = true bigAnswersLoading.value = true
try { try {
const data = await adminApi.getBigAnswers(questionGroup.value) const data = await adminApi.getBigAnswers(g)
const result = data?.data || data const result = data?.data || data
if (questionGroup.value !== g) return
bigAnswers.value = result?.list || [] bigAnswers.value = result?.list || []
bigAnswersTotal.value = Number(result?.total) || bigAnswers.value.length bigAnswersTotal.value = Number(result?.total) || bigAnswers.value.length
bigAnswersDrawn.value = Number(result?.drawnCount) || 0 bigAnswersDrawn.value = Number(result?.drawnCount) || 0
if (Number(result?.subCount) > 0) bigSubCount.value = Number(result.subCount) if (Number(result?.subCount) > 0) bigSubCount.value = Number(result.subCount)
} catch (e) { } catch (e) {
console.warn('获取大题作答失败', e) console.warn('获取大题作答失败', e)
if (questionGroup.value !== g) return
bigAnswers.value = [] bigAnswers.value = []
bigAnswersTotal.value = 0 bigAnswersTotal.value = 0
bigAnswersDrawn.value = 0 bigAnswersDrawn.value = 0
@@ -1737,6 +1770,95 @@ async function fetchBigAnswers() {
} }
} }
/**
* 「大题库 / 大题作答」数据当前所属组别(2026-09-23)。
*
* 修复「切中职/高职后必须刷新页面才显示新数据」:
* 原实现用 `bigQuestions.length === 0` 作为「要不要拉」的判据,但切组时
* 上一组的数据仍在数组里(长度 ≠ 0)⇒ 永远不会重新拉取。
* 改为按「数据属于哪个组」判定,而不是按「数组是否为空」。
*/
const bigDataGroup = ref('')
/** 保证大题库 + 大题作答与当前组别一致(同组且非强制时不重复请求) */
async function ensureBigData(force = false) {
const g = questionGroup.value
if (!force && bigDataGroup.value === g && bigQuestions.value.length > 0) return
await Promise.allSettled([fetchBigQuestions(), fetchBigAnswers()])
if (questionGroup.value === g) bigDataGroup.value = g
}
/**
* 刷新「题目管理」菜单下与组别相关的全部数据(2026-09-23)。
*
* 原先各入口各刷一半(切组只刷题库、评分只刷大题作答),导致另一处仍是旧值,
* 测试人员只能手动刷新整页。此处收敛为**唯一刷新入口**,避免再出现遗漏。
*/
async function refreshQuestionData(force = false) {
await Promise.allSettled([
fetchQuestions(),
fetchExamSubmissions(),
ensureBigData(force),
])
}
/** ==================== 大题导出 Word(2026-09-23) ==================== */
const bigTableRef = ref(null)
/** 当前勾选的大题 ID(跨页保留;切组时必须清空 —— ID 是组内作用域) */
const bigSelectedIds = ref([])
const bigExporting = ref(false)
function onBigSelectionChange(rows) {
bigSelectedIds.value = (rows || [])
.map(r => Number(r && r.id))
.filter(v => Number.isInteger(v) && v > 0)
}
/** 清空勾选(同时清 el-table 内部状态,否则切组后旧勾选仍显示为选中) */
function clearBigSelection() {
const t = bigTableRef.value
if (t && typeof t.clearSelection === 'function') t.clearSelection()
bigSelectedIds.value = []
}
/**
* 导出大题为 Word。
* · 不传 ids ⇒ 导出该组**全部**
* · 传数组 ⇒ 只导出数组内、且确属当前组别的题目(陈旧 ID 被过滤);
* 数组为空或全部失效 ⇒ 提示而不导出(⛔ 不可退化成「导出全部」)
* · 1 道 ⇒ 直接 .docx;多道 ⇒ .zip(内含每道题一个 .docx)
* 文件名由后端 Content-Disposition 下发(RFC5987 已由 koa 处理),此处只给兜底名。
*/
async function exportBigQuestions(ids) {
const groupLabel = questionGroup.value === 'zhongzhi' ? '中职组' : '高职组'
// 区分「导出全部」与「导出选中」:前者不传参,后者传数组。
// 若不区分,则「选中的题全被过滤掉」时 qs 为空 ⇒ 静默退化成导出全部,与用户意图相反。
const isAll = !Array.isArray(ids)
// 兜底:把「不属于当前组别列表」的残留 ID 过滤掉(删除后前端的勾选可能滞后一拍)
const available = new Set(bigQuestions.value.map(q => Number(q.id)))
const idsArr = Array.isArray(ids) ? ids : []
const list = idsArr
.map(v => Number(v))
.filter(v => Number.isInteger(v) && v > 0 && available.has(v))
if (!isAll && list.length === 0) {
ElMessage.warning('选中的题目已失效(可能已被删除),请刷新后重试')
return
}
const qs = list.length ? `?ids=${list.join(',')}` : ''
bigExporting.value = true
try {
await safeDownload(`/api/admin/big-questions/${questionGroup.value}/export${qs}`, {
fallbackName: list.length === 1
? `${groupLabel}_综合大题.docx`
: `${groupLabel}_综合大题.zip`,
onSuccess: name => ElMessage.success(`已开始下载:${name}`),
})
} finally {
bigExporting.value = false
}
}
/** 详情弹窗内第 i 小问的考生作答原文(空值统一为 '') */ /** 详情弹窗内第 i 小问的考生作答原文(空值统一为 '') */
function bigAnswerTextOf(i) { function bigAnswerTextOf(i) {
const v = bigAnswerRow.value['answer' + i] const v = bigAnswerRow.value['answer' + i]
@@ -1772,7 +1894,9 @@ async function saveBigScores() {
}) })
ElMessage.success(`评分已保存(大题合计 ${res?.total ?? bigScoreTotal.value} 分)`) ElMessage.success(`评分已保存(大题合计 ${res?.total ?? bigScoreTotal.value} 分)`)
bigAnswerDialogVisible.value = false bigAnswerDialogVisible.value = false
await fetchBigAnswers() // 一次刷新全部相关列表:大题作答(含大题得分列)+ 考生答题记录(卷面分/大题分)。
// 原实现只调 fetchBigAnswers(),切到「考生答题记录」仍是旧分数 ⇒ 测试人员只能刷整页。
await refreshQuestionData(true)
} catch (e) { } catch (e) {
ElMessage.error(e?.message || '评分保存失败') ElMessage.error(e?.message || '评分保存失败')
} finally { } finally {
@@ -1781,23 +1905,17 @@ async function saveBigScores() {
} }
function onQuestionGroupChange() { function onQuestionGroupChange() {
// 仅重置分页与勾选;数据刷新统一交给 watch(questionGroup),避免两处各刷一半造成遗漏
choicePage.value = 1 choicePage.value = 1
judgePage.value = 1 judgePage.value = 1
bigPage.value = 1 bigPage.value = 1
fetchQuestions() clearBigSelection()
if (activeQuestionType.value === 'big') {
fetchBigQuestions()
fetchBigAnswers()
}
if (activeMenu.value === 'questions') fetchExamSubmissions()
} }
function onQuestionTypeChange(name) { function onQuestionTypeChange(name) {
// 切到综合大题时按需拉取(切换 Tab 不重复请求) // 切到综合大题时按「该数据属于哪个组」判定是否拉取
if (name === 'big') { // (原按 bigQuestions.length === 0 判定:切组后旧数据仍在数组里 ⇒ 永不刷新)
if (bigQuestions.value.length === 0) fetchBigQuestions() if (name === 'big') ensureBigData()
if (bigAnswers.value.length === 0) fetchBigAnswers()
}
} }
function onQuestionSearchChange() { function onQuestionSearchChange() {
@@ -1826,7 +1944,7 @@ function openQuestionDialog(row = null, qType = activeQuestionType.value) {
if (row) { if (row) {
questionForm.group = questionGroup.value questionForm.group = questionGroup.value
questionForm.score = row.score || 5 questionForm.score = OBJECTIVE_SCORE
if (qType === 'choice') { if (qType === 'choice') {
questionForm.content = row.question || '' questionForm.content = row.question || ''
@@ -1843,7 +1961,7 @@ function openQuestionDialog(row = null, qType = activeQuestionType.value) {
} else { } else {
questionForm.group = questionGroup.value questionForm.group = questionGroup.value
questionForm.content = '' questionForm.content = ''
questionForm.score = 5 questionForm.score = OBJECTIVE_SCORE
questionForm.optionA = '' questionForm.optionA = ''
questionForm.optionB = '' questionForm.optionB = ''
questionForm.optionC = '' questionForm.optionC = ''
@@ -1922,9 +2040,8 @@ async function batchDeleteSelectedQuestions() {
} catch (e) { /* 取消 */ } } catch (e) { /* 取消 */ }
} }
// ==================== 客观题批量设分 / Excel 导入 / 模板 ==================== // ==================== 客观题 Excel 导入 / 模板 ====================
const batchChoiceScore = ref(5) // 注:原「批量设分」已移除 —— 客观题分值口径统一为 1 分,不再可设。
const batchJudgeScore = ref(5)
const excelFileInput = ref(null) const excelFileInput = ref(null)
// 客观题总分预览:当前组选择题 + 判断题分值之和 // 客观题总分预览:当前组选择题 + 判断题分值之和
@@ -1933,15 +2050,6 @@ const objectiveTotalScore = computed(() => {
return sum(choiceQuestions.value) + sum(judgeQuestions.value) return sum(choiceQuestions.value) + sum(judgeQuestions.value)
}) })
async function applyTypeScore(qType) {
const score = qType === 'choice' ? batchChoiceScore.value : batchJudgeScore.value
try {
await adminApi.setQuestions(questionGroup.value, qType, { action: 'setTypeScore', score })
ElMessage.success(`已将${qType === 'choice' ? '选择题' : '判断题'}统一设为 ${score} 分`)
fetchQuestions()
} catch (e) { ElMessage.error('批量设分失败') }
}
function triggerExcelImport() { function triggerExcelImport() {
if (excelFileInput.value) excelFileInput.value.click() if (excelFileInput.value) excelFileInput.value.click()
} }
@@ -2045,15 +2153,16 @@ async function saveExamScore() {
}) })
ElMessage.success('评分已保存') ElMessage.success('评分已保存')
examScoreDialogVisible.value = false examScoreDialogVisible.value = false
fetchExamSubmissions() // 同上:客观分 / 大题分同时影响两张表,必须一起刷新(原仅刷考生答题记录)
await refreshQuestionData(true)
} catch (e) { ElMessage.error('保存失败') } finally { examScoreSaving.value = false } } catch (e) { ElMessage.error('保存失败') } finally { examScoreSaving.value = false }
} }
// 监听问题组别切换时自动刷新答题记录 // 组别切换 → 刷新该组全部相关数据(唯一刷新入口)
// 2026-09-23 修复:原仅刷新 fetchExamSubmissions(),题库 / 大题目录 / 大题作答
// 仍停留在上一组数据,导致测试人员必须手动刷新整页才能看到新组内容。
watch(questionGroup, () => { watch(questionGroup, () => {
if (activeMenu.value === 'questions') { if (activeMenu.value === 'questions') refreshQuestionData(true)
fetchExamSubmissions()
}
}) })
// ==================== 3.5 备赛资料管理 ==================== // ==================== 3.5 备赛资料管理 ====================
@@ -2889,8 +2998,7 @@ onMounted(() => {
.search-input { width: 220px; } .search-input { width: 220px; }
.filter-select { width: 140px; } .filter-select { width: 140px; }
.batch-score-label { color: #606266; font-size: 13px; } .score-policy-tag { margin-left: 12px; }
.batch-score-item { display: inline-flex; align-items: center; gap: 4px; color: #606266; font-size: 13px; }
.avail-hint { margin-left: 8px; color: #909399; font-size: 12px; } .avail-hint { margin-left: 8px; color: #909399; font-size: 12px; }
.total-score-tag { margin-left: 4px; } .total-score-tag { margin-left: 4px; }