选课名单/课程修读情况
This commit is contained in:
1 parent
4575758812
commit
ef3fbf92f0
7 files changed
+241
-139
No files matched your search
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
Check,
|
||||
Clock,
|
||||
@@ -49,6 +50,7 @@ interface SubstitutionTarget {
|
||||
}
|
||||
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
const isManager = computed(() => auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(r)) ?? false)
|
||||
const isTeacher = computed(() => auth.user?.roles.includes('Teacher') && !isManager.value)
|
||||
const isStudent = computed(() => auth.user?.roles.includes('Student') && !isManager.value)
|
||||
@@ -92,6 +94,8 @@ const dialogTitle = computed(() => ({
|
||||
substitution: '发起课程替代申请',
|
||||
}[dialogType.value] ?? '发起申请'))
|
||||
|
||||
const actionableApprovalTypes = ['CourseExemption', 'DeferredExam', 'CourseSubstitution', 'CourseAdjustment']
|
||||
|
||||
const selectedCourse = computed(() =>
|
||||
enrolledCourses.value.find(course => course.teachingTaskId === form.teachingTaskId))
|
||||
|
||||
@@ -278,7 +282,7 @@ async function submitForm() {
|
||||
|
||||
async function approveItem(item: any) {
|
||||
try {
|
||||
const ep = `/approvals/${item.type === 'CourseExemption' ? 'exemptions' : item.type === 'DeferredExam' ? 'deferred' : item.type === 'CourseSubstitution' ? 'substitutions' : ''}`
|
||||
const ep = approvalEndpoint(item.type)
|
||||
if (ep) {
|
||||
await http.post(`${ep}/${item.id}/approve`)
|
||||
ElMessage.success('已通过')
|
||||
@@ -298,7 +302,7 @@ function openReject(item: any) {
|
||||
async function rejectSubmit() {
|
||||
if (!rejectItem.value) return
|
||||
try {
|
||||
const ep = `/approvals/${rejectItem.value.type === 'CourseExemption' ? 'exemptions' : rejectItem.value.type === 'DeferredExam' ? 'deferred' : rejectItem.value.type === 'CourseSubstitution' ? 'substitutions' : ''}`
|
||||
const ep = approvalEndpoint(rejectItem.value.type)
|
||||
if (ep) {
|
||||
await http.post(`${ep}/${rejectItem.value.id}/reject`, { comment: rejectComment.value })
|
||||
rejectDialog.value = false
|
||||
@@ -310,6 +314,14 @@ async function rejectSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
function approvalEndpoint(type: string) {
|
||||
if (type === 'CourseAdjustment') return '/course-adjustments'
|
||||
const segment = type === 'CourseExemption' ? 'exemptions'
|
||||
: type === 'DeferredExam' ? 'deferred'
|
||||
: type === 'CourseSubstitution' ? 'substitutions' : ''
|
||||
return segment ? `/approvals/${segment}` : ''
|
||||
}
|
||||
|
||||
function statusText(status: string) {
|
||||
return status === 'Submitted' ? '待审核' : status === 'Approved' ? '已通过' : '已驳回'
|
||||
}
|
||||
@@ -323,7 +335,10 @@ function courseScheduleText(course: CourseOption) {
|
||||
return course.hasPublishedSchedule ? '课表已发布' : '等待课表发布'
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
onMounted(() => {
|
||||
if (isManager.value && route.query.tab === 'pending') tab.value = 'pending'
|
||||
return load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -331,8 +346,8 @@ onMounted(load)
|
||||
<section class="page-intro">
|
||||
<div>
|
||||
<span class="section-kicker">APPROVAL CENTER</span>
|
||||
<h2>审批中心</h2>
|
||||
<p>{{ isStudent ? '从实际修读课程发起申请,查看审核进度与处理结果。' : '集中处理教务申请与审核任务。' }}</p>
|
||||
<h2>审批工作台</h2>
|
||||
<p>{{ isStudent ? '从实际修读课程发起申请,查看审核进度与处理结果。' : isTeacher ? '从一个入口发起教学申请,集中追踪处理进度。' : '集中处理全部教务申请与审核任务。' }}</p>
|
||||
</div>
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
</section>
|
||||
@@ -367,6 +382,27 @@ onMounted(load)
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section v-if="isTeacher" class="application-launcher teacher-launcher">
|
||||
<button class="launch-card adjustment" type="button" @click="$router.push({ path: '/course-adjustments', query: { create: '1', from: 'approvals' } })">
|
||||
<span class="launch-icon"><Switch /></span>
|
||||
<span class="launch-copy">
|
||||
<small>COURSE ADJUSTMENT</small>
|
||||
<b>发起调停课</b>
|
||||
<em>调课、停课、补课、代课统一从审批工作台发起</em>
|
||||
</span>
|
||||
<span class="launch-meta">填写申请 <Plus /></span>
|
||||
</button>
|
||||
<button class="launch-card grade-modification" type="button" @click="openDialog('gradeMod')">
|
||||
<span class="launch-icon"><DocumentChecked /></span>
|
||||
<span class="launch-copy">
|
||||
<small>GRADE MODIFICATION</small>
|
||||
<b>发起成绩修改</b>
|
||||
<em>提交成绩修正依据,进入同一审批队列</em>
|
||||
</span>
|
||||
<span class="launch-meta">填写申请 <Plus /></span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div class="approval-tabs">
|
||||
<el-segmented
|
||||
v-model="tab"
|
||||
@@ -375,7 +411,6 @@ onMounted(load)
|
||||
{ label: `我的记录${isStudent ? ` (${recordCount})` : ''}`, value: 'mine' },
|
||||
]"
|
||||
/>
|
||||
<el-button v-if="isTeacher" :icon="Plus" type="primary" @click="openDialog('gradeMod')">成绩修改</el-button>
|
||||
</div>
|
||||
|
||||
<section v-if="tab === 'pending'" v-loading="loading" class="appr-list">
|
||||
@@ -386,7 +421,7 @@ onMounted(load)
|
||||
<p>{{ item.desc }}</p>
|
||||
<small>{{ item.college }} · {{ new Date(item.time).toLocaleString('zh-CN') }}</small>
|
||||
</div>
|
||||
<div v-if="['CourseExemption','DeferredExam','CourseSubstitution'].includes(item.type)" class="appr-actions">
|
||||
<div v-if="actionableApprovalTypes.includes(item.type)" class="appr-actions">
|
||||
<el-button size="small" type="danger" plain :icon="Close" @click="openReject(item)">驳回</el-button>
|
||||
<el-button size="small" type="success" :icon="Check" @click="approveItem(item)">通过</el-button>
|
||||
</div>
|
||||
@@ -395,7 +430,7 @@ onMounted(load)
|
||||
size="small"
|
||||
text
|
||||
type="primary"
|
||||
@click="$router.push(item.type==='StudentStatusChange'?'/student-status-changes':item.type==='CourseAdjustment'?'/course-adjustments':item.type==='GradeSheet'?'/grades':'/teacher-attendance')"
|
||||
@click="$router.push(item.type==='StudentStatusChange'?'/student-status-changes':item.type==='GradeSheet'?'/grades':'/teacher-attendance')"
|
||||
>
|
||||
查看 →
|
||||
</el-button>
|
||||
@@ -668,6 +703,9 @@ onMounted(load)
|
||||
.launch-icon svg, .dialog-lead-icon svg { width: 21px; }
|
||||
.deferred .launch-icon, .dialog-lead-icon.deferred { color: #a85d00; background: #fff3dc; }
|
||||
.substitution .launch-icon { color: #087f5b; background: #e8f8f1; }
|
||||
.adjustment .launch-icon { color: #7c3aed; background: #f1eafe; }
|
||||
.grade-modification .launch-icon { color: #0d7490; background: #e1f5f8; }
|
||||
.teacher-launcher { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.launch-copy { display: grid; gap: 3px; min-width: 0; }
|
||||
.launch-copy small { color: #8791a3; font: 600 9px/1.2 ui-monospace, monospace; letter-spacing: .1em; }
|
||||
.launch-copy b { margin-top: 3px; font-size: 17px; }
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Check, Plus, Refresh } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { academicTermLabel, academicTermOptionClass, defaultAcademicTermId } from '../utils/academicTerms'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const isManager = computed(() =>
|
||||
auth.user?.roles.some(r => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(r)) ?? false)
|
||||
const isTeacher = computed(() => auth.user?.roles.includes('Teacher') && !isManager.value)
|
||||
@@ -246,6 +249,10 @@ onMounted(async () => {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
await load()
|
||||
if (isTeacher.value && route.query.create === '1') {
|
||||
openCreate()
|
||||
await router.replace({ path: '/course-adjustments' })
|
||||
}
|
||||
})
|
||||
|
||||
function changeType() {
|
||||
|
||||
@@ -7,132 +7,92 @@ import http, { apiErrorMessage } from '../api/http'
|
||||
import { downloadApiFile } from '../api/excel'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
type ViewMode = 'course' | 'student'
|
||||
const auth = useAuthStore()
|
||||
const router = useRouter()
|
||||
const mode = ref<ViewMode>('course')
|
||||
const rows = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const terms = ref<any[]>([])
|
||||
const courses = ref<any[]>([])
|
||||
const colleges = ref<any[]>([])
|
||||
const majors = ref<any[]>([])
|
||||
const classes = 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,
|
||||
academicTermId: undefined as string | undefined, courseId: undefined as string | undefined,
|
||||
collegeId: undefined as string | undefined, majorId: undefined as string | undefined,
|
||||
administrativeClassId: undefined as string | undefined, grade: undefined as number | undefined,
|
||||
enrollmentYear: undefined as number | undefined, status: undefined as string | undefined,
|
||||
enrollmentType: undefined as string | undefined, exemptionStatus: 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,
|
||||
}
|
||||
}
|
||||
|
||||
const canAdjust = computed(() => auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false)
|
||||
const scopedCollegeId = computed(() => auth.user?.roles.includes('CollegeAdmin') ? auth.user.collegeId : undefined)
|
||||
const availableColleges = computed(() => scopedCollegeId.value ? colleges.value.filter((x) => x.id === scopedCollegeId.value) : colleges.value)
|
||||
const availableMajors = computed(() => majors.value.filter((x) => (!query.collegeId || x.collegeId === query.collegeId) && (!scopedCollegeId.value || x.collegeId === scopedCollegeId.value)))
|
||||
const availableClasses = computed(() => classes.value.filter((x) => (!query.majorId || x.majorId === query.majorId) && (!query.collegeId || availableMajors.value.some((major) => major.id === x.majorId))))
|
||||
const gradeOptions = computed(() => [...new Set(classes.value.map((x) => x.grade))].filter(Boolean).sort((a, b) => b - a))
|
||||
const yearOptions = computed(() => [...new Set(classes.value.map((x) => x.enrollmentYear))].filter(Boolean).sort((a, b) => b - a))
|
||||
const statusLabels: Record<string, string> = { Enrolled: '已修读', Waitlisted: '候补中', Withdrawn: '已退选', Cancelled: '已取消', Expired: '已失效' }
|
||||
const enrollmentTypeLabels: Record<string, string> = { Normal: '正常修读', Retake: '重修' }
|
||||
const exemptionLabels: Record<string, string> = { Submitted: '审核中', Approved: '已批准', Rejected: '已驳回' }
|
||||
function params() { return { ...query, keyword: query.keyword.trim() || undefined } }
|
||||
function updateCollege() { query.majorId = undefined; query.administrativeClassId = undefined; void load(1) }
|
||||
function updateMajor() { query.administrativeClassId = undefined; void load(1) }
|
||||
function endpoint() { return mode.value === 'student' ? '/course-selections/enrollments/students' : '/course-selections/enrollments' }
|
||||
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
|
||||
}
|
||||
query.page = page; loading.value = true
|
||||
try { const { data } = await http.get(endpoint(), { 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))
|
||||
}
|
||||
const url = mode.value === 'student' ? '/course-selections/enrollments/students/export.xlsx' : '/course-selections/enrollments/export.xlsx'
|
||||
await downloadApiFile(url, mode.value === 'student' ? '学生修读汇总.xlsx' : '修读课程名单.xlsx', { params: params() })
|
||||
ElMessage.success(`已导出符合条件的全部 ${total.value} 条${mode.value === 'student' ? '学生' : '修读记录'}`)
|
||||
} 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 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))
|
||||
}
|
||||
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
|
||||
const [term, course, college, major, administrativeClass] = await Promise.all([http.get('/base-data/terms'), http.get('/courses/options'), http.get('/base-data/colleges'), http.get('/base-data/majors'), http.get('/base-data/classes')])
|
||||
terms.value = term.data; courses.value = course.data; colleges.value = college.data; majors.value = major.data; classes.value = administrativeClass.data
|
||||
if (scopedCollegeId.value) query.collegeId = scopedCollegeId.value
|
||||
await load(1)
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
} 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="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">
|
||||
<el-radio-group v-model="mode" class="view-switch" @change="() => load(1)"><el-radio-button value="course">按课程 / 教学班</el-radio-button><el-radio-button value="student">按学生汇总</el-radio-button></el-radio-group>
|
||||
<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>
|
||||
<el-select v-model="query.academicTermId" clearable filterable placeholder="全部学期" @change="load(1)"><el-option v-for="x in terms" :key="x.id" :label="x.name" :value="x.id" /></el-select>
|
||||
<el-select v-model="query.collegeId" clearable filterable placeholder="全部开课院系" :disabled="!!scopedCollegeId" @change="updateCollege"><el-option v-for="x in availableColleges" :key="x.id" :label="x.name" :value="x.id" /></el-select>
|
||||
<el-select v-model="query.majorId" clearable filterable placeholder="全部专业" @change="updateMajor"><el-option v-for="x in availableMajors" :key="x.id" :label="x.name" :value="x.id" /></el-select>
|
||||
<el-select v-model="query.administrativeClassId" clearable filterable placeholder="全部行政班" @change="load(1)"><el-option v-for="x in availableClasses" :key="x.id" :label="x.name" :value="x.id" /></el-select>
|
||||
<el-select v-model="query.grade" clearable placeholder="全部年级" @change="load(1)"><el-option v-for="x in gradeOptions" :key="x" :label="`${x} 级`" :value="x" /></el-select>
|
||||
<el-select v-model="query.enrollmentYear" clearable placeholder="全部入学年份" @change="load(1)"><el-option v-for="x in yearOptions" :key="x" :label="`${x} 年入学`" :value="x" /></el-select>
|
||||
<el-select v-model="query.courseId" clearable filterable placeholder="全部课程" @change="load(1)"><el-option v-for="x in courses" :key="x.id" :label="`${x.code} · ${x.name}`" :value="x.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-select v-model="query.enrollmentType" clearable placeholder="全部修读类型" @change="load(1)"><el-option v-for="(label, value) in enrollmentTypeLabels" :key="value" :label="label" :value="value" /></el-select>
|
||||
<el-select v-model="query.exemptionStatus" clearable placeholder="全部免修状态" @change="load(1)"><el-option v-for="(label, value) in exemptionLabels" :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>
|
||||
<p class="result-note">导出不受当前页限制,将按全部筛选条件导出 {{ total }} 条{{ mode === 'student' ? '学生' : '修读记录' }}。</p>
|
||||
<el-table v-if="mode === 'course'" v-loading="loading" :data="rows" row-key="id">
|
||||
<el-table-column prop="termName" label="学期" width="120" /><el-table-column label="课程 / 教学班" min-width="220"><template #default="{ row }"><b>{{ row.courseName }}</b><small>{{ row.courseCode }} · {{ row.taskNumber }} · {{ row.credits }} 学分</small></template></el-table-column><el-table-column label="学生" min-width="155"><template #default="{ row }">{{ row.studentNumber }}<small>{{ row.studentName }} · {{ row.className }}</small></template></el-table-column><el-table-column prop="majorName" 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-table v-else v-loading="loading" :data="rows" row-key="studentId">
|
||||
<el-table-column type="expand"><template #default="{ row }"><el-table :data="row.courses" class="student-courses"><el-table-column prop="termName" label="学期" width="120" /><el-table-column label="课程 / 教学班" min-width="220"><template #default="{ row: course }"><b>{{ course.courseName }}</b><small>{{ course.courseCode }} · {{ course.taskNumber }} · {{ course.credits }} 学分</small></template></el-table-column><el-table-column label="状态" width="100"><template #default="{ row: course }"><el-tag size="small">{{ statusLabels[course.status] }}</el-tag></template></el-table-column><el-table-column label="免修" width="100"><template #default="{ row: course }">{{ course.exemptionStatus ? exemptionLabels[course.exemptionStatus] : '—' }}</template></el-table-column><el-table-column v-if="canAdjust" label="操作" width="80"><template #default="{ row: course }"><el-button v-if="course.status === 'Enrolled'" link type="danger" @click="withdraw(course)">退选</el-button></template></el-table-column></el-table></template></el-table-column><el-table-column prop="studentNumber" label="学号" width="140" /><el-table-column prop="studentName" label="姓名" width="110" /><el-table-column prop="majorName" label="专业" min-width="130" /><el-table-column prop="className" label="行政班" min-width="140" /><el-table-column label="年级" width="85"><template #default="{ row }">{{ row.grade }} 级</template></el-table-column><el-table-column prop="courseCount" label="课程数" width="85" /><el-table-column prop="activeCredits" label="在修学分" width="100" /><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>
|
||||
@@ -140,11 +100,5 @@ onMounted(async () => {
|
||||
</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%; } }
|
||||
.intro-actions,.filter-bar { display:flex; flex-wrap:wrap; gap:8px; }.view-switch { margin-bottom:16px; }.filter-bar { margin-bottom:10px; }.filter-bar .el-select { width:174px; }.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; }.student-courses { margin:6px 24px; width:calc(100% - 48px); }@media (max-width:640px) { .intro-actions,.filter-bar { align-items:stretch; flex-direction:column; }.filter-bar .el-select,.filter-bar .el-input { width:100%; }.student-courses { margin:4px; width:calc(100% - 8px); } }
|
||||
</style>
|
||||
Loaded 3 of 7 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user