自动排考
This commit is contained in:
+133
-40
@@ -12,6 +12,11 @@ const isManager = computed(() =>
|
||||
const isTeacher = computed(() => auth.user?.roles.includes('Teacher') && !isManager.value)
|
||||
const plans = ref<any[]>([])
|
||||
const selected = ref<any | null>(null)
|
||||
const sessionPage = ref(1)
|
||||
const sessionPageSize = ref(20)
|
||||
const sessionTotal = ref(0)
|
||||
const planSessionTotal = ref(0)
|
||||
const sessionLoading = ref(false)
|
||||
const personal = ref<any[]>([])
|
||||
const terms = ref<any[]>([])
|
||||
const tasks = ref<any[]>([])
|
||||
@@ -52,9 +57,10 @@ const taskColleges = computed(() => {
|
||||
})
|
||||
const filteredTasks = computed(() => {
|
||||
const keyword = taskFilter.keyword.trim().toLowerCase()
|
||||
const arrangedIds = new Set((selected.value?.sessions ?? [])
|
||||
.filter((session: any) => session.id !== editingSession.value?.id)
|
||||
.map((session: any) => session.teachingTaskId))
|
||||
const arrangedIds = new Set<string>(selected.value?.scheduledTeachingTaskIds ?? [])
|
||||
if (editingSession.value?.teachingTaskId) {
|
||||
arrangedIds.delete(editingSession.value.teachingTaskId)
|
||||
}
|
||||
return tasks.value.filter((task: any) => {
|
||||
if (arrangedIds.has(task.id)) return false
|
||||
if (taskFilter.collegeId && task.collegeId !== taskFilter.collegeId) return false
|
||||
@@ -65,19 +71,7 @@ const filteredTasks = computed(() => {
|
||||
.some((value: any) => String(value ?? '').toLowerCase().includes(keyword))
|
||||
})
|
||||
})
|
||||
const filteredSessions = computed(() => {
|
||||
const keyword = sessionFilter.keyword.trim().toLowerCase()
|
||||
return (selected.value?.sessions ?? []).filter((session: any) => {
|
||||
if (sessionFilter.allocation === 'room' && hasRoomAssignment(session)) return false
|
||||
if (sessionFilter.allocation === 'invigilator' &&
|
||||
hasCompleteInvigilators(session)) return false
|
||||
if (sessionFilter.allocation === 'complete' &&
|
||||
(!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))
|
||||
})
|
||||
})
|
||||
const filteredSessions = computed(() => selected.value?.sessions ?? [])
|
||||
|
||||
function sessionRooms(session: any) {
|
||||
return session.examRooms ?? []
|
||||
@@ -85,14 +79,6 @@ function sessionRooms(session: any) {
|
||||
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
|
||||
@@ -131,6 +117,9 @@ async function load() {
|
||||
else {
|
||||
selected.value = null
|
||||
selectedSessionIds.value = []
|
||||
sessionPage.value = 1
|
||||
sessionTotal.value = 0
|
||||
planSessionTotal.value = 0
|
||||
tasks.value = []
|
||||
timeSlots.value = []
|
||||
}
|
||||
@@ -138,10 +127,63 @@ async function load() {
|
||||
finally { loading.value = false }
|
||||
}
|
||||
async function selectPlan(id: string) {
|
||||
selected.value = (await http.get(`/exams/plans/${id}`)).data
|
||||
sessionPage.value = 1
|
||||
selectedSessionIds.value = []
|
||||
await loadSelectedPlan(id)
|
||||
await loadPlanResources(selected.value.academicTermId)
|
||||
}
|
||||
async function loadSelectedPlan(id: string) {
|
||||
sessionLoading.value = true
|
||||
try {
|
||||
const { data } = await http.get(`/exams/plans/${id}`, {
|
||||
params: {
|
||||
page: sessionPage.value,
|
||||
pageSize: sessionPageSize.value,
|
||||
keyword: sessionFilter.keyword.trim() || undefined,
|
||||
allocation: sessionFilter.allocation || undefined,
|
||||
},
|
||||
})
|
||||
selected.value = data
|
||||
sessionPage.value = data.sessionPage
|
||||
sessionPageSize.value = data.sessionPageSize
|
||||
sessionTotal.value = data.filteredSessionCount
|
||||
planSessionTotal.value = data.totalSessionCount
|
||||
const planSummary = plans.value.find((plan: any) => plan.id === data.id)
|
||||
if (planSummary) planSummary.sessionCount = data.totalSessionCount
|
||||
} finally {
|
||||
sessionLoading.value = false
|
||||
}
|
||||
}
|
||||
async function reloadSelectedPlan() {
|
||||
if (!selected.value) return
|
||||
await loadSelectedPlan(selected.value.id)
|
||||
}
|
||||
async function applySessionFilters() {
|
||||
if (!selected.value) return
|
||||
sessionPage.value = 1
|
||||
try {
|
||||
await reloadSelectedPlan()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
async function changeSessionPage(page: number) {
|
||||
sessionPage.value = page
|
||||
try {
|
||||
await reloadSelectedPlan()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
async function changeSessionPageSize(pageSize: number) {
|
||||
sessionPageSize.value = pageSize
|
||||
sessionPage.value = 1
|
||||
try {
|
||||
await reloadSelectedPlan()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
async function loadPlanResources(academicTermId: string) {
|
||||
const [taskRes, slotRes] = await Promise.all([
|
||||
http.get('/teaching-tasks/options', {
|
||||
@@ -218,14 +260,15 @@ async function saveSession() {
|
||||
}
|
||||
sessionDialog.value = false
|
||||
editingSession.value = null
|
||||
await selectPlan(selected.value.id)
|
||||
await reloadSelectedPlan()
|
||||
} catch (error) { ElMessage.error(apiErrorMessage(error)) }
|
||||
}
|
||||
async function removeSession(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`移除"${row.courseName}"考试场次?`, '移除场次', { type: 'warning' })
|
||||
await http.delete(`/exams/plans/${selected.value.id}/sessions/${row.id}`)
|
||||
await selectPlan(selected.value.id)
|
||||
selectedSessionIds.value = selectedSessionIds.value.filter(id => id !== row.id)
|
||||
await reloadSelectedPlan()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
@@ -252,7 +295,8 @@ async function removeSelectedSessions() {
|
||||
{ sessionIds: selectedSessionIds.value },
|
||||
)
|
||||
ElMessage.success(`已移除 ${res.data.removedCount} 个考试场次`)
|
||||
await selectPlan(selected.value.id)
|
||||
selectedSessionIds.value = []
|
||||
await reloadSelectedPlan()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
@@ -279,7 +323,14 @@ function toggleTaskSelection(id: string, checked: boolean) {
|
||||
: selectedTaskIds.value.filter(value => value !== id)
|
||||
}
|
||||
function selectFilteredSessions() {
|
||||
selectedSessionIds.value = filteredSessions.value.map((session: any) => session.id)
|
||||
const selectedIds = Array.from(new Set([
|
||||
...selectedSessionIds.value,
|
||||
...filteredSessions.value.map((session: any) => session.id),
|
||||
]))
|
||||
if (selectedIds.length > 100) {
|
||||
ElMessage.warning('一次最多选择100个考试场次')
|
||||
}
|
||||
selectedSessionIds.value = selectedIds.slice(0, 100)
|
||||
}
|
||||
function clearSessionSelection() {
|
||||
selectedSessionIds.value = []
|
||||
@@ -309,7 +360,7 @@ async function autoArrange(mode: 'rooms' | 'invigilators' | 'all') {
|
||||
assignInvigilators: mode !== 'rooms',
|
||||
})
|
||||
ElMessage.success(res.data.message)
|
||||
await selectPlan(selected.value.id)
|
||||
await reloadSelectedPlan()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||
} finally { arrangeLoading.value = false }
|
||||
@@ -329,7 +380,7 @@ async function deleteDraftPlan() {
|
||||
if (!selected.value || selected.value.status !== 'Draft') return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除草稿考试计划“${selected.value.name}”吗?计划内 ${selected.value.sessions.length} 个考试场次也会一并删除,此操作无法撤销。`,
|
||||
`确定删除草稿考试计划“${selected.value.name}”吗?计划内 ${planSessionTotal.value} 个考试场次也会一并删除,此操作无法撤销。`,
|
||||
'删除考试计划',
|
||||
{
|
||||
type: 'warning',
|
||||
@@ -427,13 +478,13 @@ onMounted(async () => {
|
||||
<div>
|
||||
<span>EXAM TIMELINE</span>
|
||||
<h3>{{ selected.name }}</h3>
|
||||
<p>{{ selected.termName }} · {{ selected.sessions.length }} 个考试场次</p>
|
||||
<p>{{ selected.termName }} · {{ planSessionTotal }} 个考试场次</p>
|
||||
</div>
|
||||
<div class="exam-actions">
|
||||
<el-button
|
||||
:icon="Download"
|
||||
:loading="exportLoading"
|
||||
:disabled="selected.sessions.length === 0"
|
||||
:disabled="planSessionTotal === 0"
|
||||
@click="exportSignInSheets"
|
||||
>导出考场签名单</el-button>
|
||||
<el-button v-if="selected.status === 'Draft'" @click="autoArrange('rooms')" :loading="arrangeLoading">一键分配考场</el-button>
|
||||
@@ -452,15 +503,32 @@ onMounted(async () => {
|
||||
</div>
|
||||
</header>
|
||||
<div class="exam-filter-bar">
|
||||
<el-input v-model="sessionFilter.keyword" clearable placeholder="筛选课程、课程号或教学班号" />
|
||||
<el-select v-model="sessionFilter.allocation" clearable placeholder="全部分配状态">
|
||||
<el-input
|
||||
v-model="sessionFilter.keyword"
|
||||
clearable
|
||||
placeholder="筛选课程、课程号或教学班号"
|
||||
@keyup.enter="applySessionFilters"
|
||||
@clear="applySessionFilters"
|
||||
/>
|
||||
<el-select
|
||||
v-model="sessionFilter.allocation"
|
||||
clearable
|
||||
placeholder="全部分配状态"
|
||||
@change="applySessionFilters"
|
||||
>
|
||||
<el-option label="待分配考场" value="room" />
|
||||
<el-option label="待补足监考" value="invigilator" />
|
||||
<el-option label="已完成分配" value="complete" />
|
||||
</el-select>
|
||||
<span>显示 {{ filteredSessions.length }}/{{ selected.sessions.length }} 个场次</span>
|
||||
<el-button :loading="sessionLoading" @click="applySessionFilters">查询</el-button>
|
||||
<span>
|
||||
本页 {{ filteredSessions.length }} 个,共 {{ sessionTotal }} 个符合条件
|
||||
<template v-if="sessionTotal !== planSessionTotal">
|
||||
(计划共 {{ planSessionTotal }} 个)
|
||||
</template>
|
||||
</span>
|
||||
<template v-if="selected.status === 'Draft'">
|
||||
<el-button link type="primary" @click="selectFilteredSessions">选择筛选结果</el-button>
|
||||
<el-button link type="primary" @click="selectFilteredSessions">选择本页</el-button>
|
||||
<el-button link @click="clearSessionSelection">清空选择</el-button>
|
||||
<el-tag v-if="selectedSessionIds.length" type="info">已选 {{ selectedSessionIds.length }} 个</el-tag>
|
||||
<el-button
|
||||
@@ -473,7 +541,7 @@ onMounted(async () => {
|
||||
>批量移除已选场次</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="exam-timeline">
|
||||
<div class="exam-timeline" v-loading="sessionLoading">
|
||||
<article v-for="session in filteredSessions" :key="session.id" :class="{ unassigned: !hasRoomAssignment(session) }">
|
||||
<time>
|
||||
<el-checkbox
|
||||
@@ -513,9 +581,27 @@ onMounted(async () => {
|
||||
<el-button v-if="selected.status === 'Draft'" link type="danger" @click="removeSession(session)">移除</el-button>
|
||||
</div>
|
||||
</article>
|
||||
<el-empty v-if="!selected.sessions.length" description="尚未安排考试场次,点击「批量安排场次」开始。" />
|
||||
<el-empty v-else-if="!filteredSessions.length" description="没有符合筛选条件的考试场次" />
|
||||
<el-empty
|
||||
v-if="planSessionTotal === 0"
|
||||
description="尚未安排考试场次,点击「批量安排场次」开始。"
|
||||
/>
|
||||
<el-empty
|
||||
v-else-if="sessionTotal === 0"
|
||||
description="没有符合筛选条件的考试场次"
|
||||
/>
|
||||
</div>
|
||||
<el-pagination
|
||||
v-if="sessionTotal > 0"
|
||||
class="exam-pagination"
|
||||
background
|
||||
:current-page="sessionPage"
|
||||
:page-size="sessionPageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="sessionTotal"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="changeSessionPage"
|
||||
@size-change="changeSessionPageSize"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -704,6 +790,10 @@ onMounted(async () => {
|
||||
display: block;
|
||||
margin: 3px 0;
|
||||
}
|
||||
.exam-pagination {
|
||||
justify-content: flex-end;
|
||||
margin-top: 18px;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.exam-actions {
|
||||
justify-content: flex-start;
|
||||
@@ -715,5 +805,8 @@ onMounted(async () => {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
.exam-pagination {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user