This commit is contained in:
2026-07-27 08:59:02 +08:00 Unverified
parent 22321abe23
commit 2f5c6af37c
12 changed files with 6281 additions and 212 deletions
+113 -23
View File
@@ -9,6 +9,7 @@ import {
Plus,
Promotion,
Refresh,
Search,
Upload,
} from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
@@ -31,6 +32,13 @@ const createDialog = ref(false)
const returnDialog = ref(false)
const termId = ref<string | undefined>()
const status = ref<string | undefined>()
const taskKeyword = ref('')
const taskPage = ref(1)
const taskPageSize = 12
const taskTotal = ref(0)
const recordKeyword = ref('')
const recordPage = ref(1)
const recordPageSize = 50
const createForm = reactive({
regularWeight: 30,
finalWeight: 70,
@@ -67,8 +75,7 @@ const itemsWeight = computed(() =>
const weightSum = computed(() =>
createForm.regularWeight + createForm.finalWeight + itemsWeight.value)
const completedCount = computed(() =>
detail.value?.records.filter((record: any) =>
record.totalScore != null || record.examStatus !== 'Normal').length ?? 0)
selectedTask.value?.sheet?.completedCount ?? 0)
const averageScore = computed(() => {
const scores = (detail.value?.records ?? [])
.map((record: any) => Number(record.totalScore))
@@ -78,11 +85,9 @@ const averageScore = computed(() => {
.toFixed(1)
})
const passRate = computed(() => {
const scores = (detail.value?.records ?? [])
.map((record: any) => Number(record.totalScore))
.filter((value: number) => Number.isFinite(value))
if (!scores.length) return '—'
return `${Math.round(scores.filter((value: number) => value >= 60).length / scores.length * 100)}%`
const completed = Number(selectedTask.value?.sheet?.completedCount ?? 0)
if (!completed) return '—'
return `${Math.round(Number(selectedTask.value?.sheet?.passedCount ?? 0) / completed * 100)}%`
})
const transcriptGpa = computed(() => {
const numeric = transcript.value.records.filter((record: any) => record.gradePoint != null)
@@ -121,7 +126,7 @@ function removeItem(index: number) {
createForm.items.splice(index, 1)
}
async function load() {
async function load(resetPage = false) {
loading.value = true
try {
if (isStudent.value) {
@@ -130,16 +135,23 @@ async function load() {
})).data
return
}
tasks.value = (await http.get('/grades/sheets', {
if (resetPage) taskPage.value = 1
const response = (await http.get('/grades/sheets', {
params: {
academicTermId: termId.value,
status: status.value,
keyword: taskKeyword.value.trim() || undefined,
page: taskPage.value,
pageSize: taskPageSize,
},
})).data
tasks.value = response.items
taskTotal.value = response.total
const preferred = tasks.value.find((item) => item.id === selectedTask.value?.id)
?? tasks.value[0]
if (preferred) await selectTask(preferred)
else {
if (preferred) {
selectedTask.value = preferred
if (preferred.sheet) await loadDetail()
} else {
selectedTask.value = null
detail.value = null
}
@@ -152,11 +164,24 @@ async function load() {
async function selectTask(task: any) {
selectedTask.value = task
recordPage.value = 1
recordKeyword.value = ''
detail.value = null
if (!task.sheet) return
await loadDetail()
}
async function loadDetail() {
if (!selectedTask.value?.sheet) return
detailLoading.value = true
try {
const response = (await http.get(`/grades/sheets/${task.sheet.id}`)).data
const response = (await http.get(`/grades/sheets/${selectedTask.value.sheet.id}`, {
params: {
recordPage: recordPage.value,
recordPageSize,
studentKeyword: recordKeyword.value.trim() || undefined,
},
})).data
detail.value = response.sheet
detail.value.canEdit = response.canEdit
detail.value.canReview = response.canReview
@@ -168,6 +193,16 @@ async function selectTask(task: any) {
}
}
async function changeTaskPage(page: number) {
taskPage.value = page
await load()
}
async function searchRecords() {
recordPage.value = 1
await loadDetail()
}
function openCreate() {
createForm.regularWeight = 30
createForm.finalWeight = 70
@@ -213,7 +248,7 @@ async function saveRecords() {
})),
})
ElMessage.success('成绩已保存并重新计算总评')
await selectTask(selectedTask.value)
await loadDetail()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
@@ -257,7 +292,7 @@ async function handleImport(event: Event) {
file,
)
ElMessage.success(`导入完成:已更新 ${result.data.updated} 条成绩记录`)
await selectTask(selectedTask.value)
await loadDetail()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
@@ -369,7 +404,7 @@ onMounted(async () => {
<p v-if="isStudent">查看学校已正式发布的课程成绩学分与绩点</p>
<p v-else>从教师登记学院复核到校级发布支持自定义平时实验实习等分项</p>
</div>
<el-button :icon="Refresh" @click="load">刷新</el-button>
<el-button :icon="Refresh" @click="load()">刷新</el-button>
</section>
<template v-if="isStudent">
@@ -381,7 +416,7 @@ onMounted(async () => {
</div>
<label>
<span>查看学期</span>
<el-select v-model="termId" clearable placeholder="全部学期" @change="load">
<el-select v-model="termId" clearable placeholder="全部学期" @change="load()">
<el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" :class="academicTermOptionClass(term)" />
</el-select>
</label>
@@ -421,13 +456,22 @@ onMounted(async () => {
<template v-else>
<section class="grade-toolbar">
<el-select v-model="termId" clearable placeholder="全部学期" @change="load">
<el-input
v-model="taskKeyword"
clearable
:prefix-icon="Search"
placeholder="搜索课程、课程号或教学班号"
@keyup.enter="load(true)"
@clear="load(true)"
/>
<el-select v-model="termId" clearable placeholder="全部学期" @change="load(true)">
<el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" :class="academicTermOptionClass(term)" />
</el-select>
<el-select v-model="status" clearable placeholder="全部状态" @change="load">
<el-select v-model="status" clearable placeholder="全部状态" @change="load(true)">
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value" />
</el-select>
<span> {{ tasks.length }} 个可管理教学班</span>
<el-button :icon="Search" @click="load(true)">查询</el-button>
<span> {{ taskTotal }} 个可管理教学班</span>
</section>
<section class="grade-workspace" v-loading="loading">
@@ -447,10 +491,20 @@ onMounted(async () => {
</i>
</button>
<el-empty v-if="!tasks.length" description="没有可管理的教学班" />
<el-pagination
v-if="taskTotal > taskPageSize"
class="grade-task-pagination"
small
layout="prev, pager, next"
:current-page="taskPage"
:page-size="taskPageSize"
:total="taskTotal"
@current-change="changeTaskPage"
/>
</aside>
<main class="grade-register" v-loading="detailLoading">
<el-empty v-if="!selectedTask" description="请选择教学班" />
<el-empty v-if="!selectedTask" description="从左侧选择一个教学班,进入对应成绩册" />
<section v-else-if="!selectedTask.sheet" class="grade-create-empty">
<el-icon><DocumentChecked /></el-icon>
<h3>{{ selectedTask.courseName }}</h3>
@@ -474,9 +528,9 @@ onMounted(async () => {
</header>
<section class="grade-register-ruler">
<div><span>学生数</span><b>{{ detail.records.length }}</b></div>
<div><span>学生数</span><b>{{ detail.recordTotal }}</b></div>
<div><span>已完成</span><b>{{ completedCount }}</b></div>
<div><span>平均</span><b>{{ averageScore }}</b></div>
<div><span>本页平均</span><b>{{ averageScore }}</b></div>
<div><span>及格率</span><b>{{ passRate }}</b></div>
<p>
平时 {{ detail.regularWeight }}%
@@ -493,6 +547,19 @@ onMounted(async () => {
show-icon
/>
<section class="grade-record-toolbar">
<el-input
v-model="recordKeyword"
clearable
:prefix-icon="Search"
placeholder="搜索姓名、学号或行政班"
@keyup.enter="searchRecords"
@clear="searchRecords"
/>
<el-button :icon="Search" @click="searchRecords">查找学生</el-button>
<span>每页最多显示 {{ recordPageSize }} 当前第 {{ detail.recordPage }} </span>
</section>
<div class="grade-table-scroll">
<el-table :data="detail.records" class="data-table grade-entry-table">
<el-table-column label="学生" fixed min-width="165">
@@ -565,6 +632,16 @@ onMounted(async () => {
</el-table-column>
</el-table>
</div>
<el-pagination
v-if="detail.recordTotal > recordPageSize"
class="grade-record-pagination"
background
layout="total, prev, pager, next"
:current-page="recordPage"
:page-size="recordPageSize"
:total="detail.recordTotal"
@current-change="(page: number) => { recordPage = page; loadDetail() }"
/>
<input ref="importFileInput" type="file" accept=".xlsx" style="display:none" @change="handleImport" />
@@ -688,4 +765,17 @@ onMounted(async () => {
.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; }
.grade-toolbar > .el-input { width: min(320px, 100%); }
.grade-task-pagination { justify-content: center; padding: 14px 8px; border-top: 1px solid var(--line); }
.grade-record-toolbar {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 12px 16px; border-bottom: 1px solid var(--line); background: #f7f9fb;
}
.grade-record-toolbar .el-input { width: min(300px, 100%); }
.grade-record-toolbar > span { margin-left: auto; color: var(--muted); font-size: 11px; }
.grade-record-pagination { justify-content: flex-end; padding: 14px 16px; border-top: 1px solid var(--line); }
@media (max-width: 760px) {
.grade-record-toolbar > span { width: 100%; margin-left: 0; }
.grade-record-pagination { justify-content: center; }
}
</style>