feat(admin): 成绩管理 Tab 分区 + 作品管理移除中职/高职 + 分页最小档补 5
R80 三项 UI 改造(单文件 src/views/Admin.vue,+78/-37):
1. 作品管理:移除「中职组 / 高职组」两个选项 —— 这两组已取消 Word 作品上传,
留着会让人误以为可以下载到作品。顺带清掉因此变成恒真的死判据
v-if="submissionGroup === 'benke' || submissionGroup === 'jiaoshi'"(批量下载作品按钮)。
后端 getWorks 仍支持 4 组,保留兼容不删。
2. 分页:9 处 page-sizes 统一补最小 5 档([5,10,20,50];账号管理 [5,10,20,50,100])。
试卷管理 3 处原本已是 5 档,无需改。动因:成绩管理表体量大、翻页粒度需要更细。
3. 成绩管理:由「一段纵向长页」改为「顶栏 + 三页签」——
- 顶栏只留组别级动作:组别选择 / 发布·撤回成绩 / 导出Excel / 批量下载作品
- 表级筛选(姓名 / 提交状态 / 日期范围)下沉到「成绩总览」页签,
否则切到别的页签时筛选框仍在,会造成「筛的是哪张表」的歧义
- el-tabs:成绩总览(始终)/ 考生答题记录(仅中职、高职)/ 考生大题作答查证(同上)
- 大题作答详情 el-dialog 放在 el-tabs 之外:留在页签内会随 v-if 卸载而销毁弹窗状态
🔴 唯一高危边界:activeScoreTab 停在后两个页签时切到本科/教师,两个页签 v-if 消失
⇒ el-tabs 无 active 内容 ⇒ 页签栏在、内容区全空(白屏)。治法:在
watch(scoreExportGroup) 内复位为 'overview' —— 用 watch 而非 onScoreGroupChange,
因为 watch 默认 pre-flush 会在组件重渲染之前执行,不会先闪一帧空白。
验证:verify_frontend_tdz PASS(activeScoreTab 声明顺序,本页历史真炸过的坑)·
@vue/compiler-sfc parse/compileTemplate 0 错 · ui-runtime-check 真 chromium 17 步 17/17 PASS。
变异自证 2 轮:恢复中职/高职选项 ⇒ expectNoText 2 项变红;拆掉切组复位 ⇒ 切本科组后
expectText("提交状态") 命中 0 处变红。
This commit is contained in:
@@ -187,7 +187,7 @@
|
||||
<el-pagination
|
||||
v-model:current-page="accountsPage"
|
||||
v-model:page-size="accountsPageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-sizes="[5, 10, 20, 50, 100]"
|
||||
:total="accountsTotal"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@@ -562,7 +562,7 @@
|
||||
<el-pagination
|
||||
v-model:current-page="materialsPage"
|
||||
v-model:page-size="materialsPageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:page-sizes="[5, 10, 20, 50]"
|
||||
:total="filteredAdminMaterials.length"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@@ -575,16 +575,14 @@
|
||||
<template v-if="activeMenu === 'submissions'">
|
||||
<h2 class="section-heading">作品管理</h2>
|
||||
<div class="toolbar">
|
||||
<!-- R80:中职组 / 高职组已取消 Word 作品上传 ⇒ 作品管理不再提供这两个选项 -->
|
||||
<el-select v-model="submissionGroup" placeholder="选择组别" class="filter-select" @change="onSubmissionGroupChange">
|
||||
<el-option label="中职组" value="zhongzhi" />
|
||||
<el-option label="高职组" value="gaozhi" />
|
||||
<el-option label="本科组" value="benke" />
|
||||
<el-option label="教师组" value="jiaoshi" />
|
||||
</el-select>
|
||||
<el-input v-model="submissionSearch" placeholder="搜索选手姓名" clearable size="small" class="search-input" @input="onSubmissionFilterChange" />
|
||||
<!-- R29:中职/高职组已取消 Word 作品上传 ⇒ 这两组不显示「批量下载作品」 -->
|
||||
<!-- R80:组别只剩本科/教师(两组都需下载作品)⇒ 原「仅 benke/jiaoshi 显示」判据已恒真,移除死判据 -->
|
||||
<el-button
|
||||
v-if="submissionGroup === 'benke' || submissionGroup === 'jiaoshi'"
|
||||
type="success"
|
||||
v-guard="() => batchDownloadWorks()"
|
||||
>批量下载作品</el-button>
|
||||
@@ -613,7 +611,7 @@
|
||||
<el-pagination
|
||||
v-model:current-page="submissionsPage"
|
||||
v-model:page-size="submissionsPageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:page-sizes="[5, 10, 20, 50]"
|
||||
:total="filteredSubmissions.length"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@@ -691,6 +689,10 @@
|
||||
<!-- ========== 成绩管理 ========== -->
|
||||
<template v-if="activeMenu === 'scores'">
|
||||
<h2 class="section-heading">成绩管理</h2>
|
||||
|
||||
<!-- R80:顶栏只保留「组别级」动作(组别 / 发布 / 导出 / 批量下载)。
|
||||
表级筛选(姓名 / 提交状态 / 日期)随表格下沉到「成绩总览」页签 ——
|
||||
否则切到别的页签时这些筛选框仍然在,会造成「筛的是哪张表」的歧义。 -->
|
||||
<div class="toolbar">
|
||||
<el-select v-model="scoreExportGroup" placeholder="选择组别" class="filter-select" @change="onScoreGroupChange">
|
||||
<el-option label="中职组" value="zhongzhi" />
|
||||
@@ -698,6 +700,37 @@
|
||||
<el-option label="本科组" value="benke" />
|
||||
<el-option label="教师组" value="jiaoshi" />
|
||||
</el-select>
|
||||
<el-button :type="scoresPublishedMap[scoreExportGroup] ? 'warning' : 'success'" v-guard="() => togglePublish()" :disabled="!scoreExportGroup">
|
||||
{{ scoresPublishedMap[scoreExportGroup] ? '撤回成绩' : '发布成绩' }}
|
||||
</el-button>
|
||||
<!-- R29:由「导出CSV」改为直接导出真正的 Excel(.xlsx) -->
|
||||
<el-button type="primary" plain v-guard="() => exportExcel()" :disabled="!scoreExportGroup">导出Excel</el-button>
|
||||
<!-- R29:中职/高职组已取消 Word 作品上传 ⇒ 这两组不显示「批量下载作品」 -->
|
||||
<el-button
|
||||
v-if="!isExamScoreGroup"
|
||||
type="success"
|
||||
v-guard="() => batchDownload()"
|
||||
:disabled="!scoreExportGroup"
|
||||
>批量下载作品</el-button>
|
||||
</div>
|
||||
|
||||
<!-- R80:功能分区页签(交互口径对齐「试卷管理」的题型 Tab ——
|
||||
el-tabs 只声明 label / name,各区块内容自己用 v-if 控制渲染)。
|
||||
后两个页签仅中职 / 高职有意义:只有这两组才有客观题与综合大题;
|
||||
本科 / 教师提交的是 Word/PPT 作品,故按组别隐藏。
|
||||
🔴 切到本科 / 教师时必须把 activeScoreTab 复位为 overview,
|
||||
否则 v-model 指向一个已不存在的页签 ⇒ 页签栏在、内容区全空(白屏)。
|
||||
⚠️ 以下三个分区 <template> 仅作外层包裹,内部区块缩进保持原样,
|
||||
以免 300 行缩进重排制造 diff 噪声。 -->
|
||||
<el-tabs v-model="activeScoreTab">
|
||||
<el-tab-pane label="成绩总览" name="overview" />
|
||||
<el-tab-pane v-if="isExamScoreGroup" label="考生答题记录" name="examSubmissions" />
|
||||
<el-tab-pane v-if="isExamScoreGroup" label="考生大题作答查证" name="bigAnswers" />
|
||||
</el-tabs>
|
||||
|
||||
<!-- ① 成绩总览 -->
|
||||
<template v-if="activeScoreTab === 'overview'">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="scoreSearch" placeholder="搜索姓名/身份证号" clearable size="small" class="search-input" @input="onScoreFilterChange" />
|
||||
<el-select v-model="scoreStatusFilter" placeholder="提交状态" clearable size="small" class="filter-select" @change="onScoreFilterChange">
|
||||
<el-option label="已提交" value="true" />
|
||||
@@ -714,18 +747,6 @@
|
||||
class="date-range"
|
||||
@change="onScoreFilterChange"
|
||||
/>
|
||||
<el-button :type="scoresPublishedMap[scoreExportGroup] ? 'warning' : 'success'" v-guard="() => togglePublish()" :disabled="!scoreExportGroup">
|
||||
{{ scoresPublishedMap[scoreExportGroup] ? '撤回成绩' : '发布成绩' }}
|
||||
</el-button>
|
||||
<!-- R29:由「导出CSV」改为直接导出真正的 Excel(.xlsx) -->
|
||||
<el-button type="primary" plain v-guard="() => exportExcel()" :disabled="!scoreExportGroup">导出Excel</el-button>
|
||||
<!-- R29:中职/高职组已取消 Word 作品上传 ⇒ 这两组不显示「批量下载作品」 -->
|
||||
<el-button
|
||||
v-if="!isExamScoreGroup"
|
||||
type="success"
|
||||
v-guard="() => batchDownload()"
|
||||
:disabled="!scoreExportGroup"
|
||||
>批量下载作品</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 成绩明细表 -->
|
||||
@@ -788,7 +809,7 @@
|
||||
<el-pagination
|
||||
v-model:current-page="scoreListPage"
|
||||
v-model:page-size="scoreListPageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:page-sizes="[5, 10, 20, 50]"
|
||||
:total="filteredScoreList.length"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@@ -806,11 +827,10 @@
|
||||
{{ scoresPublishedMap[scoreExportGroup] ? '成绩已发布' : '成绩未发布' }}
|
||||
</template>
|
||||
</el-result>
|
||||
<!-- ========== R29:由「试卷管理」迁入(仅中职/高职有客观题与综合大题) ========== -->
|
||||
<template v-if="isExamScoreGroup">
|
||||
<!-- 考生答题记录 -->
|
||||
<el-divider />
|
||||
<h3 class="section-subheading">考生答题记录</h3>
|
||||
</template><!-- /① 成绩总览 -->
|
||||
|
||||
<!-- ② 考生答题记录(R29 由「试卷管理」迁入;仅中职/高职有客观题数据) -->
|
||||
<template v-if="isExamScoreGroup && activeScoreTab === 'examSubmissions'">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="examSubsSearch" placeholder="搜索选手姓名" clearable size="small" class="search-input" @input="onExamSubsFilterChange" />
|
||||
<el-date-picker
|
||||
@@ -851,12 +871,13 @@
|
||||
<el-pagination
|
||||
v-model:current-page="examSubmissionsPage"
|
||||
v-model:page-size="examSubmissionsPageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:page-sizes="[5, 10, 20, 50]"
|
||||
:total="filteredExamSubmissions.length"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</template><!-- /② 考生答题记录 -->
|
||||
|
||||
<!-- R30:原「考生答题记录 · 评分」弹窗已删除。
|
||||
原因:大题评分统一在下方「考生大题作答查证」按小问给分
|
||||
@@ -865,9 +886,8 @@
|
||||
⛔ 后端 PUT /api/admin/scores/:group 与 adminApi.setScores 均**保留**:
|
||||
前者被 10+ 个门禁脚本依赖,后者作品管理页(本科/教师)仍在使用。 -->
|
||||
|
||||
<!-- ========== 考生大题作答查证 ========== -->
|
||||
<el-divider />
|
||||
<h3 class="section-subheading">考生大题作答查证</h3>
|
||||
<!-- ③ 考生大题作答查证(仅中职/高职) -->
|
||||
<template v-if="isExamScoreGroup && activeScoreTab === 'bigAnswers'">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
@@ -967,8 +987,11 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="filteredBigAnswers.length === 0 && !bigAnswersLoading" description="该组暂无考生数据" :image-size="80" />
|
||||
</template><!-- /③ 考生大题作答查证 -->
|
||||
|
||||
<!-- 综合大题:作答详情 + 评分弹窗 -->
|
||||
<!-- 综合大题:作答详情 + 评分弹窗。
|
||||
R80:弹窗移到页签之外 —— 若留在页签内,切换页签会随 v-if 卸载而销毁弹窗状态。 -->
|
||||
<template v-if="isExamScoreGroup">
|
||||
<el-dialog
|
||||
v-model="bigAnswerDialogVisible"
|
||||
:title="'大题作答详情 — ' + (bigAnswerRow.name || '')"
|
||||
@@ -1355,7 +1378,7 @@
|
||||
<el-pagination
|
||||
v-model:current-page="adminNewsPage"
|
||||
v-model:page-size="adminNewsPageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:page-sizes="[5, 10, 20, 50]"
|
||||
:total="filteredAdminNews.length"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@@ -2893,11 +2916,29 @@ const scoreExportGroup = ref('')
|
||||
*/
|
||||
const isExamScoreGroup = computed(() => scoreExportGroup.value === 'zhongzhi' || scoreExportGroup.value === 'gaozhi')
|
||||
|
||||
/**
|
||||
* R80:成绩管理页「功能分区页签」当前项。
|
||||
*
|
||||
* 三个页签:overview(成绩总览,任何组别都有)/ examSubmissions(考生答题记录)/
|
||||
* bigAnswers(考生大题作答查证)—— 后两个仅中职 / 高职存在。
|
||||
*
|
||||
* 🔴 TDZ:本 ref 必须声明在下方 watch() **之前**。watch() 是顶层语句、会立即执行
|
||||
* (取值收集依赖),若引用其后声明的 const,setup 阶段会抛
|
||||
* "Cannot access 'activeScoreTab' before initialization"。
|
||||
* 验证:node .workbuddy/tools/verify_frontend_tdz.mjs frontend/src/views/Admin.vue
|
||||
*/
|
||||
const activeScoreTab = ref('overview')
|
||||
|
||||
// R29:成绩管理页切组 → 刷新成绩明细 + 两个由试卷管理迁入的板块
|
||||
// (这两个板块的组别跟随 scoreExportGroup,故其刷新必须挂在这里)
|
||||
// 🔴 必须写在 `scoreExportGroup` 声明**之后**:watch() 会立即取值收集依赖,
|
||||
// 前置引用会在 setup 阶段触发 TDZ(Cannot access 'scoreExportGroup' before initialization)。
|
||||
watch(scoreExportGroup, () => {
|
||||
// R80:切到本科 / 教师时两个考试组专属页签会消失。若 activeScoreTab 仍停在
|
||||
// 被隐藏的页签上,el-tabs 将没有任何 active 内容 ⇒ 页签栏在、内容区全空。
|
||||
// 放在此处而非 onScoreGroupChange:watch 默认 pre-flush,会在组件重渲染**之前**
|
||||
// 执行,因此不会先渲染一次空白再复位(无闪屏)。
|
||||
if (!isExamScoreGroup.value) activeScoreTab.value = 'overview'
|
||||
if (activeMenu.value === 'scores') refreshExamCtxData(true)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user