自动排课
This commit is contained in:
@@ -91,6 +91,8 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
|||||||
.OrderByDescending(item => item.IsPrimary)
|
.OrderByDescending(item => item.IsPrimary)
|
||||||
.Select(item => item.Teacher!.Name),
|
.Select(item => item.Teacher!.Name),
|
||||||
x.Capacity,
|
x.Capacity,
|
||||||
|
x.StartWeek,
|
||||||
|
x.EndWeek,
|
||||||
x.WeeklyHours,
|
x.WeeklyHours,
|
||||||
x.SchedulingMode
|
x.SchedulingMode
|
||||||
})
|
})
|
||||||
@@ -114,6 +116,8 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
|||||||
task.CollegeName,
|
task.CollegeName,
|
||||||
task.TeacherNames,
|
task.TeacherNames,
|
||||||
task.Capacity,
|
task.Capacity,
|
||||||
|
task.StartWeek,
|
||||||
|
task.EndWeek,
|
||||||
task.WeeklyHours,
|
task.WeeklyHours,
|
||||||
task.SchedulingMode,
|
task.SchedulingMode,
|
||||||
HasCustomConstraint = constraint is not null,
|
HasCustomConstraint = constraint is not null,
|
||||||
|
|||||||
@@ -233,10 +233,10 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
|||||||
student.Status == StudentStatus.Active) ?? 0));
|
student.Status == StudentStatus.Active) ?? 0));
|
||||||
return classrooms.Where(room =>
|
return classrooms.Where(room =>
|
||||||
room.Capacity >= minimumCapacity &&
|
room.Capacity >= minimumCapacity &&
|
||||||
(!constraint?.RequiredCampusId.HasValue ?? true ||
|
(constraint?.RequiredCampusId is not Guid requiredCampusId ||
|
||||||
room.Building!.CampusId == constraint.RequiredCampusId) &&
|
room.Building!.CampusId == requiredCampusId) &&
|
||||||
(!constraint?.RequiredBuildingId.HasValue ?? true ||
|
(constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
|
||||||
room.BuildingId == constraint.RequiredBuildingId) &&
|
room.BuildingId == requiredBuildingId) &&
|
||||||
(allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)))
|
(allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ public sealed class AutomaticScheduleGeneratorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Generator_does_not_attach_duplicate_buildings_from_untracked_classrooms()
|
public async Task Generator_uses_scoped_rooms_without_attaching_duplicate_buildings()
|
||||||
{
|
{
|
||||||
await using var connection = new SqliteConnection("Data Source=:memory:");
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
@@ -333,6 +333,34 @@ public sealed class AutomaticScheduleGeneratorTests
|
|||||||
EndsAt = new TimeOnly(9, 40)
|
EndsAt = new TimeOnly(9, 40)
|
||||||
});
|
});
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
db.TeachingTaskScheduleConstraints.AddRange(
|
||||||
|
new TeachingTaskScheduleConstraint
|
||||||
|
{
|
||||||
|
TeachingTaskId = smallTask.Id,
|
||||||
|
RequiredCampusId = campus.Id,
|
||||||
|
RequiredBuildingId = building.Id,
|
||||||
|
AllowedClassrooms =
|
||||||
|
[
|
||||||
|
new TeachingTaskAllowedClassroom
|
||||||
|
{
|
||||||
|
ClassroomId = smallRoom.Id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
new TeachingTaskScheduleConstraint
|
||||||
|
{
|
||||||
|
TeachingTaskId = largeTask.Id,
|
||||||
|
RequiredCampusId = campus.Id,
|
||||||
|
RequiredBuildingId = building.Id,
|
||||||
|
AllowedClassrooms =
|
||||||
|
[
|
||||||
|
new TeachingTaskAllowedClassroom
|
||||||
|
{
|
||||||
|
ClassroomId = largeRoom.Id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
db.ChangeTracker.Clear();
|
db.ChangeTracker.Clear();
|
||||||
plan = await db.SchedulePlans.SingleAsync();
|
plan = await db.SchedulePlans.SingleAsync();
|
||||||
|
|||||||
+11
-3
@@ -26,10 +26,18 @@ http.interceptors.response.use(
|
|||||||
export function apiErrorMessage(error: unknown) {
|
export function apiErrorMessage(error: unknown) {
|
||||||
if (!axios.isAxiosError(error)) return '操作失败,请稍后重试。'
|
if (!axios.isAxiosError(error)) return '操作失败,请稍后重试。'
|
||||||
const data = error.response?.data
|
const data = error.response?.data
|
||||||
if (data?.errors) {
|
if (data?.errors && typeof data.errors === 'object') {
|
||||||
return Object.values(data.errors).flat().join(';')
|
const validationMessage = Object.values(data.errors)
|
||||||
|
.flat()
|
||||||
|
.map((item) => String(item).trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(';')
|
||||||
|
if (validationMessage) return validationMessage
|
||||||
}
|
}
|
||||||
return data?.detail ?? data?.title ?? '操作失败,请检查网络连接。'
|
const detail = typeof data?.detail === 'string' ? data.detail.trim() : ''
|
||||||
|
if (detail) return detail
|
||||||
|
const title = typeof data?.title === 'string' ? data.title.trim() : ''
|
||||||
|
return title || '操作失败,请检查网络连接。'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default http
|
export default http
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ const autoStatusText = computed(() => {
|
|||||||
const selectedTaskConstraint = computed(() =>
|
const selectedTaskConstraint = computed(() =>
|
||||||
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
|
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
|
||||||
)
|
)
|
||||||
|
const entryWeekdays = computed(() => {
|
||||||
|
const allowedDays = selectedTaskConstraint.value?.allowedDayOfWeeks ?? []
|
||||||
|
return allowedDays.length
|
||||||
|
? weekdays.filter((day) => allowedDays.includes(day.value))
|
||||||
|
: weekdays
|
||||||
|
})
|
||||||
const constraintColleges = computed(() => {
|
const constraintColleges = computed(() => {
|
||||||
const result = new Map<string, string>()
|
const result = new Map<string, string>()
|
||||||
constraints.value.forEach((item) => result.set(item.collegeId, item.collegeName))
|
constraints.value.forEach((item) => result.set(item.collegeId, item.collegeName))
|
||||||
@@ -586,12 +592,51 @@ function openEntry(entry?: any, day?: number, period?: number) {
|
|||||||
entryDialog.value = true
|
entryDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeEntryTask() {
|
||||||
|
if (editingEntryId.value) return
|
||||||
|
const task = selectedTaskConstraint.value
|
||||||
|
if (!task) return
|
||||||
|
entryForm.startWeek = task.startWeek
|
||||||
|
entryForm.endWeek = task.endWeek
|
||||||
|
if (task.allowedDayOfWeeks?.length &&
|
||||||
|
!task.allowedDayOfWeeks.includes(entryForm.dayOfWeek)) {
|
||||||
|
entryForm.dayOfWeek = task.allowedDayOfWeeks[0]
|
||||||
|
}
|
||||||
|
if (task.requiresClassroom === false) {
|
||||||
|
entryForm.classroomId = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const room = classrooms.value.find((item) => item.id === entryForm.classroomId)
|
||||||
|
if (room && (
|
||||||
|
(task.requiredCampusId && room.campusId !== task.requiredCampusId) ||
|
||||||
|
(task.requiredBuildingId && room.buildingId !== task.requiredBuildingId) ||
|
||||||
|
(task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id))
|
||||||
|
)) {
|
||||||
|
entryForm.classroomId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveEntry() {
|
async function saveEntry() {
|
||||||
if (!entryForm.teachingTaskId ||
|
if (!entryForm.teachingTaskId ||
|
||||||
(selectedTaskConstraint.value?.requiresClassroom !== false && !entryForm.classroomId)) {
|
(selectedTaskConstraint.value?.requiresClassroom !== false && !entryForm.classroomId)) {
|
||||||
ElMessage.warning('请选择教学任务,并按课程要求选择教室。')
|
ElMessage.warning('请选择教学任务,并按课程要求选择教室。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const task = selectedTaskConstraint.value
|
||||||
|
if (task && (
|
||||||
|
entryForm.startWeek < task.startWeek ||
|
||||||
|
entryForm.endWeek > task.endWeek
|
||||||
|
)) {
|
||||||
|
ElMessage.warning(
|
||||||
|
`排课周次必须位于该教学任务的第 ${task.startWeek}—${task.endWeek} 周内。`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (task?.allowedDayOfWeeks?.length &&
|
||||||
|
!task.allowedDayOfWeeks.includes(entryForm.dayOfWeek)) {
|
||||||
|
ElMessage.warning('所选星期不在该教学任务允许的上课日内。')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (selectedTaskConstraint.value?.requiresClassroom === false) entryForm.classroomId = null
|
if (selectedTaskConstraint.value?.requiresClassroom === false) entryForm.classroomId = null
|
||||||
try {
|
try {
|
||||||
const base = `/schedules/plans/${selected.value.id}/entries`
|
const base = `/schedules/plans/${selected.value.id}/entries`
|
||||||
@@ -827,10 +872,17 @@ onBeforeUnmount(clearAutoSchedulePoll)
|
|||||||
<el-dialog v-model="entryDialog" :title="editingEntryId ? '调整排课' : '添加排课'" width="680px">
|
<el-dialog v-model="entryDialog" :title="editingEntryId ? '调整排课' : '添加排课'" width="680px">
|
||||||
<el-form label-position="top">
|
<el-form label-position="top">
|
||||||
<el-form-item label="教学任务" required>
|
<el-form-item label="教学任务" required>
|
||||||
<el-select v-model="entryForm.teachingTaskId" filterable>
|
<el-select v-model="entryForm.teachingTaskId" filterable @change="changeEntryTask">
|
||||||
<el-option v-for="item in tasks" :key="item.id" :label="`${item.taskNumber} · ${item.name}`" :value="item.id" />
|
<el-option v-for="item in tasks" :key="item.id" :label="`${item.taskNumber} · ${item.name}`" :value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-alert
|
||||||
|
v-if="selectedTaskConstraint"
|
||||||
|
:title="`可排第 ${selectedTaskConstraint.startWeek}—${selectedTaskConstraint.endWeek} 周;允许上课日:${entryWeekdays.map((day) => day.label).join('、')}`"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="selectedTaskConstraint?.requiresClassroom === false"
|
v-if="selectedTaskConstraint?.requiresClassroom === false"
|
||||||
title="该课程不占用教室,仍会校验教师和行政班时间冲突。"
|
title="该课程不占用教室,仍会校验教师和行政班时间冲突。"
|
||||||
@@ -858,7 +910,7 @@ onBeforeUnmount(clearAutoSchedulePoll)
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div class="form-grid three">
|
<div class="form-grid three">
|
||||||
<el-form-item label="星期"><el-select v-model="entryForm.dayOfWeek"><el-option v-for="day in weekdays" :key="day.value" :label="day.label" :value="day.value" /></el-select></el-form-item>
|
<el-form-item label="星期"><el-select v-model="entryForm.dayOfWeek"><el-option v-for="day in entryWeekdays" :key="day.value" :label="day.label" :value="day.value" /></el-select></el-form-item>
|
||||||
<el-form-item label="开始节次">
|
<el-form-item label="开始节次">
|
||||||
<el-select v-model="entryForm.startPeriod">
|
<el-select v-model="entryForm.startPeriod">
|
||||||
<el-option
|
<el-option
|
||||||
|
|||||||
Reference in New Issue
Block a user