优化考试排考

This commit is contained in:
2026-07-27 19:31:19 +08:00 Unverified
parent 64decdab7e
commit c3e8a42641
14 changed files with 7013 additions and 272 deletions
+58 -9
View File
@@ -68,17 +68,39 @@ const filteredTasks = computed(() => {
const filteredSessions = computed(() => {
const keyword = sessionFilter.keyword.trim().toLowerCase()
return (selected.value?.sessions ?? []).filter((session: any) => {
if (sessionFilter.allocation === 'room' && session.classroomId) return false
if (sessionFilter.allocation === 'room' && hasRoomAssignment(session)) return false
if (sessionFilter.allocation === 'invigilator' &&
session.invigilatorIds.length >= session.requiredInvigilatorCount) return false
hasCompleteInvigilators(session)) return false
if (sessionFilter.allocation === 'complete' &&
(!session.classroomId || session.invigilatorIds.length < session.requiredInvigilatorCount)) return false
(!hasRoomAssignment(session) || !hasCompleteInvigilators(session))) return false
if (!keyword) return true
return [session.taskNumber, session.courseCode, session.courseName, session.taskName]
.some((value: any) => String(value ?? '').toLowerCase().includes(keyword))
})
})
function sessionRooms(session: any) {
return session.examRooms ?? []
}
function hasRoomAssignment(session: any) {
return sessionRooms(session).length > 0 || Boolean(session.classroomId)
}
function hasCompleteInvigilators(session: any) {
const roomAssignments = sessionRooms(session)
if (roomAssignments.length > 0) {
return roomAssignments.every((room: any) =>
room.invigilatorIds.length >= session.requiredInvigilatorCount)
}
return session.invigilatorIds.length >= session.requiredInvigilatorCount
}
function sessionInvigilatorNames(session: any) {
const roomAssignments = sessionRooms(session)
const names = roomAssignments.length > 0
? roomAssignments.flatMap((room: any) => room.invigilatorNames)
: session.invigilatorNames
return Array.from(new Set(names))
}
function timeText(startsAt: string) {
return new Date(startsAt).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false })
}
@@ -144,7 +166,18 @@ async function savePlan() {
await load()
} catch (error) { ElMessage.error(apiErrorMessage(error)) }
}
function openSession(existing?: any) {
async function openSession(existing?: any) {
if (existing && sessionRooms(existing).length > 0) {
try {
await ElMessageBox.confirm(
'修改该教学班的课程或考试时间会清除相关混排考场和座位,需要重新执行考场编排。',
'修改混排场次',
{ type: 'warning', confirmButtonText: '继续修改' },
)
} catch {
return
}
}
editingSession.value = existing ?? null
selectedTaskIds.value = existing ? [existing.teachingTaskId] : []
Object.assign(taskFilter, { keyword: '', collegeId: '', courseNature: '' })
@@ -263,8 +296,11 @@ async function autoArrange(mode: 'rooms' | 'invigilators' | 'all') {
: '当前计划全部场次'
const action = mode === 'rooms' ? '分配考场'
: mode === 'invigilators' ? '分配监考教师' : '分配考场和监考教师'
const arrangementNotice = mode === 'invigilators'
? '已有考场和座位不会改变。'
: '系统会按同一课程、同一时间生成完整混排组,并重建相关考场和座位。'
await ElMessageBox.confirm(
`系统将为${target}${action},已有安排不会被覆盖`,
`系统将为${target}${action}${arrangementNotice}`,
`一键${action}`, { type: 'info', confirmButtonText: '开始分配' })
arrangeLoading.value = true
const res = await http.post(`/exams/plans/${selected.value.id}/auto-arrange`, {
@@ -373,7 +409,7 @@ onMounted(async () => {
<div>
<span class="section-kicker">EXAMINATION OFFICE</span>
<h2>{{ isManager ? '考试安排与考场' : isTeacher ? '我的监考' : '我的考试' }}</h2>
<p>{{ isManager ? '基于课表节次安排考试,自动匹配考场与监考教师。' : '查看学校已经正式发布的考试日程。' }}</p>
<p>{{ isManager ? '基于课表节次安排考试,同课程教学班混排考场并自动匹配监考教师。' : '查看学校已经正式发布的考试日程。' }}</p>
</div>
<el-button v-if="isManager" type="primary" :icon="Plus" @click="openPlan">新建考试计划</el-button>
<el-button v-else :icon="Refresh" @click="load">刷新日程</el-button>
@@ -438,7 +474,7 @@ onMounted(async () => {
</template>
</div>
<div class="exam-timeline">
<article v-for="session in filteredSessions" :key="session.id" :class="{ unassigned: !session.classroomId }">
<article v-for="session in filteredSessions" :key="session.id" :class="{ unassigned: !hasRoomAssignment(session) }">
<time>
<el-checkbox
v-if="selected.status === 'Draft'"
@@ -452,7 +488,14 @@ onMounted(async () => {
<span>{{ session.courseCode }} · {{ session.taskNumber }}</span>
<h4>{{ session.courseName }}</h4>
<p>
<template v-if="session.classroomId">{{ session.buildingName }} · {{ session.classroomName }} · {{ session.classroomCapacity }}</template>
<template v-if="sessionRooms(session).length">
<span v-for="room in sessionRooms(session)" :key="room.examRoomId" class="exam-room-chip">
{{ room.buildingName }} · {{ room.classroomName }}
· 本班{{ room.seatCount }}/全场{{ room.totalSeatCount }}
<el-tag v-if="room.isMixed" size="small" type="success">混排</el-tag>
</span>
</template>
<template v-else-if="session.classroomId">{{ session.buildingName }} · {{ session.classroomName }} · {{ session.classroomCapacity }}</template>
<template v-else><el-tag size="small" type="warning">待分配考场</el-tag></template>
· {{ session.studentCount }}
<template v-if="session.requiredBuildingName"> · {{ session.requiredBuildingName }}</template>
@@ -461,7 +504,7 @@ onMounted(async () => {
</div>
<div class="exam-staff">
<span>监考</span>
<b>{{ session.invigilatorNames.length ? session.invigilatorNames.join('、') : '待分配' }}</b>
<b>{{ sessionInvigilatorNames(session).length ? sessionInvigilatorNames(session).join('、') : '待分配' }}</b>
<small>{{ timeText(session.startsAt) }}{{ timeText(session.endsAt) }}</small>
</div>
<div class="exam-row-actions">
@@ -607,6 +650,8 @@ onMounted(async () => {
<!-- Roster Drawer -->
<el-drawer v-model="rosterDrawer" title="考生名单" size="560px">
<el-table v-if="roster" :data="roster.students">
<el-table-column prop="buildingName" label="教学楼" width="110" />
<el-table-column prop="classroomName" label="考场" width="90" />
<el-table-column prop="seatNumber" label="座位号" width="80" />
<el-table-column prop="studentNumber" label="学号" width="130" />
<el-table-column prop="name" label="姓名" width="90" />
@@ -655,6 +700,10 @@ onMounted(async () => {
.exam-timeline article.unassigned {
border-left-color: #e6a23c;
}
.exam-room-chip {
display: block;
margin: 3px 0;
}
@media (max-width: 720px) {
.exam-actions {
justify-content: flex-start;