调课
This commit is contained in:
+113
-23
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user