成绩册 Excel 导入/导出 — 已完成

  后端 (GradesController.cs)

  GET /api/grades/sheets/{id}/template — 下载成绩导入模板
  - 根据成绩单的动态分项(GradeItems)生成对应的 Excel 列:学号 | 姓名 | 班级 | 平时成绩 | [分项1] | [分项2] | ... | 期末成绩 | 考试状态 | 备注
  - 预填已有学生信息和已有成绩
  - 附带填写说明工作表

  POST /api/grades/sheets/{id}/import — 从 Excel 导入成绩
  - 按学号匹配学生,批量更新平时成绩、分项成绩、期末成绩、考试状态、备注
  - 支持留空(不覆盖已有成绩)、0-100 范围校验、考试状态校验(正常/缺考/缓考/免修)
  - 导入后自动重新计算总评成绩和绩点
  - 仅限 Draft/Returned 状态的成绩单,仅限有编辑权限的用户
  - 错误时整批回滚,最多显示 50 条错误详情

  前端 (GradesView.vue)

  在成绩登记册底部操作栏新增两个按钮(仅在 canEdit 时显示):
  - 下载模板 — 调用 downloadApiFile 下载当前成绩单的 Excel 模板
  - Excel导入 — 通过隐藏 file input 选择 .xlsx 文件,调用 importExcel 上传

  工作流程

  1. 教师打开成绩单 → 点击「下载模板」
  2. 在 Excel 中填写/修改学生成绩
  3. 点击「Excel导入」选择文件上传
  4. 系统自动校验、合并成绩、重新计算总评 → 提示更新条数
  5. 成绩表刷新显示最新数据

✻ Crunched for 5m 50s
This commit is contained in:
2026-07-25 17:57:56 +08:00 Unverified
parent 97691cb203
commit 15a34a306c
2 changed files with 262 additions and 0 deletions
+38
View File
@@ -4,12 +4,15 @@ import {
Check,
Delete,
DocumentChecked,
Download,
EditPen,
Plus,
Promotion,
Refresh,
Upload,
} from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
import { downloadApiFile, importExcel } from '../api/excel'
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
@@ -230,6 +233,37 @@ async function submitSheet() {
}
}
const importFileInput = ref<HTMLInputElement>()
function downloadTemplate() {
downloadApiFile(
`/grades/sheets/${detail.value.id}/template`,
'成绩导入模板.xlsx',
)
}
function chooseImportFile() {
importFileInput.value?.click()
}
async function handleImport(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
try {
const result = await importExcel(
`/grades/sheets/${detail.value.id}/import`,
file,
)
ElMessage.success(`导入完成:已更新 ${result.data.updated} 条成绩记录`)
await selectTask(selectedTask.value)
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
input.value = ''
}
}
async function approveSheet() {
try {
await http.post(`/grades/sheets/${detail.value.id}/approve`)
@@ -531,6 +565,8 @@ onMounted(async () => {
</el-table>
</div>
<input ref="importFileInput" type="file" accept=".xlsx" style="display:none" @change="handleImport" />
<footer class="grade-actions">
<div class="workflow-status">
<span>{{ statusMeta[detail.status]?.icon }} {{ statusLabels[detail.status] }}</span>
@@ -540,6 +576,8 @@ onMounted(async () => {
</small>
</div>
<div class="grade-actions-buttons">
<el-button v-if="detail.canEdit" :icon="Download" @click="downloadTemplate">下载模板</el-button>
<el-button v-if="detail.canEdit" :icon="Upload" @click="chooseImportFile">Excel导入</el-button>
<el-button v-if="detail.canEdit" :icon="EditPen" @click="saveRecords">保存成绩</el-button>
<el-button v-if="detail.canEdit" type="primary" :icon="Promotion" @click="submitSheet">提交审核</el-button>
<el-button v-if="detail.canReview" type="danger" plain @click="openReturn">退回修改</el-button>