自动排课
This commit is contained in:
@@ -91,6 +91,8 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
||||
.OrderByDescending(item => item.IsPrimary)
|
||||
.Select(item => item.Teacher!.Name),
|
||||
x.Capacity,
|
||||
x.StartWeek,
|
||||
x.EndWeek,
|
||||
x.WeeklyHours,
|
||||
x.SchedulingMode
|
||||
})
|
||||
@@ -114,6 +116,8 @@ public sealed class ScheduleSettingsController(AppDbContext db) : ControllerBase
|
||||
task.CollegeName,
|
||||
task.TeacherNames,
|
||||
task.Capacity,
|
||||
task.StartWeek,
|
||||
task.EndWeek,
|
||||
task.WeeklyHours,
|
||||
task.SchedulingMode,
|
||||
HasCustomConstraint = constraint is not null,
|
||||
|
||||
@@ -233,10 +233,10 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
||||
student.Status == StudentStatus.Active) ?? 0));
|
||||
return classrooms.Where(room =>
|
||||
room.Capacity >= minimumCapacity &&
|
||||
(!constraint?.RequiredCampusId.HasValue ?? true ||
|
||||
room.Building!.CampusId == constraint.RequiredCampusId) &&
|
||||
(!constraint?.RequiredBuildingId.HasValue ?? true ||
|
||||
room.BuildingId == constraint.RequiredBuildingId) &&
|
||||
(constraint?.RequiredCampusId is not Guid requiredCampusId ||
|
||||
room.Building!.CampusId == requiredCampusId) &&
|
||||
(constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
|
||||
room.BuildingId == requiredBuildingId) &&
|
||||
(allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ public sealed class AutomaticScheduleGeneratorTests
|
||||
}
|
||||
|
||||
[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 connection.OpenAsync();
|
||||
@@ -333,6 +333,34 @@ public sealed class AutomaticScheduleGeneratorTests
|
||||
EndsAt = new TimeOnly(9, 40)
|
||||
});
|
||||
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();
|
||||
plan = await db.SchedulePlans.SingleAsync();
|
||||
|
||||
+11
-3
@@ -26,10 +26,18 @@ http.interceptors.response.use(
|
||||
export function apiErrorMessage(error: unknown) {
|
||||
if (!axios.isAxiosError(error)) return '操作失败,请稍后重试。'
|
||||
const data = error.response?.data
|
||||
if (data?.errors) {
|
||||
return Object.values(data.errors).flat().join(';')
|
||||
if (data?.errors && typeof data.errors === 'object') {
|
||||
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
|
||||
|
||||
@@ -97,6 +97,12 @@ const autoStatusText = computed(() => {
|
||||
const selectedTaskConstraint = computed(() =>
|
||||
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 result = new Map<string, string>()
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!entryForm.teachingTaskId ||
|
||||
(selectedTaskConstraint.value?.requiresClassroom !== false && !entryForm.classroomId)) {
|
||||
ElMessage.warning('请选择教学任务,并按课程要求选择教室。')
|
||||
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
|
||||
try {
|
||||
const base = `/schedules/plans/${selected.value.id}/entries`
|
||||
@@ -827,10 +872,17 @@ onBeforeUnmount(clearAutoSchedulePoll)
|
||||
<el-dialog v-model="entryDialog" :title="editingEntryId ? '调整排课' : '添加排课'" width="680px">
|
||||
<el-form label-position="top">
|
||||
<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-select>
|
||||
</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
|
||||
v-if="selectedTaskConstraint?.requiresClassroom === false"
|
||||
title="该课程不占用教室,仍会校验教师和行政班时间冲突。"
|
||||
@@ -858,7 +910,7 @@ onBeforeUnmount(clearAutoSchedulePoll)
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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-select v-model="entryForm.startPeriod">
|
||||
<el-option
|
||||
|
||||
Reference in New Issue
Block a user