diff --git a/src/Jiaowu.Api/Controllers/ScheduleSettingsController.cs b/src/Jiaowu.Api/Controllers/ScheduleSettingsController.cs index a00a881..2ea6900 100644 --- a/src/Jiaowu.Api/Controllers/ScheduleSettingsController.cs +++ b/src/Jiaowu.Api/Controllers/ScheduleSettingsController.cs @@ -342,6 +342,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache) Building? building = null; List allowedRooms = []; var experimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? []; + Building? experimentBuilding = null; List allowedExperimentRooms = []; if (request.UpdateClassroomScope) { @@ -381,12 +382,34 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache) } if (request.UpdateExperimentClassroomScope) { + if (request.ExperimentRequiredBuildingId.HasValue) + { + experimentBuilding = await db.Buildings.AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == request.ExperimentRequiredBuildingId && x.IsEnabled, + cancellationToken); + if (experimentBuilding is null) + return ValidationProblem("指定实验教学楼不存在或已停用。"); + if (request.ExperimentRequiredCampusId.HasValue && + experimentBuilding.CampusId != request.ExperimentRequiredCampusId) + return ValidationProblem("指定实验教学楼不属于所选实验校区。"); + } + if (request.ExperimentRequiredCampusId.HasValue && + !await db.Campuses.AnyAsync(x => x.Id == request.ExperimentRequiredCampusId && x.IsEnabled, + cancellationToken)) + return ValidationProblem("指定实验校区不存在或已停用。"); allowedExperimentRooms = await db.Classrooms.AsNoTracking() .Where(x => x.IsEnabled) .WhereIn(experimentRoomIds, x => x.Id) + .Include(x => x.Building) .ToListAsync(cancellationToken); if (allowedExperimentRooms.Count != experimentRoomIds.Length) return ValidationProblem("部分指定实验场地不存在或已停用。"); + if (experimentBuilding is not null && + allowedExperimentRooms.Any(x => x.BuildingId != experimentBuilding.Id)) + return ValidationProblem("指定实验场地必须位于所选实验教学楼。"); + if (request.ExperimentRequiredCampusId.HasValue && + allowedExperimentRooms.Any(x => x.Building!.CampusId != request.ExperimentRequiredCampusId)) + return ValidationProblem("指定实验场地必须位于所选实验校区。"); } var constraints = await db.TeachingTaskScheduleConstraints @@ -457,6 +480,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache) constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures.Value; if (task.Course?.PracticeHours > 0 && request.UpdateExperimentClassroomScope) { + constraint.ExperimentRequiredCampusId = request.ExperimentRequiredCampusId; + constraint.ExperimentRequiredBuildingId = request.ExperimentRequiredBuildingId; db.TeachingTaskAllowedExperimentClassrooms.RemoveRange( constraint.AllowedExperimentClassrooms); constraint.AllowedExperimentClassrooms = allowedExperimentRooms.Select(room => @@ -548,4 +573,6 @@ public sealed record TeachingTaskScheduleConstraintBatchRequest( [Range(1, 30)] int? LatestPeriod, bool UpdateExperimentClassroomScope = false, TeachingVenueNature? AllowedExperimentVenueNatures = null, - IReadOnlyList? AllowedExperimentClassroomIds = null); + IReadOnlyList? AllowedExperimentClassroomIds = null, + Guid? ExperimentRequiredCampusId = null, + Guid? ExperimentRequiredBuildingId = null); diff --git a/tests/Jiaowu.Api.Tests/ScheduleSettingsControllerTests.cs b/tests/Jiaowu.Api.Tests/ScheduleSettingsControllerTests.cs index a765119..b7f7ec9 100644 --- a/tests/Jiaowu.Api.Tests/ScheduleSettingsControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/ScheduleSettingsControllerTests.cs @@ -85,13 +85,19 @@ public sealed class ScheduleSettingsControllerTests [classroom.Id], false, null, - null), + null, + UpdateExperimentClassroomScope: true, + AllowedExperimentVenueNatures: TeachingVenueNature.Laboratory, + AllowedExperimentClassroomIds: [classroom.Id], + ExperimentRequiredCampusId: campus.Id, + ExperimentRequiredBuildingId: building.Id), CancellationToken.None); Assert.IsType(result); db.ChangeTracker.Clear(); var constraints = await db.TeachingTaskScheduleConstraints .Include(item => item.AllowedClassrooms) + .Include(item => item.AllowedExperimentClassrooms) .OrderBy(item => item.TeachingTaskId) .ToListAsync(); Assert.Equal(2, constraints.Count); @@ -103,6 +109,11 @@ public sealed class ScheduleSettingsControllerTests Assert.Equal( classroom.Id, Assert.Single(constraint.AllowedClassrooms).ClassroomId); + Assert.Equal(campus.Id, constraint.ExperimentRequiredCampusId); + Assert.Equal(building.Id, constraint.ExperimentRequiredBuildingId); + Assert.Equal( + classroom.Id, + Assert.Single(constraint.AllowedExperimentClassrooms).ClassroomId); }); } diff --git a/web/src/style.css b/web/src/style.css index 9161291..351df9e 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -512,6 +512,8 @@ button { cursor: pointer; } .constraint-batch-form { margin-top: 16px; display: grid; gap: 10px; } .constraint-batch-form > .el-checkbox { padding: 8px 10px; background: #f7f9fb; border-left: 3px solid #ccd8e1; } .batch-classroom-scope { padding: 12px 14px 2px; display: grid; gap: 12px; border: 1px solid #d8e2e8; background: #fbfcfd; } +.experiment-classroom-scope { margin-bottom: 18px; padding: 12px 14px 2px; border: 1px solid #d8e2e8; background: #fbfcfd; } +.experiment-classroom-scope__title { margin-bottom: 12px; color: #34435e; font-size: 13px; font-weight: 650; } .constraint-list article { min-width: 0; padding: 13px 15px; display: grid; grid-template-columns: minmax(230px, 1fr) minmax(170px, auto) auto; align-items: center; gap: 14px; border: 1px solid var(--line); background: #fff; } .constraint-list article > div:first-child { min-width: 0; display: grid; gap: 4px; } .constraint-list article span { color: var(--teal); font: 700 9px/1.2 Consolas, monospace; } diff --git a/web/src/views/SchedulesView.vue b/web/src/views/SchedulesView.vue index 4314d2b..9d18275 100644 --- a/web/src/views/SchedulesView.vue +++ b/web/src/views/SchedulesView.vue @@ -236,8 +236,17 @@ const batchFilteredClassrooms = computed(() => item.buildingId === constraintBatchForm.requiredBuildingId), ), ) +const batchFilteredExperimentBuildings = computed(() => + constraintBatchForm.experimentRequiredCampusId + ? buildings.value.filter((item) => item.campusId === constraintBatchForm.experimentRequiredCampusId) + : buildings.value, +) const batchFilteredExperimentClassrooms = computed(() => - batchFilteredClassrooms.value.filter((item) => + classrooms.value.filter((item) => + (!constraintBatchForm.experimentRequiredCampusId || + item.campusId === constraintBatchForm.experimentRequiredCampusId) && + (!constraintBatchForm.experimentRequiredBuildingId || + item.buildingId === constraintBatchForm.experimentRequiredBuildingId) && (!(constraintBatchForm.allowedExperimentVenueNatures ?? []).length || (venueNatureValue(item.teachingVenueNature) & (constraintBatchForm.allowedExperimentVenueNatures ?? []) @@ -405,6 +414,8 @@ function openConstraintBatch() { requiredBuildingId: undefined, allowedClassroomIds: [], updateExperimentClassroomScope: false, + experimentRequiredCampusId: undefined, + experimentRequiredBuildingId: undefined, allowedExperimentVenueNatures: [], allowedExperimentClassroomIds: [], updateDays: false, @@ -463,6 +474,12 @@ async function saveConstraintBatch() { ? constraintBatchForm.allowedClassroomIds : null, updateExperimentClassroomScope, + experimentRequiredCampusId: updateExperimentClassroomScope + ? constraintBatchForm.experimentRequiredCampusId || null + : null, + experimentRequiredBuildingId: updateExperimentClassroomScope + ? constraintBatchForm.experimentRequiredBuildingId || null + : null, allowedExperimentVenueNatures: updateExperimentClassroomScope ? (constraintBatchForm.allowedExperimentVenueNatures ?? []) .reduce((value: number, nature: number) => value | nature, 0) @@ -1299,7 +1316,8 @@ onBeforeUnmount(() => { 仅约束实验课;不勾选时可使用全部实验教学场地。 - +
+
实验课指定可用场地
@@ -1312,16 +1330,18 @@ onBeforeUnmount(() => {
- - - - 可在上方场地性质范围内指定实验室;不选择时按场地性质自动筛选。 - + + + + + 可在上方场地性质范围内指定实验室;不选择时按场地性质自动筛选。 + +
@@ -1406,32 +1426,6 @@ onBeforeUnmount(() => { - - - 批量指定实验课场地 - -
- - - - {{ nature.label }} - - - - - - -
{ + + 批量指定实验课场地 + +
+ + + + {{ nature.label }} + + +
+ + + + + + + + + + +
+ + + + + +
修改允许上课日