成绩录入页面优化

This commit is contained in:
biss committed 2026-08-15 18:57:39 +08:00
1 parent 53bee58a84
commit 7124011da9
3 files changed
+234 -54

No files matched your search

+82 -34
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import {
Check,
Download,
@@ -19,8 +19,10 @@ import {
academicTermOptionClass,
defaultAcademicTermId,
} from '../utils/academicTerms'
import { useScoreGridNavigation } from '../utils/scoreGridNavigation'
const auth = useAuthStore()
const { handleScoreGridKeydown } = useScoreGridNavigation('experiment-grade-entry')
const isStudent = computed(() => auth.user?.roles.includes('Student') ?? false)
const canViewAllColleges = computed(() =>
auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false,
@@ -28,6 +30,10 @@ const canViewAllColleges = computed(() =>
const loading = ref(false)
const detailLoading = ref(false)
const saving = ref(false)
const autoSaveState = ref<'idle' | 'saving' | 'saved' | 'error'>('idle')
let autoSaveTimer: ReturnType<typeof setTimeout> | undefined
let savedRecordSnapshot = ''
let autoSaving = false
const terms = ref<any[]>([])
const colleges = ref<any[]>([])
const termId = ref('')
@@ -252,6 +258,7 @@ async function loadDetail() {
detail.value.canEdit = response.canEdit
detail.value.canReview = response.canReview
detail.value.canPublish = response.canPublish
savedRecordSnapshot = experimentRecordSnapshot()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
@@ -276,36 +283,67 @@ function previewTotal(record: any): number | null {
return Math.round(total * 10) / 10
}
async function saveRecords() {
function experimentRecordPayload() {
return detail.value.records.map((record: any) => ({
id: record.id,
participationStatus: record.participationStatus,
safetyViolation: record.safetyViolation,
attemptNumber: record.attemptNumber,
submissionReference: record.submissionReference,
submittedAt: record.submittedAt,
isLate: record.isLate,
teacherComment: record.teacherComment,
itemScores: record.itemScores.map((score: any) => ({
experimentGradeItemId: score.experimentGradeItemId,
score: score.score,
comment: score.comment,
})),
}))
}
function experimentRecordSnapshot() {
return detail.value ? JSON.stringify(experimentRecordPayload()) : ''
}
function scheduleAutoSave() {
if (!detail.value?.canEdit || experimentRecordSnapshot() === savedRecordSnapshot) return
autoSaveState.value = 'idle'
clearTimeout(autoSaveTimer)
autoSaveTimer = setTimeout(() => { void saveRecords(true) }, 60_000)
}
async function saveRecords(automatic = false) {
if (!detail.value) return
saving.value = true
if (automatic && autoSaving) return
const records = experimentRecordPayload()
const snapshot = JSON.stringify(records)
if (automatic) {
autoSaving = true
autoSaveState.value = 'saving'
} else saving.value = true
try {
await http.put(`/experiment-grades/sheets/${detail.value.id}/records`, {
records: detail.value.records.map((record: any) => ({
id: record.id,
participationStatus: record.participationStatus,
safetyViolation: record.safetyViolation,
attemptNumber: record.attemptNumber,
submissionReference: record.submissionReference,
submittedAt: record.submittedAt,
isLate: record.isLate,
teacherComment: record.teacherComment,
itemScores: record.itemScores.map((score: any) => ({
experimentGradeItemId: score.experimentGradeItemId,
score: score.score,
comment: score.comment,
})),
})),
records,
})
ElMessage.success('本页实验成绩已保存')
await loadDetail()
savedRecordSnapshot = snapshot
if (automatic) autoSaveState.value = 'saved'
else {
ElMessage.success('本页实验成绩已保存')
await loadDetail()
}
} catch (error) {
ElMessage.error(apiErrorMessage(error))
if (automatic) autoSaveState.value = 'error'
else ElMessage.error(apiErrorMessage(error))
} finally {
saving.value = false
if (automatic) {
autoSaving = false
if (experimentRecordSnapshot() !== savedRecordSnapshot) scheduleAutoSave()
} else saving.value = false
}
}
watch(() => detail.value?.records, scheduleAutoSave, { deep: true })
function downloadTemplate() {
if (!detail.value) return
downloadApiFile(
@@ -788,17 +826,24 @@ onMounted(async () => {
width="125"
>
<template #default="{ row }">
<el-input-number
v-if="detail.canEdit && ['Completed', 'Makeup'].includes(row.participationStatus)"
:model-value="itemScore(row, item.id)?.score"
:min="0"
:max="100"
:step="0.5"
:controls="false"
size="small"
@update:model-value="(value: number | undefined) => { const target = itemScore(row, item.id); if (target) target.score = value }"
/>
<span v-else>{{ itemScore(row, item.id)?.score ?? '—' }}</span>
<div
data-score-grid="experiment-grade-entry"
:data-score-row="detail.records.indexOf(row)"
:data-score-column="detail.items.indexOf(item)"
@keydown="handleScoreGridKeydown"
>
<el-input-number
v-if="detail.canEdit && ['Completed', 'Makeup'].includes(row.participationStatus)"
:model-value="itemScore(row, item.id)?.score"
:min="0"
:max="100"
:step="0.5"
:controls="false"
size="small"
@update:model-value="(value: number | undefined) => { const target = itemScore(row, item.id); if (target) target.score = value }"
/>
<span v-else>{{ itemScore(row, item.id)?.score ?? '—' }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="安全违规" width="105">
@@ -839,11 +884,14 @@ onMounted(async () => {
<div>
<b v-if="detail.reviewComment">审核意见:{{ detail.reviewComment }}</b>
<span>开课学院:{{ detail.courseCollegeName }}</span>
<small v-if="detail.canEdit" class="autosave-state">
{{ autoSaveState === 'saving' ? '正在自动保存…' : autoSaveState === 'saved' ? '已自动保存' : autoSaveState === 'error' ? '自动保存失败,请手动保存' : '停止编辑 1 分钟后自动保存' }}
</small>
</div>
<div>
<el-button v-if="detail.canEdit" :icon="Download" @click="downloadTemplate">下载模板</el-button>
<el-button v-if="detail.canEdit" :icon="Upload" :loading="saving" @click="chooseImportFile">Excel 导入</el-button>
<el-button v-if="detail.canEdit" :icon="EditPen" :loading="saving" @click="saveRecords">保存本页</el-button>
<el-button v-if="detail.canEdit" :icon="EditPen" :loading="saving" @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="returnSheet">退回修改</el-button>
<el-button v-if="detail.canReview" type="success" :icon="Check" @click="approveSheet">审核通过</el-button>