feat(admin): 功能分布优化——板块迁入成绩管理 + 导出 Excel + 废弃按钮清理(R29)
- 试卷管理只留试卷相关:「考生答题记录」(含评分弹窗)与「考生大题作答查证」 整段迁至成绩管理页,外层 <template v-if="isExamScoreGroup"> 仅中职/高职可见 - 组别绑定改跟随成绩页 scoreExportGroup(避免串组):新增 isExamScoreGroup / refreshExamCtxData / refreshScorePageData + watch(scoreExportGroup);ensureBigData 收窄为只刷题库 - 导出按钮:导出CSV → 导出Excel(GET /api/admin/export-excel/:group,fallback .xlsx) - 中职/高职已取消 Word 作品上传:成绩页与作品管理页「批量下载作品」按组别 v-if 隐藏 - 评分弹窗文字错位修复:label-width 120px + label-position="left" + 标签左对齐不换行 - 5 个纯白按钮补 type(info/primary plain)提升可见度
This commit is contained in:
@@ -212,7 +212,7 @@
|
|||||||
<el-button type="primary" @click="openQuestionDialog(null, activeQuestionType)">添加题目</el-button>
|
<el-button type="primary" @click="openQuestionDialog(null, activeQuestionType)">添加题目</el-button>
|
||||||
<el-tag type="info" size="small" class="score-policy-tag">客观题分值口径:每小题固定 1 分</el-tag>
|
<el-tag type="info" size="small" class="score-policy-tag">客观题分值口径:每小题固定 1 分</el-tag>
|
||||||
<el-divider direction="vertical" />
|
<el-divider direction="vertical" />
|
||||||
<el-button size="small" v-guard="() => downloadQuestionTpl()">下载Excel模板</el-button>
|
<el-button size="small" type="info" plain 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>
|
||||||
<input ref="excelFileInput" type="file" accept=".xlsx,.xls" style="display:none" @change="onExcelFileChange" />
|
<input ref="excelFileInput" type="file" accept=".xlsx,.xls" style="display:none" @change="onExcelFileChange" />
|
||||||
<el-button size="small" type="danger" :disabled="selectedQuestionRows.length === 0" v-guard="() => batchDeleteSelectedQuestions()">批量删除({{ selectedQuestionRows.length }})</el-button>
|
<el-button size="small" type="danger" :disabled="selectedQuestionRows.length === 0" v-guard="() => batchDeleteSelectedQuestions()">批量删除({{ selectedQuestionRows.length }})</el-button>
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<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" type="info" plain :loading="bigQuestionsLoading" @click="fetchBigQuestions">刷新</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
type="warning"
|
type="warning"
|
||||||
@@ -367,103 +367,6 @@
|
|||||||
|
|
||||||
<el-empty v-if="activeQuestionType !== 'big' && currentQuestions.length === 0 && !questionsLoading" :description="'暂无' + questionTypeLabel + '题目'" :image-size="80" />
|
<el-empty v-if="activeQuestionType !== 'big' && currentQuestions.length === 0 && !questionsLoading" :description="'暂无' + questionTypeLabel + '题目'" :image-size="80" />
|
||||||
|
|
||||||
<!-- ========== 综合大题:考生作答查证 ========== -->
|
|
||||||
<template v-if="activeQuestionType === 'big'">
|
|
||||||
<el-divider />
|
|
||||||
<h3 class="section-subheading">考生大题作答查证</h3>
|
|
||||||
<el-alert
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
style="margin-bottom:12px"
|
|
||||||
title="用于核对考生实际抽到的题目与其提交的答案,处理考生纠纷。题干为该考生抽题时刻的快照,管理员后续修改题库不会影响此处显示。"
|
|
||||||
/>
|
|
||||||
<div class="toolbar">
|
|
||||||
<el-input v-model="bigAnswerSearch" placeholder="搜索考生姓名 / 身份证号" clearable size="small" class="search-input" />
|
|
||||||
<el-select v-model="bigAnswerFilter" size="small" class="filter-select">
|
|
||||||
<el-option label="全部考生" value="all" />
|
|
||||||
<el-option label="已抽题" value="drawn" />
|
|
||||||
<el-option label="未抽题" value="undrawn" />
|
|
||||||
</el-select>
|
|
||||||
<el-button size="small" :loading="bigAnswersLoading" @click="fetchBigAnswers">刷新</el-button>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
:disabled="bigAnswerSelectedIds.length === 0"
|
|
||||||
:loading="bigAnswersExporting"
|
|
||||||
v-guard="() => exportBigAnswers(bigAnswerSelectedIds)"
|
|
||||||
>导出选中 Word({{ bigAnswerSelectedIds.length }})</el-button>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
type="success"
|
|
||||||
:loading="bigAnswersExporting"
|
|
||||||
v-guard="() => exportBigAnswers()"
|
|
||||||
>导出全部 Word</el-button>
|
|
||||||
<el-tag type="info" class="total-score-tag">
|
|
||||||
共 {{ bigAnswersTotal }} 人,已抽题 {{ bigAnswersDrawn }} 人
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<el-table
|
|
||||||
ref="bigAnswersTableRef"
|
|
||||||
:data="filteredBigAnswers"
|
|
||||||
stripe
|
|
||||||
v-loading="bigAnswersLoading"
|
|
||||||
class="admin-table"
|
|
||||||
row-key="contestantId"
|
|
||||||
@selection-change="onBigAnswerSelectionChange"
|
|
||||||
>
|
|
||||||
<el-table-column type="selection" width="44" :selectable="row => row.drawn" />
|
|
||||||
<el-table-column prop="name" label="姓名" width="110" />
|
|
||||||
<el-table-column prop="idNumber" label="身份证号" width="180" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="phone" label="手机号" width="130" />
|
|
||||||
<el-table-column label="抽题" width="90" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.drawn ? 'success' : 'info'" size="small">{{ row.drawn ? '已抽题' : '未抽题' }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="抽到大题" min-width="180" show-overflow-tooltip>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row.questionSnapshot">{{ row.questionSnapshot.title }}</span>
|
|
||||||
<span v-else style="color:#909399">--</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="已答小问" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row.drawn">{{ row.answeredCount }} / {{ bigSubCount }}</span>
|
|
||||||
<span v-else style="color:#909399">--</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="大题得分" width="110" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row.bigScore !== null && row.bigScore !== undefined">{{ fmtScore(row.bigScore) }} 分</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>
|
|
||||||
<span v-else style="color:#909399">未提交</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="210" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button text type="primary" size="small" :disabled="!row.drawn" @click="openBigAnswerDetail(row)">
|
|
||||||
{{ row.bigScore === null || row.bigScore === undefined ? '查看/评分' : '查看/改分' }}
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
text
|
|
||||||
type="success"
|
|
||||||
size="small"
|
|
||||||
:disabled="!row.drawn"
|
|
||||||
:loading="bigAnswersExporting"
|
|
||||||
@click="exportBigAnswers([row.contestantId])"
|
|
||||||
>导出Word</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-empty v-if="filteredBigAnswers.length === 0 && !bigAnswersLoading" description="该组暂无考生数据" :image-size="80" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 题目编辑弹窗 -->
|
<!-- 题目编辑弹窗 -->
|
||||||
<el-dialog v-model="questionDialogVisible" :title="dialogTitle" width="640px">
|
<el-dialog v-model="questionDialogVisible" :title="dialogTitle" width="640px">
|
||||||
<el-form :model="questionForm" label-position="top">
|
<el-form :model="questionForm" label-position="top">
|
||||||
@@ -597,158 +500,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 综合大题:作答详情 + 评分弹窗 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="bigAnswerDialogVisible"
|
|
||||||
:title="'大题作答详情 — ' + (bigAnswerRow.name || '')"
|
|
||||||
width="880px"
|
|
||||||
top="5vh"
|
|
||||||
>
|
|
||||||
<template v-if="bigAnswerRow.contestantId">
|
|
||||||
<el-descriptions :column="3" border size="small" class="big-answer-meta">
|
|
||||||
<el-descriptions-item label="姓名">{{ bigAnswerRow.name }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="身份证号">{{ bigAnswerRow.idNumber }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="手机号">{{ bigAnswerRow.phone }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="抽题状态">{{ bigAnswerRow.drawn ? '已抽题' : '未抽题' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="抽到题号">{{ bigAnswerRow.bigQuestionUid || '--' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="提交时间">
|
|
||||||
{{ bigAnswerRow.submitTime ? formatDateTime(bigAnswerRow.submitTime) : '未提交' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<template v-if="bigAnswerRow.drawn">
|
|
||||||
<h4 class="big-answer-h4">考生抽到的题目(抽题时刻快照,防纠纷)</h4>
|
|
||||||
<div class="big-snapshot">
|
|
||||||
<div class="big-snapshot-title">
|
|
||||||
{{ bigAnswerRow.questionSnapshot && bigAnswerRow.questionSnapshot.title }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="big-preview-body"
|
|
||||||
v-html="previewHtml(bigAnswerRow.questionSnapshot && bigAnswerRow.questionSnapshot.description)"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h4 class="big-answer-h4">考生提交的答案与评分</h4>
|
|
||||||
<div v-for="i in bigSubCount" :key="i" class="big-answer-sub">
|
|
||||||
<div class="big-answer-sub-head">
|
|
||||||
<span>第 {{ i }} 小问</span>
|
|
||||||
<span class="big-answer-chars">
|
|
||||||
{{ bigAnswerTextOf(i).length }} 字
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="bigSubPromptsOfRow[i - 1]" class="big-answer-prompt">
|
|
||||||
{{ bigSubPromptsOfRow[i - 1] }}
|
|
||||||
</div>
|
|
||||||
<div class="big-answer-text">
|
|
||||||
<span v-if="bigAnswerTextOf(i).trim()">{{ bigAnswerTextOf(i) }}</span>
|
|
||||||
<span v-else class="big-answer-empty">(未作答)</span>
|
|
||||||
</div>
|
|
||||||
<div class="big-answer-score">
|
|
||||||
<span class="big-answer-score-label">得分:</span>
|
|
||||||
<el-input-number
|
|
||||||
v-model="bigScoreForm[`score_${i}`]"
|
|
||||||
:min="0"
|
|
||||||
:max="bigScorePerSub"
|
|
||||||
:step="0.25"
|
|
||||||
:precision="2"
|
|
||||||
size="small"
|
|
||||||
controls-position="right"
|
|
||||||
/>
|
|
||||||
<span class="big-answer-score-max">/ {{ bigScorePerSub }} 分</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="big-answer-total">
|
|
||||||
本大题合计:
|
|
||||||
<strong>{{ fmtScore(bigScoreTotal) }}</strong> 分(满分 {{ bigScorePerSub * bigSubCount }} 分)
|
|
||||||
<span class="big-answer-tip">保存后将写入该考生「作品评分」,总分 = 客观题得分 + 作品评分</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-alert v-else type="info" :closable="false" show-icon title="该考生尚未抽取综合大题(可能未进入考试)" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="bigAnswerDialogVisible = false">关闭</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="bigAnswerRow.drawn"
|
|
||||||
type="primary"
|
|
||||||
:loading="bigScoreSaving"
|
|
||||||
v-guard="() => saveBigScores()"
|
|
||||||
>保存评分</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 考生答题记录 -->
|
|
||||||
<el-divider />
|
|
||||||
<h3 class="section-subheading">考生答题记录</h3>
|
|
||||||
<div class="toolbar">
|
|
||||||
<el-input v-model="examSubsSearch" placeholder="搜索选手姓名" clearable size="small" class="search-input" @input="onExamSubsFilterChange" />
|
|
||||||
<el-date-picker
|
|
||||||
v-model="examSubsDateRange"
|
|
||||||
type="daterange"
|
|
||||||
range-separator="至"
|
|
||||||
start-placeholder="提交开始日期"
|
|
||||||
end-placeholder="提交结束日期"
|
|
||||||
align="right"
|
|
||||||
size="small"
|
|
||||||
class="date-range"
|
|
||||||
@change="onExamSubsFilterChange"
|
|
||||||
/>
|
|
||||||
<el-button text type="primary" v-guard="() => fetchExamSubmissions()" :loading="examSubmissionsLoading">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table :data="pagedExamSubmissions" stripe v-loading="examSubmissionsLoading" class="admin-table">
|
|
||||||
<el-table-column prop="userName" label="选手姓名" width="120" />
|
|
||||||
<el-table-column label="提交状态" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.submitted ? 'success' : 'info'" size="small">
|
|
||||||
{{ row.submitted ? '已提交' : '未提交' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="答题情况" min-width="220" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span>已答 {{ row.answeredCount || 0 }} / {{ row.totalQuestions || 0 }} 题</span>
|
|
||||||
<span style="margin-left:10px;color:#1a3a5c;font-weight:600">客观分:{{ row.examScore ?? 0 }}</span>
|
|
||||||
<span style="margin-left:10px;color:#1a3a5c;font-weight:600">大题分:{{ row.wordScore ?? 0 }}</span>
|
|
||||||
<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>
|
|
||||||
</el-table>
|
|
||||||
<div class="pagination-wrap" v-if="filteredExamSubmissions.length > 0">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="examSubmissionsPage"
|
|
||||||
v-model:page-size="examSubmissionsPageSize"
|
|
||||||
:page-sizes="[10, 20, 50]"
|
|
||||||
:total="filteredExamSubmissions.length"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 考生大题评分弹窗(客观分只读,大题得分可改) -->
|
|
||||||
<el-dialog v-model="examScoreDialogVisible" title="评分" width="460px">
|
|
||||||
<el-form label-width="100px" v-if="examScoreDialogRow">
|
|
||||||
<el-form-item label="选手">{{ examScoreDialogRow.userName }}</el-form-item>
|
|
||||||
<el-form-item label="客观分(卷面)">
|
|
||||||
<span>{{ 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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ========== 备赛资料管理 ========== -->
|
<!-- ========== 备赛资料管理 ========== -->
|
||||||
@@ -816,7 +567,12 @@
|
|||||||
<el-option label="教师组" value="jiaoshi" />
|
<el-option label="教师组" value="jiaoshi" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input v-model="submissionSearch" placeholder="搜索选手姓名" clearable size="small" class="search-input" @input="onSubmissionFilterChange" />
|
<el-input v-model="submissionSearch" placeholder="搜索选手姓名" clearable size="small" class="search-input" @input="onSubmissionFilterChange" />
|
||||||
<el-button type="success" v-guard="() => batchDownloadWorks()">批量下载作品</el-button>
|
<!-- R29:中职/高职组已取消 Word 作品上传 ⇒ 这两组不显示「批量下载作品」 -->
|
||||||
|
<el-button
|
||||||
|
v-if="submissionGroup === 'benke' || submissionGroup === 'jiaoshi'"
|
||||||
|
type="success"
|
||||||
|
v-guard="() => batchDownloadWorks()"
|
||||||
|
>批量下载作品</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="pagedSubmissions" stripe v-loading="submissionsLoading" class="admin-table">
|
<el-table :data="pagedSubmissions" stripe v-loading="submissionsLoading" class="admin-table">
|
||||||
<el-table-column prop="name" label="选手姓名" width="120" />
|
<el-table-column prop="name" label="选手姓名" width="120" />
|
||||||
@@ -946,8 +702,15 @@
|
|||||||
<el-button :type="scoresPublishedMap[scoreExportGroup] ? 'warning' : 'success'" v-guard="() => togglePublish()" :disabled="!scoreExportGroup">
|
<el-button :type="scoresPublishedMap[scoreExportGroup] ? 'warning' : 'success'" v-guard="() => togglePublish()" :disabled="!scoreExportGroup">
|
||||||
{{ scoresPublishedMap[scoreExportGroup] ? '撤回成绩' : '发布成绩' }}
|
{{ scoresPublishedMap[scoreExportGroup] ? '撤回成绩' : '发布成绩' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-guard="() => exportCsv()" :disabled="!scoreExportGroup">导出CSV</el-button>
|
<!-- R29:由「导出CSV」改为直接导出真正的 Excel(.xlsx) -->
|
||||||
<el-button v-guard="() => batchDownload()" :disabled="!scoreExportGroup">批量下载作品</el-button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 成绩明细表 -->
|
<!-- 成绩明细表 -->
|
||||||
@@ -1028,6 +791,264 @@
|
|||||||
{{ scoresPublishedMap[scoreExportGroup] ? '成绩已发布' : '成绩未发布' }}
|
{{ scoresPublishedMap[scoreExportGroup] ? '成绩已发布' : '成绩未发布' }}
|
||||||
</template>
|
</template>
|
||||||
</el-result>
|
</el-result>
|
||||||
|
<!-- ========== R29:由「试卷管理」迁入(仅中职/高职有客观题与综合大题) ========== -->
|
||||||
|
<template v-if="isExamScoreGroup">
|
||||||
|
<!-- 考生答题记录 -->
|
||||||
|
<el-divider />
|
||||||
|
<h3 class="section-subheading">考生答题记录</h3>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-input v-model="examSubsSearch" placeholder="搜索选手姓名" clearable size="small" class="search-input" @input="onExamSubsFilterChange" />
|
||||||
|
<el-date-picker
|
||||||
|
v-model="examSubsDateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="提交开始日期"
|
||||||
|
end-placeholder="提交结束日期"
|
||||||
|
align="right"
|
||||||
|
size="small"
|
||||||
|
class="date-range"
|
||||||
|
@change="onExamSubsFilterChange"
|
||||||
|
/>
|
||||||
|
<el-button text type="primary" v-guard="() => fetchExamSubmissions()" :loading="examSubmissionsLoading">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="pagedExamSubmissions" stripe v-loading="examSubmissionsLoading" class="admin-table">
|
||||||
|
<el-table-column prop="userName" label="选手姓名" width="120" />
|
||||||
|
<el-table-column label="提交状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.submitted ? 'success' : 'info'" size="small">
|
||||||
|
{{ row.submitted ? '已提交' : '未提交' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="答题情况" min-width="220" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>已答 {{ row.answeredCount || 0 }} / {{ row.totalQuestions || 0 }} 题</span>
|
||||||
|
<span style="margin-left:10px;color:#1a3a5c;font-weight:600">客观分:{{ row.examScore ?? 0 }}</span>
|
||||||
|
<span style="margin-left:10px;color:#1a3a5c;font-weight:600">大题分:{{ row.wordScore ?? 0 }}</span>
|
||||||
|
<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>
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination-wrap" v-if="filteredExamSubmissions.length > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="examSubmissionsPage"
|
||||||
|
v-model:page-size="examSubmissionsPageSize"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
:total="filteredExamSubmissions.length"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- ========== 考生大题作答查证 ========== -->
|
||||||
|
<el-divider />
|
||||||
|
<h3 class="section-subheading">考生大题作答查证</h3>
|
||||||
|
<el-alert
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom:12px"
|
||||||
|
title="用于核对考生实际抽到的题目与其提交的答案,处理考生纠纷。题干为该考生抽题时刻的快照,管理员后续修改题库不会影响此处显示。"
|
||||||
|
/>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-input v-model="bigAnswerSearch" placeholder="搜索考生姓名 / 身份证号" clearable size="small" class="search-input" />
|
||||||
|
<el-select v-model="bigAnswerFilter" size="small" class="filter-select">
|
||||||
|
<el-option label="全部考生" value="all" />
|
||||||
|
<el-option label="已抽题" value="drawn" />
|
||||||
|
<el-option label="未抽题" value="undrawn" />
|
||||||
|
</el-select>
|
||||||
|
<el-button size="small" type="info" plain :loading="bigAnswersLoading" @click="fetchBigAnswers">刷新</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:disabled="bigAnswerSelectedIds.length === 0"
|
||||||
|
:loading="bigAnswersExporting"
|
||||||
|
v-guard="() => exportBigAnswers(bigAnswerSelectedIds)"
|
||||||
|
>导出选中 Word({{ bigAnswerSelectedIds.length }})</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="success"
|
||||||
|
:loading="bigAnswersExporting"
|
||||||
|
v-guard="() => exportBigAnswers()"
|
||||||
|
>导出全部 Word</el-button>
|
||||||
|
<el-tag type="info" class="total-score-tag">
|
||||||
|
共 {{ bigAnswersTotal }} 人,已抽题 {{ bigAnswersDrawn }} 人
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
ref="bigAnswersTableRef"
|
||||||
|
:data="filteredBigAnswers"
|
||||||
|
stripe
|
||||||
|
v-loading="bigAnswersLoading"
|
||||||
|
class="admin-table"
|
||||||
|
row-key="contestantId"
|
||||||
|
@selection-change="onBigAnswerSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="44" :selectable="row => row.drawn" />
|
||||||
|
<el-table-column prop="name" label="姓名" width="110" />
|
||||||
|
<el-table-column prop="idNumber" label="身份证号" width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="phone" label="手机号" width="130" />
|
||||||
|
<el-table-column label="抽题" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.drawn ? 'success' : 'info'" size="small">{{ row.drawn ? '已抽题' : '未抽题' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="抽到大题" min-width="180" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.questionSnapshot">{{ row.questionSnapshot.title }}</span>
|
||||||
|
<span v-else style="color:#909399">--</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="已答小问" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.drawn">{{ row.answeredCount }} / {{ bigSubCount }}</span>
|
||||||
|
<span v-else style="color:#909399">--</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="大题得分" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.bigScore !== null && row.bigScore !== undefined">{{ fmtScore(row.bigScore) }} 分</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>
|
||||||
|
<span v-else style="color:#909399">未提交</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="210" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button text type="primary" size="small" :disabled="!row.drawn" @click="openBigAnswerDetail(row)">
|
||||||
|
{{ row.bigScore === null || row.bigScore === undefined ? '查看/评分' : '查看/改分' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
text
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
:disabled="!row.drawn"
|
||||||
|
:loading="bigAnswersExporting"
|
||||||
|
@click="exportBigAnswers([row.contestantId])"
|
||||||
|
>导出Word</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-if="filteredBigAnswers.length === 0 && !bigAnswersLoading" description="该组暂无考生数据" :image-size="80" />
|
||||||
|
|
||||||
|
<!-- 综合大题:作答详情 + 评分弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="bigAnswerDialogVisible"
|
||||||
|
:title="'大题作答详情 — ' + (bigAnswerRow.name || '')"
|
||||||
|
width="880px"
|
||||||
|
top="5vh"
|
||||||
|
>
|
||||||
|
<template v-if="bigAnswerRow.contestantId">
|
||||||
|
<el-descriptions :column="3" border size="small" class="big-answer-meta">
|
||||||
|
<el-descriptions-item label="姓名">{{ bigAnswerRow.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="身份证号">{{ bigAnswerRow.idNumber }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机号">{{ bigAnswerRow.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="抽题状态">{{ bigAnswerRow.drawn ? '已抽题' : '未抽题' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="抽到题号">{{ bigAnswerRow.bigQuestionUid || '--' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="提交时间">
|
||||||
|
{{ bigAnswerRow.submitTime ? formatDateTime(bigAnswerRow.submitTime) : '未提交' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<template v-if="bigAnswerRow.drawn">
|
||||||
|
<h4 class="big-answer-h4">考生抽到的题目(抽题时刻快照,防纠纷)</h4>
|
||||||
|
<div class="big-snapshot">
|
||||||
|
<div class="big-snapshot-title">
|
||||||
|
{{ bigAnswerRow.questionSnapshot && bigAnswerRow.questionSnapshot.title }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="big-preview-body"
|
||||||
|
v-html="previewHtml(bigAnswerRow.questionSnapshot && bigAnswerRow.questionSnapshot.description)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 class="big-answer-h4">考生提交的答案与评分</h4>
|
||||||
|
<div v-for="i in bigSubCount" :key="i" class="big-answer-sub">
|
||||||
|
<div class="big-answer-sub-head">
|
||||||
|
<span>第 {{ i }} 小问</span>
|
||||||
|
<span class="big-answer-chars">
|
||||||
|
{{ bigAnswerTextOf(i).length }} 字
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="bigSubPromptsOfRow[i - 1]" class="big-answer-prompt">
|
||||||
|
{{ bigSubPromptsOfRow[i - 1] }}
|
||||||
|
</div>
|
||||||
|
<div class="big-answer-text">
|
||||||
|
<span v-if="bigAnswerTextOf(i).trim()">{{ bigAnswerTextOf(i) }}</span>
|
||||||
|
<span v-else class="big-answer-empty">(未作答)</span>
|
||||||
|
</div>
|
||||||
|
<div class="big-answer-score">
|
||||||
|
<span class="big-answer-score-label">得分:</span>
|
||||||
|
<el-input-number
|
||||||
|
v-model="bigScoreForm[`score_${i}`]"
|
||||||
|
:min="0"
|
||||||
|
:max="bigScorePerSub"
|
||||||
|
:step="0.25"
|
||||||
|
:precision="2"
|
||||||
|
size="small"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
<span class="big-answer-score-max">/ {{ bigScorePerSub }} 分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="big-answer-total">
|
||||||
|
本大题合计:
|
||||||
|
<strong>{{ fmtScore(bigScoreTotal) }}</strong> 分(满分 {{ bigScorePerSub * bigSubCount }} 分)
|
||||||
|
<span class="big-answer-tip">保存后将写入该考生「作品评分」,总分 = 客观题得分 + 作品评分</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-alert v-else type="info" :closable="false" show-icon title="该考生尚未抽取综合大题(可能未进入考试)" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="bigAnswerDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="bigAnswerRow.drawn"
|
||||||
|
type="primary"
|
||||||
|
:loading="bigScoreSaving"
|
||||||
|
v-guard="() => saveBigScores()"
|
||||||
|
>保存评分</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ========== 成绩复核 ========== -->
|
<!-- ========== 成绩复核 ========== -->
|
||||||
@@ -1302,7 +1323,7 @@
|
|||||||
@change="onNewsFilterChange"
|
@change="onNewsFilterChange"
|
||||||
/>
|
/>
|
||||||
<el-button type="primary" @click="openNewsDialog()">添加新闻</el-button>
|
<el-button type="primary" @click="openNewsDialog()">添加新闻</el-button>
|
||||||
<el-button v-guard="() => saveNewsOrder()" :disabled="adminNews.length < 2">保存排序</el-button>
|
<el-button type="primary" plain v-guard="() => saveNewsOrder()" :disabled="adminNews.length < 2">保存排序</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-loading="adminNewsLoading" class="news-card-list" ref="newsCardListRef">
|
<div v-loading="adminNewsLoading" class="news-card-list" ref="newsCardListRef">
|
||||||
<div
|
<div
|
||||||
@@ -1392,7 +1413,7 @@
|
|||||||
<h2 class="section-heading">轮播管理</h2>
|
<h2 class="section-heading">轮播管理</h2>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<el-button type="primary" @click="openCarouselDialog()">添加轮播</el-button>
|
<el-button type="primary" @click="openCarouselDialog()">添加轮播</el-button>
|
||||||
<el-button v-guard="() => saveCarouselOrder()" :disabled="adminCarousels.length < 2">保存排序</el-button>
|
<el-button type="primary" plain v-guard="() => saveCarouselOrder()" :disabled="adminCarousels.length < 2">保存排序</el-button>
|
||||||
<span class="drag-hint" v-if="adminCarousels.length > 1">
|
<span class="drag-hint" v-if="adminCarousels.length > 1">
|
||||||
<el-icon><Rank /></el-icon> 拖拽卡片可调整排序
|
<el-icon><Rank /></el-icon> 拖拽卡片可调整排序
|
||||||
</span>
|
</span>
|
||||||
@@ -1563,13 +1584,18 @@ 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) {
|
||||||
// 单一刷新入口:题库 + 考生答题记录 +(大题库 / 大题作答)
|
// R29:试卷管理只刷题库(含大题库);「考生答题记录 / 大题作答查证」已迁到成绩管理
|
||||||
refreshQuestionData(true)
|
refreshQuestionData(true)
|
||||||
}
|
}
|
||||||
if (index === 'materials') fetchAdminMaterials()
|
if (index === 'materials') fetchAdminMaterials()
|
||||||
if (index === 'submissions' && submissionGroup.value) fetchSubmissions()
|
if (index === 'submissions' && submissionGroup.value) fetchSubmissions()
|
||||||
if (index === 'times') fetchTimes()
|
if (index === 'times') fetchTimes()
|
||||||
if (index === 'scores') { fetchPublishStatus(); fetchScoreList() }
|
if (index === 'scores') {
|
||||||
|
fetchPublishStatus()
|
||||||
|
fetchScoreList()
|
||||||
|
// R29:成绩管理还承载两个由试卷管理迁入的板块(仅中职/高职)
|
||||||
|
refreshExamCtxData(true)
|
||||||
|
}
|
||||||
if (index === 'news') fetchAdminNews()
|
if (index === 'news') fetchAdminNews()
|
||||||
if (index === 'carousel') fetchCarousels()
|
if (index === 'carousel') fetchCarousels()
|
||||||
}
|
}
|
||||||
@@ -2056,7 +2082,8 @@ function onBigAnswerSelectionChange(rows) {
|
|||||||
* ⚠️ 与同页「导出选中/全部 Word」(exportBigQuestions,导出的是**题库题目**)是两个功能,勿混。
|
* ⚠️ 与同页「导出选中/全部 Word」(exportBigQuestions,导出的是**题库题目**)是两个功能,勿混。
|
||||||
*/
|
*/
|
||||||
async function exportBigAnswers(ids) {
|
async function exportBigAnswers(ids) {
|
||||||
const g = questionGroup.value
|
// R29:本板块已迁入「成绩管理」⇒ 组别跟随成绩页
|
||||||
|
const g = scoreExportGroup.value
|
||||||
const groupLabel = g === 'zhongzhi' ? '中职组' : '高职组'
|
const groupLabel = g === 'zhongzhi' ? '中职组' : '高职组'
|
||||||
const isAll = !Array.isArray(ids)
|
const isAll = !Array.isArray(ids)
|
||||||
// 兜底:剔除非本组 / 未抽题的残留 ID(与 exportBigQuestions 同源处理)。
|
// 兜底:剔除非本组 / 未抽题的残留 ID(与 exportBigQuestions 同源处理)。
|
||||||
@@ -2086,20 +2113,21 @@ async function exportBigAnswers(ids) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchBigAnswers() {
|
async function fetchBigAnswers() {
|
||||||
// 同 fetchBigQuestions:固定发起时的组别,防止切组竞态把别组数据写回
|
// R29:本板块已由「试卷管理」迁到「成绩管理」⇒ 组别跟随成绩页的 scoreExportGroup(中职/高职)。
|
||||||
const g = questionGroup.value
|
// 仍固定发起时的组别,防止切组竞态把别组数据写回。
|
||||||
|
const g = scoreExportGroup.value
|
||||||
bigAnswersLoading.value = true
|
bigAnswersLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await adminApi.getBigAnswers(g)
|
const data = await adminApi.getBigAnswers(g)
|
||||||
const result = data?.data || data
|
const result = data?.data || data
|
||||||
if (questionGroup.value !== g) return
|
if (scoreExportGroup.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
|
if (scoreExportGroup.value !== g) return
|
||||||
bigAnswers.value = []
|
bigAnswers.value = []
|
||||||
bigAnswersTotal.value = 0
|
bigAnswersTotal.value = 0
|
||||||
bigAnswersDrawn.value = 0
|
bigAnswersDrawn.value = 0
|
||||||
@@ -2118,26 +2146,46 @@ async function fetchBigAnswers() {
|
|||||||
*/
|
*/
|
||||||
const bigDataGroup = ref('')
|
const bigDataGroup = ref('')
|
||||||
|
|
||||||
/** 保证大题库 + 大题作答与当前组别一致(同组且非强制时不重复请求) */
|
/** 保证大题库与当前组别一致(同组且非强制时不重复请求) */
|
||||||
async function ensureBigData(force = false) {
|
async function ensureBigData(force = false) {
|
||||||
const g = questionGroup.value
|
const g = questionGroup.value
|
||||||
if (!force && bigDataGroup.value === g && bigQuestions.value.length > 0) return
|
if (!force && bigDataGroup.value === g && bigQuestions.value.length > 0) return
|
||||||
await Promise.allSettled([fetchBigQuestions(), fetchBigAnswers()])
|
await fetchBigQuestions()
|
||||||
if (questionGroup.value === g) bigDataGroup.value = g
|
if (questionGroup.value === g) bigDataGroup.value = g
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 刷新「题目管理」菜单下与组别相关的全部数据(2026-09-23)。
|
* 刷新「试卷管理」菜单下的数据(R29 收敛)。
|
||||||
*
|
*
|
||||||
* 原先各入口各刷一半(切组只刷题库、评分只刷大题作答),导致另一处仍是旧值,
|
* ⚠️ R29 起「考生答题记录 / 大题作答查证」已迁到「成绩管理」,故此处**只刷题库**
|
||||||
* 测试人员只能手动刷新整页。此处收敛为**唯一刷新入口**,避免再出现遗漏。
|
* (含大题库);那两块改由 refreshExamCtxData 负责 —— 原来一个入口刷三块,
|
||||||
|
* 迁走后若仍在这里刷,就会用**试卷页的组别**去拉**成绩页的板块**,必然串组。
|
||||||
*/
|
*/
|
||||||
async function refreshQuestionData(force = false) {
|
async function refreshQuestionData(force = false) {
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([fetchQuestions(), ensureBigData(force)])
|
||||||
fetchQuestions(),
|
}
|
||||||
fetchExamSubmissions(),
|
|
||||||
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) ==================== */
|
/** ==================== 大题导出 Word(2026-09-23) ==================== */
|
||||||
@@ -2225,16 +2273,17 @@ async function saveBigScores() {
|
|||||||
}
|
}
|
||||||
bigScoreSaving.value = true
|
bigScoreSaving.value = true
|
||||||
try {
|
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_1: Number(bigScoreForm.score_1) || 0,
|
||||||
score_2: Number(bigScoreForm.score_2) || 0,
|
score_2: Number(bigScoreForm.score_2) || 0,
|
||||||
score_3: Number(bigScoreForm.score_3) || 0,
|
score_3: Number(bigScoreForm.score_3) || 0,
|
||||||
})
|
})
|
||||||
ElMessage.success(`评分已保存(大题合计 ${res?.total ?? bigScoreTotal.value} 分)`)
|
ElMessage.success(`评分已保存(大题合计 ${res?.total ?? bigScoreTotal.value} 分)`)
|
||||||
bigAnswerDialogVisible.value = false
|
bigAnswerDialogVisible.value = false
|
||||||
// 一次刷新全部相关列表:大题作答(含大题得分列)+ 考生答题记录(卷面分/大题分)。
|
// 一次刷新全部相关列表:成绩明细(总分 = 客观分 + 大题分)+ 大题作答 + 考生答题记录。
|
||||||
// 原实现只调 fetchBigAnswers(),切到「考生答题记录」仍是旧分数 ⇒ 测试人员只能刷整页。
|
// 原实现只调 fetchBigAnswers(),测试人员看到另一处仍是旧分只能刷整页。
|
||||||
await refreshQuestionData(true)
|
await refreshScorePageData(true)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e?.message || '评分保存失败')
|
ElMessage.error(e?.message || '评分保存失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2453,9 +2502,10 @@ const pagedExamSubmissions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function fetchExamSubmissions() {
|
async function fetchExamSubmissions() {
|
||||||
|
// R29:本板块已迁入「成绩管理」⇒ 组别跟随成绩页
|
||||||
examSubmissionsLoading.value = true
|
examSubmissionsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await adminApi.getSubmissions(questionGroup.value)
|
const data = await adminApi.getSubmissions(scoreExportGroup.value)
|
||||||
examSubmissions.value = Array.isArray(data) ? data : (data?.list || [])
|
examSubmissions.value = Array.isArray(data) ? data : (data?.list || [])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('获取答题记录失败', e)
|
console.warn('获取答题记录失败', e)
|
||||||
@@ -2481,7 +2531,8 @@ async function saveExamScore() {
|
|||||||
if (!examScoreDialogRow.value) return
|
if (!examScoreDialogRow.value) return
|
||||||
examScoreSaving.value = true
|
examScoreSaving.value = true
|
||||||
try {
|
try {
|
||||||
await adminApi.setScores(questionGroup.value, {
|
// R29:组别跟随「成绩管理」页
|
||||||
|
await adminApi.setScores(scoreExportGroup.value, {
|
||||||
scores: [{
|
scores: [{
|
||||||
contestantId: examScoreDialogRow.value.contestantId,
|
contestantId: examScoreDialogRow.value.contestantId,
|
||||||
// 客观分回传弹窗所见值,避免后端按答题记录重算导致与所见不一致
|
// 客观分回传弹窗所见值,避免后端按答题记录重算导致与所见不一致
|
||||||
@@ -2491,8 +2542,8 @@ async function saveExamScore() {
|
|||||||
})
|
})
|
||||||
ElMessage.success('评分已保存')
|
ElMessage.success('评分已保存')
|
||||||
examScoreDialogVisible.value = false
|
examScoreDialogVisible.value = false
|
||||||
// 同上:客观分 / 大题分同时影响两张表,必须一起刷新(原仅刷考生答题记录)
|
// 客观分 / 大题分同时影响成绩明细表与考生答题记录,必须一起刷新
|
||||||
await refreshQuestionData(true)
|
await refreshScorePageData(true)
|
||||||
} catch (e) { ElMessage.error('保存失败') } finally { examScoreSaving.value = false }
|
} catch (e) { ElMessage.error('保存失败') } finally { examScoreSaving.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2503,6 +2554,12 @@ watch(questionGroup, () => {
|
|||||||
if (activeMenu.value === 'questions') refreshQuestionData(true)
|
if (activeMenu.value === 'questions') refreshQuestionData(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// R29:成绩管理页切组 → 刷新成绩明细 + 两个由试卷管理迁入的板块
|
||||||
|
// (这两个板块的组别跟随 scoreExportGroup,故其刷新必须挂在这里)
|
||||||
|
watch(scoreExportGroup, () => {
|
||||||
|
if (activeMenu.value === 'scores') refreshExamCtxData(true)
|
||||||
|
})
|
||||||
|
|
||||||
// ==================== 3.5 备赛资料管理 ====================
|
// ==================== 3.5 备赛资料管理 ====================
|
||||||
const materialsGroup = ref('benke')
|
const materialsGroup = ref('benke')
|
||||||
const adminMaterials = ref([])
|
const adminMaterials = ref([])
|
||||||
@@ -2691,7 +2748,8 @@ async function batchSaveWorkScores() {
|
|||||||
ElMessage.success('评分保存成功')
|
ElMessage.success('评分保存成功')
|
||||||
// 保存后立即刷新作品列表 + 考生答题记录,保证两处分数同步可见(评分不同步修复)
|
// 保存后立即刷新作品列表 + 考生答题记录,保证两处分数同步可见(评分不同步修复)
|
||||||
await fetchSubmissions()
|
await fetchSubmissions()
|
||||||
if (submissionGroup.value === questionGroup.value) fetchExamSubmissions()
|
// R29:「考生答题记录」的组别已改跟随成绩管理页(scoreExportGroup)
|
||||||
|
if (submissionGroup.value === scoreExportGroup.value) fetchExamSubmissions()
|
||||||
} catch (e) { ElMessage.error('保存失败') }
|
} catch (e) { ElMessage.error('保存失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2750,6 +2808,12 @@ async function saveTimes() {
|
|||||||
// ==================== 6. 成绩管理 ====================
|
// ==================== 6. 成绩管理 ====================
|
||||||
const scoresPublishedMap = reactive({})
|
const scoresPublishedMap = reactive({})
|
||||||
const scoreExportGroup = ref('')
|
const scoreExportGroup = ref('')
|
||||||
|
/**
|
||||||
|
* 是否为「考试组」(中职 / 高职)——R29 由「试卷管理」迁入的两个板块
|
||||||
|
* (考生答题记录 / 考生大题作答查证)只对这两组有意义:
|
||||||
|
* 本科 / 教师组提交的是 Word / PPT 作品,没有客观题与综合大题数据。
|
||||||
|
*/
|
||||||
|
const isExamScoreGroup = computed(() => scoreExportGroup.value === 'zhongzhi' || scoreExportGroup.value === 'gaozhi')
|
||||||
const scoreList = ref([])
|
const scoreList = ref([])
|
||||||
const scoreListLoading = ref(false)
|
const scoreListLoading = ref(false)
|
||||||
const scoreListPage = ref(1)
|
const scoreListPage = ref(1)
|
||||||
@@ -2835,10 +2899,11 @@ async function togglePublish() {
|
|||||||
} catch (e) { ElMessage.error('操作失败') }
|
} catch (e) { ElMessage.error('操作失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportCsv() {
|
async function exportExcel() {
|
||||||
// safeDownload 自带「HTTP 状态 + content-type」双重校验,避免把错误 JSON 存成 .csv
|
// R29:由 CSV 改为直接导出 .xlsx(CSV 在 Excel 中中文易乱码、身份证/手机号变科学计数法)
|
||||||
await safeDownload(`/api/admin/export-csv/${scoreExportGroup.value}`, {
|
// safeDownload 自带「HTTP 状态 + content-type」双重校验,避免把错误 JSON 存成 .xlsx
|
||||||
fallbackName: `${scoreExportGroup.value}_scores.csv`,
|
await safeDownload(`/api/admin/export-excel/${scoreExportGroup.value}`, {
|
||||||
|
fallbackName: `${scoreExportGroup.value}_scores.xlsx`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3477,6 +3542,15 @@ onMounted(() => {
|
|||||||
align-items: center;
|
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; }
|
.search-input { width: 220px; }
|
||||||
.filter-select { width: 140px; }
|
.filter-select { width: 140px; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user