优化排课

This commit is contained in:
2026-07-24 19:35:49 +08:00 Unverified
parent 6538b6080a
commit e6d8eb0662
17 changed files with 4073 additions and 6 deletions
+168 -5
View File
@@ -9,6 +9,8 @@ const loading = ref(false)
const rows = ref<any[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const generationDialog = ref(false)
const generating = ref(false)
const editingId = ref('')
const terms = ref<any[]>([])
const courses = ref<any[]>([])
@@ -23,6 +25,9 @@ const query = reactive({
status: undefined as string | undefined,
})
const form = reactive<Record<string, any>>({})
const generationForm = reactive<Record<string, any>>({})
const eligibleTeachers = ref<any[]>([])
const manualEligibleTeachers = ref<any[]>([])
const statusLabels: Record<string, string> = {
Draft: '草稿',
Published: '已发布',
@@ -35,8 +40,29 @@ const availableClasses = computed(() => {
)
return classes.value.filter((item) => ids.has(item.majorId))
})
const selectedCourse = computed(() =>
courses.value.find((item) => item.id === form.courseId),
)
const manualCourseIsPublic = computed(() =>
['GeneralRequired', 'GeneralElective'].includes(selectedCourse.value?.nature),
)
const assignableTeachers = computed(() => {
if (!manualCourseIsPublic.value) return teachers.value
const approvedIds = new Set(manualEligibleTeachers.value.map((item) => item.teacherId))
return teachers.value.filter((item) =>
approvedIds.has(item.id) || form.teacherIds?.includes(item.id),
)
})
const selectedTeachers = computed(() =>
teachers.value.filter((item) => form.teacherIds?.includes(item.id)),
assignableTeachers.value.filter((item) => form.teacherIds?.includes(item.id)),
)
const publicCourses = computed(() =>
courses.value.filter((item) =>
['GeneralRequired', 'GeneralElective'].includes(item.nature),
),
)
const generationGroupCount = computed(() =>
Math.ceil((generationForm.classIds?.length ?? 0) / (generationForm.classesPerTask || 1)),
)
async function load() {
@@ -97,12 +123,28 @@ async function openEdit(row: any) {
const detail = (await http.get(`/teaching-tasks/${row.id}`)).data
editingId.value = row.id
resetForm(detail)
await loadManualEligibleTeachers()
dialogVisible.value = true
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
async function loadManualEligibleTeachers() {
manualEligibleTeachers.value = []
if (!form.academicTermId || !form.courseId || !manualCourseIsPublic.value) return
try {
manualEligibleTeachers.value = (await http.get('/teacher-course-applications/eligible', {
params: {
academicTermId: form.academicTermId,
courseId: form.courseId,
},
})).data
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
function onTeachersChanged(values: string[]) {
if (form.primaryTeacherId && !values.includes(form.primaryTeacherId)) {
form.primaryTeacherId = undefined
@@ -170,6 +212,65 @@ async function closeTask(row: any) {
}
}
function openGeneration() {
const currentTerm = terms.value.find((item) => item.isCurrent)
Object.assign(generationForm, {
academicTermId: query.academicTermId ?? currentTerm?.id,
courseId: undefined,
classesPerTask: 3,
startWeek: 1,
endWeek: 16,
weeklyHours: 2,
classIds: [],
})
eligibleTeachers.value = []
generationDialog.value = true
}
async function loadEligibleTeachers() {
eligibleTeachers.value = []
if (!generationForm.academicTermId || !generationForm.courseId) return
try {
eligibleTeachers.value = (await http.get('/teacher-course-applications/eligible', {
params: {
academicTermId: generationForm.academicTermId,
courseId: generationForm.courseId,
},
})).data
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
async function generatePublicTasks() {
if (!generationForm.academicTermId ||
!generationForm.courseId ||
!generationForm.classIds?.length) {
ElMessage.warning('请选择学期、公共课和需要合班的行政班。')
return
}
if (!eligibleTeachers.value.length) {
ElMessage.warning('该课程还没有审核通过的可授课教师。')
return
}
try {
await ElMessageBox.confirm(
`${generationForm.classIds.length} 个行政班分成 ${generationGroupCount.value} 个教学班,并在 ${eligibleTeachers.value.length} 名合格教师中随机均衡分配。生成结果为草稿,确定继续吗?`,
'批量生成公共课教学任务',
{ type: 'warning', confirmButtonText: '生成草稿', cancelButtonText: '取消' },
)
generating.value = true
const { data } = await http.post('/teaching-tasks/generate-public-course', generationForm)
generationDialog.value = false
ElMessage.success(`已生成 ${data.createdCount} 个教学任务草稿`)
await load()
} catch (error: any) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
} finally {
generating.value = false
}
}
onMounted(async () => {
const [termRes, courseRes, teacherRes, classRes, majorRes] = await Promise.all([
http.get('/base-data/terms'),
@@ -196,7 +297,10 @@ onMounted(async () => {
<h2>教学任务</h2>
<p>将学期课程授课教师和行政班组合为可排课可选课的教学班</p>
</div>
<el-button type="primary" :icon="Plus" @click="openCreate">新建教学任务</el-button>
<div class="page-actions">
<el-button @click="openGeneration">公共课合班生成</el-button>
<el-button type="primary" :icon="Plus" @click="openCreate">新建教学任务</el-button>
</div>
</section>
<section class="task-summary">
@@ -252,8 +356,8 @@ onMounted(async () => {
<el-form-item label="教学班名称" required><el-input v-model="form.name" /></el-form-item>
</div>
<div class="form-grid">
<el-form-item label="开课学期" required><el-select v-model="form.academicTermId"><el-option v-for="item in terms" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="课程" required><el-select v-model="form.courseId" filterable><el-option v-for="item in courses" :key="item.id" :label="`${item.code} · ${item.name}`" :value="item.id" /></el-select></el-form-item>
<el-form-item label="开课学期" required><el-select v-model="form.academicTermId" @change="loadManualEligibleTeachers"><el-option v-for="item in terms" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item>
<el-form-item label="课程" required><el-select v-model="form.courseId" filterable @change="form.teacherIds = []; form.primaryTeacherId = undefined; loadManualEligibleTeachers()"><el-option v-for="item in courses" :key="item.id" :label="`${item.code} · ${item.name}`" :value="item.id" /></el-select></el-form-item>
</div>
<div class="form-grid three">
<el-form-item label="容量"><el-input-number v-model="form.capacity" :min="1" /></el-form-item>
@@ -263,8 +367,9 @@ onMounted(async () => {
<el-form-item label="周学时"><el-input-number v-model="form.weeklyHours" :min="1" :max="40" /></el-form-item>
<el-form-item label="授课教师">
<el-select v-model="form.teacherIds" multiple filterable @change="onTeachersChanged">
<el-option v-for="item in teachers" :key="item.id" :label="`${item.teacherNumber} · ${item.name}`" :value="item.id" />
<el-option v-for="item in assignableTeachers" :key="item.id" :label="`${item.teacherNumber} · ${item.name}`" :value="item.id" />
</el-select>
<small v-if="manualCourseIsPublic" class="field-hint">公共课仅显示本学期已申报且学院审核通过的教师</small>
</el-form-item>
<el-form-item label="主讲教师">
<el-select v-model="form.primaryTeacherId" clearable :disabled="!form.teacherIds?.length">
@@ -280,5 +385,63 @@ onMounted(async () => {
</el-form>
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" @click="save">保存草稿</el-button></template>
</el-dialog>
<el-dialog v-model="generationDialog" title="公共课合班生成教学任务" width="760px">
<div class="generation-notice">
行政班按编码顺序分组教师只从学院审核通过的名单中选择并优先分配给本学期教学任务较少的教师
</div>
<el-form label-position="top">
<div class="form-grid">
<el-form-item label="开课学期" required>
<el-select v-model="generationForm.academicTermId" @change="loadEligibleTeachers">
<el-option v-for="item in terms" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="公共课程" required>
<el-select v-model="generationForm.courseId" filterable @change="loadEligibleTeachers">
<el-option v-for="item in publicCourses" :key="item.id" :label="`${item.code} · ${item.name}`" :value="item.id" />
</el-select>
</el-form-item>
</div>
<el-alert
:title="eligibleTeachers.length ? `已有 ${eligibleTeachers.length} 名教师审核通过` : '请选择课程查看可分配教师'"
:type="eligibleTeachers.length ? 'success' : 'warning'"
:closable="false"
show-icon
>
<template #default>
<span v-if="eligibleTeachers.length">{{ eligibleTeachers.map((item) => item.name).join('、') }}</span>
</template>
</el-alert>
<el-form-item label="参与合班的行政班" required style="margin-top: 16px">
<el-select v-model="generationForm.classIds" multiple filterable collapse-tags>
<el-option v-for="item in availableClasses" :key="item.id" :label="`${item.code} · ${item.name}`" :value="item.id" />
</el-select>
</el-form-item>
<div class="form-grid three">
<el-form-item label="每个教学班合并行政班数">
<el-input-number v-model="generationForm.classesPerTask" :min="1" :max="10" />
</el-form-item>
<el-form-item label="开始周">
<el-input-number v-model="generationForm.startWeek" :min="1" :max="30" />
</el-form-item>
<el-form-item label="结束周">
<el-input-number v-model="generationForm.endWeek" :min="1" :max="30" />
</el-form-item>
</div>
<el-form-item label="周学时">
<el-input-number v-model="generationForm.weeklyHours" :min="1" :max="40" />
</el-form-item>
<div class="generation-preview">
<span>预计生成</span>
<b>{{ generationGroupCount }}</b>
<small>个教学任务草稿</small>
</div>
</el-form>
<template #footer>
<el-button @click="generationDialog = false">取消</el-button>
<el-button type="primary" :loading="generating" @click="generatePublicTasks">生成教学任务草稿</el-button>
</template>
</el-dialog>
</div>
</template>