选课
This commit is contained in:
@@ -486,11 +486,26 @@ button { cursor: pointer; }
|
||||
margin: 0 0 18px; padding: 16px; display: flex; align-items: center; gap: 14px;
|
||||
color: white; background: linear-gradient(115deg, #1b3067, #294584);
|
||||
}
|
||||
.roster-summary > div { min-width: 0; flex: 1; }
|
||||
.roster-summary > .el-button { flex: 0 0 auto; }
|
||||
.roster-summary > .el-icon { font-size: 28px; color: #5bd4c3; }
|
||||
.roster-summary span, .roster-summary b, .roster-summary small { display: block; }
|
||||
.roster-summary span { color: #59d2c1; font: 700 9px Consolas, monospace; }
|
||||
.roster-summary b { margin-top: 5px; font-size: 14px; }
|
||||
.roster-summary small { margin-top: 4px; color: #bec8e2; font-size: 10px; }
|
||||
.roster-notice { margin-bottom: 14px; }
|
||||
.proxy-course-note {
|
||||
margin-bottom: 14px; padding: 13px 15px; display: flex; align-items: center;
|
||||
justify-content: space-between; gap: 16px; border-left: 3px solid var(--teal);
|
||||
background: #f5f8fa;
|
||||
}
|
||||
.proxy-course-note b { font-size: 13px; }
|
||||
.proxy-course-note span { color: var(--muted); font-size: 10px; }
|
||||
.proxy-search { margin-bottom: 12px; display: flex; gap: 9px; }
|
||||
.proxy-search .el-input { flex: 1; }
|
||||
.proxy-pagination { margin-top: 14px; justify-content: flex-end; }
|
||||
.proxy-dialog-footer { display: flex; align-items: center; }
|
||||
.proxy-selected-count { margin-right: auto; color: var(--muted); font-size: 11px; }
|
||||
|
||||
.grade-page { min-width: 0; }
|
||||
.grade-toolbar {
|
||||
|
||||
@@ -31,9 +31,18 @@ const detailLoading = ref(false)
|
||||
const roundDialog = ref(false)
|
||||
const offeringDialog = ref(false)
|
||||
const rosterDrawer = ref(false)
|
||||
const proxyDialog = ref(false)
|
||||
const editingRoundId = ref('')
|
||||
const editingOfferingId = ref('')
|
||||
const roster = ref<any | null>(null)
|
||||
const rosterLoading = ref(false)
|
||||
const eligibleStudents = ref<any[]>([])
|
||||
const eligibleTotal = ref(0)
|
||||
const eligiblePage = ref(1)
|
||||
const eligibleLoading = ref(false)
|
||||
const proxySubmitting = ref(false)
|
||||
const studentKeyword = ref('')
|
||||
const selectedStudentIds = ref<string[]>([])
|
||||
const roundForm = reactive<Record<string, any>>({})
|
||||
const offeringForm = reactive<Record<string, any>>({})
|
||||
|
||||
@@ -286,13 +295,97 @@ async function deleteOffering(offering: any) {
|
||||
}
|
||||
|
||||
async function showRoster(offering: any) {
|
||||
rosterDrawer.value = true
|
||||
await loadRoster(offering.id)
|
||||
}
|
||||
|
||||
async function loadRoster(offeringId: string) {
|
||||
rosterLoading.value = true
|
||||
try {
|
||||
roster.value = (
|
||||
await http.get(`/course-selections/offerings/${offering.id}/roster`)
|
||||
await http.get(`/course-selections/offerings/${offeringId}/roster`)
|
||||
).data
|
||||
rosterDrawer.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
rosterLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openProxyEnrollment() {
|
||||
studentKeyword.value = ''
|
||||
selectedStudentIds.value = []
|
||||
eligiblePage.value = 1
|
||||
proxyDialog.value = true
|
||||
await loadEligibleStudents()
|
||||
}
|
||||
|
||||
async function loadEligibleStudents(page = eligiblePage.value) {
|
||||
if (!roster.value) return
|
||||
eligibleLoading.value = true
|
||||
eligiblePage.value = page
|
||||
try {
|
||||
const { data } = await http.get(
|
||||
`/course-selections/offerings/${roster.value.id}/eligible-students`,
|
||||
{
|
||||
params: {
|
||||
keyword: studentKeyword.value.trim() || undefined,
|
||||
page,
|
||||
pageSize: 20,
|
||||
},
|
||||
},
|
||||
)
|
||||
eligibleStudents.value = data.items
|
||||
eligibleTotal.value = data.total
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
eligibleLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onEligibleSelectionChanged(rows: any[]) {
|
||||
selectedStudentIds.value = rows.map((item) => item.id)
|
||||
}
|
||||
|
||||
async function proxyEnroll() {
|
||||
if (!roster.value || selectedStudentIds.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一名学生。')
|
||||
return
|
||||
}
|
||||
proxySubmitting.value = true
|
||||
try {
|
||||
const { data } = await http.post(
|
||||
`/course-selections/offerings/${roster.value.id}/admin-enrollments`,
|
||||
{ studentIds: selectedStudentIds.value },
|
||||
)
|
||||
ElMessage.success(`已为 ${data.enrolledCount} 名学生完成代选`)
|
||||
proxyDialog.value = false
|
||||
await loadRoster(roster.value.id)
|
||||
if (selectedRound.value) await selectRound(selectedRound.value)
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
proxySubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFromRoster(student: any) {
|
||||
if (!roster.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将 ${student.studentNumber} ${student.name} 移出“${roster.value.courseName}”教学班名单?`,
|
||||
'调整教学班名单',
|
||||
{ type: 'warning', confirmButtonText: '确认移出', cancelButtonText: '取消' },
|
||||
)
|
||||
await http.delete(
|
||||
`/course-selections/offerings/${roster.value.id}/admin-enrollments/${student.id}`,
|
||||
)
|
||||
ElMessage.success('已移出教学班名单')
|
||||
await loadRoster(roster.value.id)
|
||||
if (selectedRound.value) await selectRound(selectedRound.value)
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +548,7 @@ onMounted(async () => {
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="190" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="showRoster(row)">查看名单</el-button>
|
||||
<el-button link type="primary" @click="showRoster(row)">名单管理</el-button>
|
||||
<el-button
|
||||
v-if="selectedRound.status === 'Draft'"
|
||||
link
|
||||
@@ -603,26 +696,98 @@ onMounted(async () => {
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="rosterDrawer" title="教学班名单" size="620px">
|
||||
<el-drawer v-model="rosterDrawer" title="教学班名单管理" size="720px">
|
||||
<template v-if="roster">
|
||||
<div class="roster-summary">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<div>
|
||||
<span>{{ roster.taskNumber }}</span>
|
||||
<span>{{ roster.courseCode }} · {{ roster.taskNumber }}</span>
|
||||
<b>{{ roster.taskName }}</b>
|
||||
<small>{{ roster.enrolledCount }} / {{ roster.capacity }} 人</small>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="roster.canProxyEnroll"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openProxyEnrollment"
|
||||
>代选学生</el-button>
|
||||
</div>
|
||||
<el-table :data="roster.students">
|
||||
<el-alert
|
||||
v-if="roster.canProxyEnroll"
|
||||
class="roster-notice"
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="公共必修课支持校级教务代选;系统仍会校验教学班容量、学分上限、重复课程和课表冲突。"
|
||||
/>
|
||||
<el-table v-loading="rosterLoading" :data="roster.students">
|
||||
<el-table-column prop="studentNumber" label="学号" width="130" />
|
||||
<el-table-column prop="name" label="姓名" width="90" />
|
||||
<el-table-column prop="className" label="行政班" min-width="150" />
|
||||
<el-table-column label="选课时间" min-width="130">
|
||||
<template #default="{ row }">{{ formatDateTime(row.enrolledAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="roster.canProxyEnroll" label="操作" width="80" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" @click="removeFromRoster(row)">移出</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无学生选课" /></template>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog v-model="proxyDialog" title="公共必修课代选" width="760px">
|
||||
<template v-if="roster">
|
||||
<div class="proxy-course-note">
|
||||
<b>{{ roster.courseName }}</b>
|
||||
<span>{{ roster.taskNumber }} · 当前 {{ roster.enrolledCount }} / {{ roster.capacity }} 人</span>
|
||||
</div>
|
||||
<div class="proxy-search">
|
||||
<el-input
|
||||
v-model="studentKeyword"
|
||||
clearable
|
||||
placeholder="输入学号、姓名或行政班"
|
||||
@keyup.enter="loadEligibleStudents(1)"
|
||||
@clear="loadEligibleStudents(1)"
|
||||
/>
|
||||
<el-button type="primary" @click="loadEligibleStudents(1)">查询学生</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="eligibleLoading"
|
||||
:data="eligibleStudents"
|
||||
row-key="id"
|
||||
height="360"
|
||||
@selection-change="onEligibleSelectionChanged"
|
||||
>
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column prop="studentNumber" label="学号" width="130" />
|
||||
<el-table-column prop="name" label="姓名" width="90" />
|
||||
<el-table-column prop="className" label="行政班" min-width="150" />
|
||||
<el-table-column prop="majorName" label="专业" min-width="150" />
|
||||
<template #empty><el-empty description="没有可代选的在籍学生" /></template>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="eligibleTotal > 20"
|
||||
class="proxy-pagination"
|
||||
layout="prev, pager, next, total"
|
||||
:current-page="eligiblePage"
|
||||
:page-size="20"
|
||||
:total="eligibleTotal"
|
||||
@current-change="loadEligibleStudents"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="proxy-dialog-footer">
|
||||
<span class="proxy-selected-count">已选择 {{ selectedStudentIds.length }} 人</span>
|
||||
<el-button @click="proxyDialog = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="proxySubmitting"
|
||||
:disabled="selectedStudentIds.length === 0"
|
||||
@click="proxyEnroll"
|
||||
>确认代选</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user