diff --git a/src/views/Admin.vue b/src/views/Admin.vue index d9fb3e1..4f56fa8 100644 --- a/src/views/Admin.vue +++ b/src/views/Admin.vue @@ -212,7 +212,7 @@ 添加题目 客观题分值口径:每小题固定 1 分 - 下载Excel模板 + 下载Excel模板 Excel批量导入 批量删除({{ selectedQuestionRows.length }}) @@ -223,7 +223,7 @@ @@ -816,7 +567,12 @@ - 批量下载作品 + + 批量下载作品 @@ -946,8 +702,15 @@ {{ scoresPublishedMap[scoreExportGroup] ? '撤回成绩' : '发布成绩' }} - 导出CSV - 批量下载作品 + + 导出Excel + + 批量下载作品 @@ -1028,6 +791,264 @@ {{ scoresPublishedMap[scoreExportGroup] ? '成绩已发布' : '成绩未发布' }} + + + @@ -1302,7 +1323,7 @@ @change="onNewsFilterChange" /> 添加新闻 - 保存排序 + 保存排序
轮播管理
添加轮播 - 保存排序 + 保存排序 拖拽卡片可调整排序 @@ -1563,13 +1584,18 @@ function onMenuSelect(index) { if (index === 'dashboard') fetchStats() if (index === 'accounts') fetchAccounts() if (index === 'questions' && questionGroup.value) { - // 单一刷新入口:题库 + 考生答题记录 +(大题库 / 大题作答) + // R29:试卷管理只刷题库(含大题库);「考生答题记录 / 大题作答查证」已迁到成绩管理 refreshQuestionData(true) } if (index === 'materials') fetchAdminMaterials() if (index === 'submissions' && submissionGroup.value) fetchSubmissions() if (index === 'times') fetchTimes() - if (index === 'scores') { fetchPublishStatus(); fetchScoreList() } + if (index === 'scores') { + fetchPublishStatus() + fetchScoreList() + // R29:成绩管理还承载两个由试卷管理迁入的板块(仅中职/高职) + refreshExamCtxData(true) + } if (index === 'news') fetchAdminNews() if (index === 'carousel') fetchCarousels() } @@ -2056,7 +2082,8 @@ function onBigAnswerSelectionChange(rows) { * ⚠️ 与同页「导出选中/全部 Word」(exportBigQuestions,导出的是**题库题目**)是两个功能,勿混。 */ async function exportBigAnswers(ids) { - const g = questionGroup.value + // R29:本板块已迁入「成绩管理」⇒ 组别跟随成绩页 + const g = scoreExportGroup.value const groupLabel = g === 'zhongzhi' ? '中职组' : '高职组' const isAll = !Array.isArray(ids) // 兜底:剔除非本组 / 未抽题的残留 ID(与 exportBigQuestions 同源处理)。 @@ -2086,20 +2113,21 @@ async function exportBigAnswers(ids) { } async function fetchBigAnswers() { - // 同 fetchBigQuestions:固定发起时的组别,防止切组竞态把别组数据写回 - const g = questionGroup.value + // R29:本板块已由「试卷管理」迁到「成绩管理」⇒ 组别跟随成绩页的 scoreExportGroup(中职/高职)。 + // 仍固定发起时的组别,防止切组竞态把别组数据写回。 + const g = scoreExportGroup.value bigAnswersLoading.value = true try { const data = await adminApi.getBigAnswers(g) const result = data?.data || data - if (questionGroup.value !== g) return + if (scoreExportGroup.value !== g) return bigAnswers.value = result?.list || [] bigAnswersTotal.value = Number(result?.total) || bigAnswers.value.length bigAnswersDrawn.value = Number(result?.drawnCount) || 0 if (Number(result?.subCount) > 0) bigSubCount.value = Number(result.subCount) } catch (e) { console.warn('获取大题作答失败', e) - if (questionGroup.value !== g) return + if (scoreExportGroup.value !== g) return bigAnswers.value = [] bigAnswersTotal.value = 0 bigAnswersDrawn.value = 0 @@ -2118,26 +2146,46 @@ async function fetchBigAnswers() { */ 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()]) + await fetchBigQuestions() if (questionGroup.value === g) bigDataGroup.value = g } /** - * 刷新「题目管理」菜单下与组别相关的全部数据(2026-09-23)。 + * 刷新「试卷管理」菜单下的数据(R29 收敛)。 * - * 原先各入口各刷一半(切组只刷题库、评分只刷大题作答),导致另一处仍是旧值, - * 测试人员只能手动刷新整页。此处收敛为**唯一刷新入口**,避免再出现遗漏。 + * ⚠️ R29 起「考生答题记录 / 大题作答查证」已迁到「成绩管理」,故此处**只刷题库** + * (含大题库);那两块改由 refreshExamCtxData 负责 —— 原来一个入口刷三块, + * 迁走后若仍在这里刷,就会用**试卷页的组别**去拉**成绩页的板块**,必然串组。 */ async function refreshQuestionData(force = false) { - await Promise.allSettled([ - fetchQuestions(), - fetchExamSubmissions(), - ensureBigData(force), - ]) + await Promise.allSettled([fetchQuestions(), ensureBigData(force)]) +} + +/** 「考生答题记录 / 大题作答查证」数据当前所属组别(用于避免重复拉取) */ +const examCtxDataGroup = ref('') + +/** + * 刷新成绩管理页内两个由试卷管理迁入的板块(R29)。 + * 仅中职/高职有客观题与综合大题;本科/教师是 Word/PPT 作品,无此二块 ⇒ 直接跳过。 + */ +async function refreshExamCtxData(force = false) { + const g = scoreExportGroup.value + if (g !== 'zhongzhi' && g !== 'gaozhi') return + if (!force && examCtxDataGroup.value === g) return + await Promise.allSettled([fetchExamSubmissions(), fetchBigAnswers()]) + if (scoreExportGroup.value === g) examCtxDataGroup.value = g +} + +/** + * 刷新「成绩管理」页全部数据(R29 收敛):成绩明细 + 两个迁入板块。 + * 评分保存后必须走这里,否则明细表与答题记录会出现「一处已更新、一处还是旧分」。 + */ +async function refreshScorePageData(force = false) { + await Promise.allSettled([fetchScoreList(), refreshExamCtxData(force)]) } /** ==================== 大题导出 Word(2026-09-23) ==================== */ @@ -2225,16 +2273,17 @@ async function saveBigScores() { } bigScoreSaving.value = true try { - const res = await adminApi.setBigAnswerScores(questionGroup.value, row.aggregationUid, { + // R29:组别跟随「成绩管理」页(本弹窗已随板块迁入成绩管理) + const res = await adminApi.setBigAnswerScores(scoreExportGroup.value, row.aggregationUid, { score_1: Number(bigScoreForm.score_1) || 0, score_2: Number(bigScoreForm.score_2) || 0, score_3: Number(bigScoreForm.score_3) || 0, }) ElMessage.success(`评分已保存(大题合计 ${res?.total ?? bigScoreTotal.value} 分)`) bigAnswerDialogVisible.value = false - // 一次刷新全部相关列表:大题作答(含大题得分列)+ 考生答题记录(卷面分/大题分)。 - // 原实现只调 fetchBigAnswers(),切到「考生答题记录」仍是旧分数 ⇒ 测试人员只能刷整页。 - await refreshQuestionData(true) + // 一次刷新全部相关列表:成绩明细(总分 = 客观分 + 大题分)+ 大题作答 + 考生答题记录。 + // 原实现只调 fetchBigAnswers(),测试人员看到另一处仍是旧分只能刷整页。 + await refreshScorePageData(true) } catch (e) { ElMessage.error(e?.message || '评分保存失败') } finally { @@ -2453,9 +2502,10 @@ const pagedExamSubmissions = computed(() => { }) async function fetchExamSubmissions() { + // R29:本板块已迁入「成绩管理」⇒ 组别跟随成绩页 examSubmissionsLoading.value = true try { - const data = await adminApi.getSubmissions(questionGroup.value) + const data = await adminApi.getSubmissions(scoreExportGroup.value) examSubmissions.value = Array.isArray(data) ? data : (data?.list || []) } catch (e) { console.warn('获取答题记录失败', e) @@ -2481,7 +2531,8 @@ async function saveExamScore() { if (!examScoreDialogRow.value) return examScoreSaving.value = true try { - await adminApi.setScores(questionGroup.value, { + // R29:组别跟随「成绩管理」页 + await adminApi.setScores(scoreExportGroup.value, { scores: [{ contestantId: examScoreDialogRow.value.contestantId, // 客观分回传弹窗所见值,避免后端按答题记录重算导致与所见不一致 @@ -2491,8 +2542,8 @@ async function saveExamScore() { }) ElMessage.success('评分已保存') examScoreDialogVisible.value = false - // 同上:客观分 / 大题分同时影响两张表,必须一起刷新(原仅刷考生答题记录) - await refreshQuestionData(true) + // 客观分 / 大题分同时影响成绩明细表与考生答题记录,必须一起刷新 + await refreshScorePageData(true) } catch (e) { ElMessage.error('保存失败') } finally { examScoreSaving.value = false } } @@ -2503,6 +2554,12 @@ watch(questionGroup, () => { if (activeMenu.value === 'questions') refreshQuestionData(true) }) +// R29:成绩管理页切组 → 刷新成绩明细 + 两个由试卷管理迁入的板块 +// (这两个板块的组别跟随 scoreExportGroup,故其刷新必须挂在这里) +watch(scoreExportGroup, () => { + if (activeMenu.value === 'scores') refreshExamCtxData(true) +}) + // ==================== 3.5 备赛资料管理 ==================== const materialsGroup = ref('benke') const adminMaterials = ref([]) @@ -2691,7 +2748,8 @@ async function batchSaveWorkScores() { ElMessage.success('评分保存成功') // 保存后立即刷新作品列表 + 考生答题记录,保证两处分数同步可见(评分不同步修复) await fetchSubmissions() - if (submissionGroup.value === questionGroup.value) fetchExamSubmissions() + // R29:「考生答题记录」的组别已改跟随成绩管理页(scoreExportGroup) + if (submissionGroup.value === scoreExportGroup.value) fetchExamSubmissions() } catch (e) { ElMessage.error('保存失败') } } @@ -2750,6 +2808,12 @@ async function saveTimes() { // ==================== 6. 成绩管理 ==================== const scoresPublishedMap = reactive({}) const scoreExportGroup = ref('') +/** + * 是否为「考试组」(中职 / 高职)——R29 由「试卷管理」迁入的两个板块 + * (考生答题记录 / 考生大题作答查证)只对这两组有意义: + * 本科 / 教师组提交的是 Word / PPT 作品,没有客观题与综合大题数据。 + */ +const isExamScoreGroup = computed(() => scoreExportGroup.value === 'zhongzhi' || scoreExportGroup.value === 'gaozhi') const scoreList = ref([]) const scoreListLoading = ref(false) const scoreListPage = ref(1) @@ -2835,10 +2899,11 @@ async function togglePublish() { } catch (e) { ElMessage.error('操作失败') } } -async function exportCsv() { - // safeDownload 自带「HTTP 状态 + content-type」双重校验,避免把错误 JSON 存成 .csv - await safeDownload(`/api/admin/export-csv/${scoreExportGroup.value}`, { - fallbackName: `${scoreExportGroup.value}_scores.csv`, +async function exportExcel() { + // R29:由 CSV 改为直接导出 .xlsx(CSV 在 Excel 中中文易乱码、身份证/手机号变科学计数法) + // safeDownload 自带「HTTP 状态 + content-type」双重校验,避免把错误 JSON 存成 .xlsx + await safeDownload(`/api/admin/export-excel/${scoreExportGroup.value}`, { + fallbackName: `${scoreExportGroup.value}_scores.xlsx`, }) } @@ -3477,6 +3542,15 @@ 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; +} + .search-input { width: 220px; } .filter-select { width: 140px; }