考试排考

This commit is contained in:
2026-07-25 14:51:17 +08:00 Unverified
parent 36049af692
commit 4e7da6709e
12 changed files with 1306 additions and 118 deletions
+77 -11
View File
@@ -45,6 +45,13 @@ const statusLabels: Record<string, string> = {
Published: '已发布',
Returned: '已退回',
}
const statusMeta: Record<string, { icon: string; description: string }> = {
Draft: { icon: '📝', description: '教师正在录入成绩' },
Submitted: { icon: '📤', description: '已提交,等待开课学院审核' },
Approved: { icon: '✅', description: '学院审核通过,待校级发布' },
Published: { icon: '📢', description: '成绩已向学生发布' },
Returned: { icon: '↩️', description: '学院退回,需教师修改后重新提交' },
}
const examStatusLabels: Record<string, string> = {
Normal: '正常',
Absent: '缺考',
@@ -281,6 +288,32 @@ function itemScore(record: any, gradeItemId: string) {
return record.itemScores?.find((itemScore: any) => itemScore.gradeItemId === gradeItemId)
}
/** Client-side preview calculation — mirrors backend GradeCalculator.CalculateTotal */
function calcPreviewTotal(record: any): number | null {
if (!detail.value) return null
if (record.examStatus !== 'Normal') return null
const { regularWeight, finalWeight, items } = detail.value
const itemWeightById: Record<string, number> = {}
for (const item of items ?? []) itemWeightById[item.id] = item.weight
// Check required scores are present
if (regularWeight > 0 && (record.regularScore == null || record.regularScore === '')) return null
if (finalWeight > 0 && (record.finalScore == null || record.finalScore === '')) return null
for (const itemScore of record.itemScores ?? []) {
const weight = itemWeightById[itemScore.gradeItemId]
if (weight > 0 && (itemScore.score == null || itemScore.score === '')) return null
}
let total = (Number(record.regularScore) || 0) * regularWeight / 100
for (const itemScore of record.itemScores ?? []) {
const weight = itemWeightById[itemScore.gradeItemId] || 0
total += (Number(itemScore.score) || 0) * weight / 100
}
total += (Number(record.finalScore) || 0) * finalWeight / 100
return Math.round(total * 10) / 10
}
onMounted(async () => {
try {
terms.value = (await http.get('/base-data/terms')).data
@@ -395,8 +428,14 @@ onMounted(async () => {
<span>{{ detail.courseCode }} · {{ detail.taskNumber }}</span>
<h3>{{ detail.courseName }}成绩登记册</h3>
<p>{{ detail.termName }} · {{ detail.teacherNames.join('、') }} · {{ detail.classNames.join('、') }}</p>
<p v-if="detail.courseCollegeName" class="college-meta">开课学院{{ detail.courseCollegeName }}</p>
</div>
<div class="status-badge-group">
<i :class="detail.status.toLowerCase()">{{ statusLabels[detail.status] }}</i>
<small v-if="detail.status === 'Submitted' && detail.needsCollegeReview">
{{ detail.needsCollegeReview }} 审核
</small>
</div>
<i :class="detail.status.toLowerCase()">{{ statusLabels[detail.status] }}</i>
</header>
<section class="grade-register-ruler">
@@ -465,9 +504,14 @@ onMounted(async () => {
<span v-else>{{ row.finalScore ?? '—' }}</span>
</template>
</el-table-column>
<el-table-column label="总评" width="80">
<el-table-column label="总评" width="110">
<template #default="{ row }">
<b class="total-score" :class="scoreClass(row.totalScore)">{{ row.totalScore ?? '—' }}</b>
<div class="total-score-cell">
<b class="total-score" :class="scoreClass(row.totalScore)">{{ row.totalScore ?? '—' }}</b>
<span v-if="detail.canEdit && row.examStatus === 'Normal'" class="preview-score" :class="scoreClass(calcPreviewTotal(row))">
参考 {{ calcPreviewTotal(row) ?? '—' }}
</span>
</div>
</template>
</el-table-column>
<el-table-column label="考试状态" width="115">
@@ -488,15 +532,20 @@ onMounted(async () => {
</div>
<footer class="grade-actions">
<div>
<span>当前流程</span>
<b>{{ statusLabels[detail.status] }}</b>
<div class="workflow-status">
<span>{{ statusMeta[detail.status]?.icon }} {{ statusLabels[detail.status] }}</span>
<small>{{ statusMeta[detail.status]?.description }}</small>
<small v-if="detail.needsCollegeReview && (detail.status === 'Submitted' || detail.status === 'Approved')" class="college-hint">
开课学院:{{ detail.needsCollegeReview }}
</small>
</div>
<div class="grade-actions-buttons">
<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>
<el-button v-if="detail.canReview" type="success" :icon="Check" @click="approveSheet">审核通过</el-button>
<el-button v-if="detail.canPublish" type="primary" :icon="Promotion" @click="publishSheet">发布成绩</el-button>
</div>
<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>
<el-button v-if="detail.canReview" type="success" :icon="Check" @click="approveSheet">审核通过</el-button>
<el-button v-if="detail.canPublish" type="primary" :icon="Promotion" @click="publishSheet">发布成绩</el-button>
</footer>
</template>
</main>
@@ -581,4 +630,21 @@ onMounted(async () => {
.item-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #eef5f0; border-radius: 4px; }
.item-row > span:first-child { flex: 1; font-size: 13px; font-weight: 650; }
.add-item-row { display: flex; align-items: center; gap: 8px; }
/* Preview score */
.total-score-cell { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.preview-score { font-size: 10px; opacity: .7; white-space: nowrap; }
.preview-score.failed { color: #b34e48; }
.preview-score.excellent { color: #2d8975; }
/* College review */
.college-meta { color: var(--muted); font-size: 12px; margin-top: 2px; }
.status-badge-group { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.status-badge-group small { font-size: 11px; color: var(--muted); }
.workflow-status { display: flex; flex-direction: column; gap: 2px; }
.workflow-status span { font-weight: 650; font-size: 13px; }
.workflow-status small { color: var(--muted); font-size: 11px; }
.workflow-status .college-hint { color: #6b4e16; font-weight: 600; }
.grade-actions { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 16px; border-top: 1px solid #e4e7ed; background: #fafbfc; }
.grade-actions-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
</style>