课程修读名单导出
This commit is contained in:
1 parent
0ce758191e
commit
4575758812
5 files changed
+301
-46
No files matched your search
@@ -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>
|
||||
@@ -25,7 +25,6 @@ const importing = ref(false)
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const activationDialog = ref(false)
|
||||
const activationLoading = ref(false)
|
||||
@@ -185,17 +184,6 @@ async function load() {
|
||||
const { data } = await http.get<PageResult>(`/personnel/${active.value}`, { params })
|
||||
rows.value = data.items
|
||||
total.value = data.total
|
||||
selectedRows.value = []
|
||||
if (active.value === 'students' && rows.value.length) {
|
||||
const ids = rows.value.map((row) => row.id)
|
||||
const { data: summaries } = await http.get('/personnel/students/current-courses', {
|
||||
params: { studentIds: ids },
|
||||
})
|
||||
const byId = new Map(summaries.map((item: any) => [item.id, item]))
|
||||
rows.value = rows.value.map((row) => ({ ...row, ...(byId.get(row.id) ?? {
|
||||
currentCourses: [], totalCredits: 0,
|
||||
}) }))
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
@@ -269,28 +257,6 @@ async function exportRows() {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportStudentCurrentCourses(selectedOnly = false) {
|
||||
if (selectedOnly && selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先勾选需要导出的学生。')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await downloadApiFile(
|
||||
'/personnel/students/current-courses/export',
|
||||
'学生正在修读课程.xlsx',
|
||||
{ params: { ...exportParams(), studentIds: selectedOnly
|
||||
? selectedRows.value.map((row) => row.id) : undefined } },
|
||||
)
|
||||
ElMessage.success(selectedOnly ? `已导出所选 ${selectedRows.value.length} 名学生` : '已导出筛选学生的修读课程')
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChange(selection: any[]) {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
function chooseImportFile() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
@@ -491,14 +457,9 @@ watch(active, async () => {
|
||||
/>
|
||||
</template>
|
||||
<el-button v-if="canExport" :icon="Download" @click="exportRows">导出结果</el-button>
|
||||
<template v-if="active === 'students' && canExport">
|
||||
<el-button :icon="Download" type="primary" @click="exportStudentCurrentCourses()">导出修读课程</el-button>
|
||||
<el-button :icon="Download" :disabled="selectedRows.length === 0" @click="exportStudentCurrentCourses(true)">导出所选({{ selectedRows.length }})</el-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" class="data-table registry-table" @selection-change="onSelectionChange">
|
||||
<el-table-column v-if="active === 'students'" type="selection" width="48" />
|
||||
<el-table v-loading="loading" :data="rows" class="data-table registry-table">
|
||||
<el-table-column :label="numberLabel" width="135">
|
||||
<template #default="{ row }"><span class="registry-number">{{ active === 'teachers' ? row.teacherNumber : row.studentNumber }}</span></template>
|
||||
</el-table-column>
|
||||
@@ -520,12 +481,6 @@ watch(active, async () => {
|
||||
<el-table-column v-if="active === 'students'" prop="className" label="行政班" min-width="155" />
|
||||
<el-table-column v-if="active === 'students'" prop="majorName" label="专业" min-width="170" />
|
||||
<el-table-column v-if="active === 'students'" prop="enrollmentYear" label="入学年级" width="105" />
|
||||
<el-table-column v-if="active === 'students'" label="正在修读课程 / 学分" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.currentCourses?.length">{{ row.currentCourses.map((course: any) => `${course.courseName}(${course.credits}学分)`).join(';') }}</span>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="active === 'students'" label="总学分" width="95">
|
||||
<template #default="{ row }">{{ row.totalCredits ?? 0 }}</template>
|
||||
</el-table-column>
|
||||
|
||||
Reference in new issue
Block a user