课程修读名单导出

This commit is contained in:
biss committed 2026-08-14 19:45:19 +08:00
1 parent 0ce758191e
commit 4575758812
5 files changed
+301 -46

No files matched your search

+4
View File
@@ -167,6 +167,10 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
label: isTeachingAdmin.value ? '选课管理' : '学生选课',
},
),
...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin']),
{ path: '/course-enrollments', label: '修读课程管理' },
),
...whenVisible(
hasAnyRole(['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher']),
{
+6
View File
@@ -251,6 +251,12 @@ const router = createRouter({
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'],
},
},
{
path: 'course-enrollments',
name: 'course-enrollments',
component: () => import('../views/CourseEnrollmentManagementView.vue'),
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'] },
},
{
path: 'grades',
name: 'grades',
@@ -0,0 +1,150 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Download, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import http, { apiErrorMessage } from '../api/http'
import { downloadApiFile } from '../api/excel'
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
const router = useRouter()
const rows = ref<any[]>([])
const total = ref(0)
const terms = ref<any[]>([])
const courses = ref<any[]>([])
const loading = ref(false)
const query = reactive({
academicTermId: undefined as string | undefined,
courseId: undefined as string | undefined,
status: undefined as string | undefined,
keyword: '',
page: 1,
pageSize: 20,
})
const canAdjust = computed(() => auth.user?.roles.some((role) =>
['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false)
const statusLabels: Record<string, string> = {
Enrolled: '已修读', Waitlisted: '候补中', Withdrawn: '已退选',
Cancelled: '已取消', Expired: '已失效',
}
const exemptionLabels: Record<string, string> = {
Submitted: '审核中', Approved: '已批准', Rejected: '已驳回',
}
function params() {
return {
...query,
keyword: query.keyword.trim() || undefined,
}
}
async function load(page = query.page) {
query.page = page
loading.value = true
try {
const { data } = await http.get('/course-selections/enrollments', { params: params() })
rows.value = data.items
total.value = data.total
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
loading.value = false
}
}
async function exportRows() {
try {
await downloadApiFile('/course-selections/enrollments/export.xlsx', '修读课程名单.xlsx', {
params: params(),
})
ElMessage.success(`已导出符合条件的全部 ${total.value} 条修读记录`)
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
async function withdraw(row: any) {
try {
await ElMessageBox.confirm(
`确认将 ${row.studentNumber} ${row.studentName} 从《${row.courseName}》移出吗?系统会按原有规则处理候补递补。`,
'退选处理', { type: 'warning', confirmButtonText: '确认退选', cancelButtonText: '取消' },
)
await http.delete(`/course-selections/offerings/${row.courseSelectionOfferingId}/admin-enrollments/${row.id}`)
ElMessage.success('已完成退选处理')
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
}
}
onMounted(async () => {
try {
const [termResponse, courseResponse] = await Promise.all([
http.get('/base-data/terms'), http.get('/courses/options'),
])
terms.value = termResponse.data
courses.value = courseResponse.data
await load(1)
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
})
</script>
<template>
<div class="page-container enrollment-management">
<section class="page-intro">
<div>
<span class="section-kicker">ENROLLMENT LEDGER</span>
<h2>修读课程管理</h2>
<p>集中查询跨学期修读记录、批量导出课程名单;免修仍通过审批中心按既有流程办理。</p>
</div>
<div class="intro-actions">
<el-button :icon="Refresh" @click="load(1)">刷新</el-button>
<el-button plain @click="router.push('/approvals')">处理免修审批</el-button>
<el-button type="primary" :icon="Download" @click="exportRows">导出全部结果</el-button>
</div>
</section>
<section class="data-card">
<div class="filter-bar">
<el-select v-model="query.academicTermId" clearable filterable placeholder="全部学期" @change="load(1)">
<el-option v-for="term in terms" :key="term.id" :label="term.name" :value="term.id" />
</el-select>
<el-select v-model="query.courseId" clearable filterable placeholder="全部课程" @change="load(1)">
<el-option v-for="course in courses" :key="course.id" :label="`${course.code} · ${course.name}`" :value="course.id" />
</el-select>
<el-select v-model="query.status" clearable placeholder="全部状态" @change="load(1)">
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value" />
</el-select>
<el-input v-model="query.keyword" clearable placeholder="课程、教学班、学号或姓名" @keyup.enter="load(1)" @clear="load(1)" />
<el-button type="primary" :icon="Search" @click="load(1)">查询</el-button>
</div>
<p class="result-note">导出不受当前页限制,将按上述条件导出全部 {{ total }} 条记录。</p>
<el-table v-loading="loading" :data="rows" row-key="id">
<el-table-column prop="termName" label="学期" width="120" />
<el-table-column label="课程 / 教学班" min-width="230">
<template #default="{ row }"><b>{{ row.courseName }}</b><small>{{ row.courseCode }} · {{ row.taskNumber }}</small></template>
</el-table-column>
<el-table-column label="学生" min-width="150"><template #default="{ row }">{{ row.studentNumber }}<small>{{ row.studentName }} · {{ row.className }}</small></template></el-table-column>
<el-table-column prop="collegeName" label="开课院系" min-width="120" />
<el-table-column label="修读状态" width="105"><template #default="{ row }"><el-tag size="small" :type="row.status === 'Enrolled' ? 'success' : 'info'">{{ statusLabels[row.status] ?? row.status }}</el-tag></template></el-table-column>
<el-table-column label="免修" width="100"><template #default="{ row }"><el-tag v-if="row.exemptionStatus" size="small" :type="row.exemptionStatus === 'Approved' ? 'success' : 'warning'">{{ exemptionLabels[row.exemptionStatus] }}</el-tag><span v-else>—</span></template></el-table-column>
<el-table-column v-if="canAdjust" label="操作" width="90" fixed="right"><template #default="{ row }"><el-button v-if="row.status === 'Enrolled'" link type="danger" @click="withdraw(row)">退选</el-button></template></el-table-column>
<template #empty><el-empty description="没有符合条件的修读记录" /></template>
</el-table>
<el-pagination v-if="total" background layout="total, sizes, prev, pager, next" :current-page="query.page" :page-size="query.pageSize" :page-sizes="[20, 50, 100]" :total="total" @current-change="load" @size-change="(size: number) => { query.pageSize = size; load(1) }" />
</section>
</div>
</template>
<style scoped>
.intro-actions,.filter-bar { display:flex; flex-wrap:wrap; gap:8px; }
.filter-bar { margin-bottom:10px; }
.filter-bar .el-select { width:180px; }.filter-bar .el-input { width:240px; }
.result-note { margin:0 0 14px; color:var(--muted); font-size:13px; }
small { display:block; margin-top:3px; color:var(--muted); }
:deep(.el-pagination) { justify-content:flex-end; margin-top:16px; }
@media (max-width:640px) { .intro-actions,.filter-bar { align-items:stretch; flex-direction:column; }.filter-bar .el-select,.filter-bar .el-input { width:100%; } }
</style>
Loaded 3 of 5 files, more files were not shown because too many files have changed in this diff. Show more