修复实验排课

This commit is contained in:
2026-08-09 15:21:58 +08:00 Unverified
parent bb2b43b0d9
commit 1e60e46c7b
12 changed files with 6968 additions and 42 deletions
@@ -105,6 +105,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking() var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
.WhereIn(taskIds, x => x.TeachingTaskId) .WhereIn(taskIds, x => x.TeachingTaskId)
.Include(x => x.AllowedClassrooms) .Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken); .ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
return Ok(tasks.Select(task => return Ok(tasks.Select(task =>
{ {
@@ -137,6 +138,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
constraint?.LatestPeriod, constraint?.LatestPeriod,
AllowedClassroomIds = constraint?.AllowedClassrooms AllowedClassroomIds = constraint?.AllowedClassrooms
.Select(x => x.ClassroomId) ?? [], .Select(x => x.ClassroomId) ?? [],
AllowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
.Select(x => x.ClassroomId) ?? [],
AllowedExperimentVenueNatures = constraint?.AllowedExperimentVenueNatures ?? 0 AllowedExperimentVenueNatures = constraint?.AllowedExperimentVenueNatures ?? 0
}; };
})); }));
@@ -168,6 +171,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
{ {
var flexibleConstraint = await db.TeachingTaskScheduleConstraints var flexibleConstraint = await db.TeachingTaskScheduleConstraints
.Include(x => x.AllowedClassrooms) .Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken); .FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
if (flexibleConstraint is not null) if (flexibleConstraint is not null)
{ {
@@ -208,8 +212,23 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
allowedRooms.Any(x => x.Building!.CampusId != request.RequiredCampusId)) allowedRooms.Any(x => x.Building!.CampusId != request.RequiredCampusId))
return ValidationProblem("指定教室必须位于所选校区。"); return ValidationProblem("指定教室必须位于所选校区。");
var allowedExperimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? [];
var allowedExperimentRooms = await db.Classrooms.AsNoTracking()
.Where(x => x.IsEnabled)
.WhereIn(allowedExperimentRoomIds, x => x.Id)
.Include(x => x.Building)
.ToListAsync(cancellationToken);
if (allowedExperimentRooms.Count != allowedExperimentRoomIds.Length)
return ValidationProblem("部分指定实验场地不存在或已停用。");
if (building is not null && allowedExperimentRooms.Any(x => x.BuildingId != building.Id))
return ValidationProblem("指定实验场地必须位于所选教学楼。");
if (request.RequiredCampusId.HasValue &&
allowedExperimentRooms.Any(x => x.Building!.CampusId != request.RequiredCampusId))
return ValidationProblem("指定实验场地必须位于所选校区。");
var constraint = await db.TeachingTaskScheduleConstraints var constraint = await db.TeachingTaskScheduleConstraints
.Include(x => x.AllowedClassrooms) .Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken); .FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
if (constraint is null) if (constraint is null)
{ {
@@ -230,10 +249,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
constraint.LatestPeriod = request.LatestPeriod; constraint.LatestPeriod = request.LatestPeriod;
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures; constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures;
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms); db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(constraint.AllowedExperimentClassrooms);
constraint.AllowedClassrooms = request.RequiresClassroom constraint.AllowedClassrooms = request.RequiresClassroom
? request.AllowedClassroomIds.Distinct().Select(classroomId => ? request.AllowedClassroomIds.Distinct().Select(classroomId =>
new TeachingTaskAllowedClassroom { ClassroomId = classroomId }).ToList() new TeachingTaskAllowedClassroom { ClassroomId = classroomId }).ToList()
: []; : [];
constraint.AllowedExperimentClassrooms = request.RequiresClassroom
? allowedExperimentRoomIds.Select(classroomId =>
new TeachingTaskAllowedExperimentClassroom { ClassroomId = classroomId }).ToList()
: [];
await db.SaveChangesAsync(cancellationToken); await db.SaveChangesAsync(cancellationToken);
return NoContent(); return NoContent();
} }
@@ -256,21 +280,26 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
!Enum.IsDefined(request.SchedulingMode.Value)) !Enum.IsDefined(request.SchedulingMode.Value))
return ValidationProblem("授课方式无效。"); return ValidationProblem("授课方式无效。");
if (!request.SchedulingMode.HasValue && if (!request.SchedulingMode.HasValue &&
!request.RequiresClassroom.HasValue && !request.RequiresClassroom.HasValue &&
request.AllowedDayOfWeeks is null && request.AllowedDayOfWeeks is null &&
!request.UpdateClassroomScope && !request.UpdateClassroomScope &&
!request.UpdatePeriodRange && !request.UpdateExperimentClassroomScope &&
!request.AllowedExperimentVenueNatures.HasValue &&
!request.UpdatePeriodRange &&
!request.EarliestPeriod.HasValue && !request.EarliestPeriod.HasValue &&
!request.LatestPeriod.HasValue) !request.LatestPeriod.HasValue)
return ValidationProblem("请至少选择一项需要批量修改的设置。"); return ValidationProblem("请至少选择一项需要批量修改的设置。");
if (request.UpdateClassroomScope && request.RequiresClassroom == false) if (request.UpdateClassroomScope && request.RequiresClassroom == false)
return ValidationProblem("批量指定教室范围时,场地要求不能设置为不占用教室。"); return ValidationProblem("批量指定教室范围时,场地要求不能设置为不占用教室。");
if (request.UpdateExperimentClassroomScope && request.RequiresClassroom == false)
return ValidationProblem("批量指定实验场地时,场地要求不能设置为不占用教室。");
var tasks = await db.TeachingTasks var tasks = await db.TeachingTasks
.Where(x => .Where(x =>
x.AcademicTermId == request.AcademicTermId && x.AcademicTermId == request.AcademicTermId &&
x.Status == TeachingTaskStatus.Published) x.Status == TeachingTaskStatus.Published)
.WhereIn(taskIds, x => x.Id) .WhereIn(taskIds, x => x.Id)
.Include(x => x.Course)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
if (tasks.Count != taskIds.Length) if (tasks.Count != taskIds.Length)
return ValidationProblem("部分教学任务不存在、未发布或不属于当前学期。"); return ValidationProblem("部分教学任务不存在、未发布或不属于当前学期。");
@@ -281,9 +310,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
(request.SchedulingMode ?? task.SchedulingMode) == (request.SchedulingMode ?? task.SchedulingMode) ==
TeachingTaskSchedulingMode.Flexible)) TeachingTaskSchedulingMode.Flexible))
return ConflictProblem("非排时课程不能指定教室,请先将当前筛选结果限定为正常排课课程。"); return ConflictProblem("非排时课程不能指定教室,请先将当前筛选结果限定为正常排课课程。");
if (request.UpdateExperimentClassroomScope && tasks.Any(task =>
(request.SchedulingMode ?? task.SchedulingMode) ==
TeachingTaskSchedulingMode.Flexible))
return ConflictProblem("非排时课程不能指定实验场地,请先将当前筛选结果限定为正常排课课程。");
Building? building = null; Building? building = null;
List<Classroom> allowedRooms = []; List<Classroom> allowedRooms = [];
var experimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? [];
List<Classroom> allowedExperimentRooms = [];
if (request.UpdateClassroomScope) if (request.UpdateClassroomScope)
{ {
if (request.RequiredBuildingId.HasValue) if (request.RequiredBuildingId.HasValue)
@@ -320,10 +355,20 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
x.Building!.CampusId != request.RequiredCampusId)) x.Building!.CampusId != request.RequiredCampusId))
return ValidationProblem("指定教室必须位于所选校区。"); return ValidationProblem("指定教室必须位于所选校区。");
} }
if (request.UpdateExperimentClassroomScope)
{
allowedExperimentRooms = await db.Classrooms.AsNoTracking()
.Where(x => x.IsEnabled)
.WhereIn(experimentRoomIds, x => x.Id)
.ToListAsync(cancellationToken);
if (allowedExperimentRooms.Count != experimentRoomIds.Length)
return ValidationProblem("部分指定实验场地不存在或已停用。");
}
var constraints = await db.TeachingTaskScheduleConstraints var constraints = await db.TeachingTaskScheduleConstraints
.WhereIn(taskIds, x => x.TeachingTaskId) .WhereIn(taskIds, x => x.TeachingTaskId)
.Include(x => x.AllowedClassrooms) .Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken); .ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
foreach (var task in tasks) foreach (var task in tasks)
{ {
@@ -342,6 +387,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
request.RequiresClassroom.HasValue || request.RequiresClassroom.HasValue ||
request.AllowedDayOfWeeks is not null || request.AllowedDayOfWeeks is not null ||
request.UpdateClassroomScope || request.UpdateClassroomScope ||
request.UpdateExperimentClassroomScope ||
request.AllowedExperimentVenueNatures.HasValue ||
request.UpdatePeriodRange; request.UpdatePeriodRange;
if (!changesConstraint) continue; if (!changesConstraint) continue;
constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = task.Id }; constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = task.Id };
@@ -357,7 +404,10 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
constraint.RequiredCampusId = null; constraint.RequiredCampusId = null;
constraint.RequiredBuildingId = null; constraint.RequiredBuildingId = null;
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms); db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
constraint.AllowedExperimentClassrooms);
constraint.AllowedClassrooms = []; constraint.AllowedClassrooms = [];
constraint.AllowedExperimentClassrooms = [];
} }
} }
if (request.AllowedDayOfWeeks is not null) if (request.AllowedDayOfWeeks is not null)
@@ -379,6 +429,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
ClassroomId = room.Id ClassroomId = room.Id
}).ToList(); }).ToList();
} }
if (task.Course?.PracticeHours > 0 && request.AllowedExperimentVenueNatures.HasValue)
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures.Value;
if (task.Course?.PracticeHours > 0 && request.UpdateExperimentClassroomScope)
{
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
constraint.AllowedExperimentClassrooms);
constraint.AllowedExperimentClassrooms = allowedExperimentRooms.Select(room =>
new TeachingTaskAllowedExperimentClassroom { ClassroomId = room.Id }).ToList();
}
if (request.UpdatePeriodRange) if (request.UpdatePeriodRange)
{ {
constraint.EarliestPeriod = request.EarliestPeriod; constraint.EarliestPeriod = request.EarliestPeriod;
@@ -406,7 +465,9 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
constraint.EarliestPeriod = null; constraint.EarliestPeriod = null;
constraint.LatestPeriod = null; constraint.LatestPeriod = null;
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms); db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(constraint.AllowedExperimentClassrooms);
constraint.AllowedClassrooms = []; constraint.AllowedClassrooms = [];
constraint.AllowedExperimentClassrooms = [];
} }
private ActionResult ConflictProblem(string detail) => private ActionResult ConflictProblem(string detail) =>
@@ -441,7 +502,8 @@ public sealed record TeachingTaskScheduleConstraintRequest(
IReadOnlyList<int> AllowedDayOfWeeks, IReadOnlyList<int> AllowedDayOfWeeks,
[Range(1, 30)] int? EarliestPeriod, [Range(1, 30)] int? EarliestPeriod,
[Range(1, 30)] int? LatestPeriod, [Range(1, 30)] int? LatestPeriod,
TeachingVenueNature AllowedExperimentVenueNatures = 0); TeachingVenueNature AllowedExperimentVenueNatures = 0,
IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null);
public sealed record TeachingTaskScheduleConstraintBatchRequest( public sealed record TeachingTaskScheduleConstraintBatchRequest(
Guid AcademicTermId, Guid AcademicTermId,
@@ -455,4 +517,7 @@ public sealed record TeachingTaskScheduleConstraintBatchRequest(
IReadOnlyList<Guid>? AllowedClassroomIds, IReadOnlyList<Guid>? AllowedClassroomIds,
bool UpdatePeriodRange, bool UpdatePeriodRange,
[Range(1, 30)] int? EarliestPeriod, [Range(1, 30)] int? EarliestPeriod,
[Range(1, 30)] int? LatestPeriod); [Range(1, 30)] int? LatestPeriod,
bool UpdateExperimentClassroomScope = false,
TeachingVenueNature? AllowedExperimentVenueNatures = null,
IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null);
@@ -552,6 +552,7 @@ public sealed class SchedulesController(
var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking() var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking()
.Include(x => x.AllowedClassrooms) .Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.FirstOrDefaultAsync( .FirstOrDefaultAsync(
x => x.TeachingTaskId == request.TeachingTaskId, x => x.TeachingTaskId == request.TeachingTaskId,
cancellationToken); cancellationToken);
@@ -580,10 +581,6 @@ public sealed class SchedulesController(
x => x.Id == request.ClassroomId && x.IsEnabled, x => x.Id == request.ClassroomId && x.IsEnabled,
cancellationToken); cancellationToken);
if (classroom is null) return ValidationProblem("所选教室不存在或已停用。"); if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
if (request.Kind == ScheduleEntryKind.Experiment &&
!TeachingVenueNatureRules.SupportsExperiment(classroom.TeachingVenueNature))
return ValidationProblem(
$"实验课必须安排在具有实验教学性质的场地;“{classroom.Name}”未标注实验室、实训室、机房或语音室性质。 ");
if (constraint?.RequiredCampusId is Guid campusId && if (constraint?.RequiredCampusId is Guid campusId &&
classroom.Building!.CampusId != campusId) classroom.Building!.CampusId != campusId)
return ValidationProblem("所选教室不在该课程指定的校区。"); return ValidationProblem("所选教室不在该课程指定的校区。");
@@ -601,6 +598,13 @@ public sealed class SchedulesController(
allowedNatures != 0 && allowedNatures != 0 &&
(classroom.TeachingVenueNature & allowedNatures) == 0) (classroom.TeachingVenueNature & allowedNatures) == 0)
return ValidationProblem("所选场地不在该实验课允许的教学场地性质范围内。"); return ValidationProblem("所选场地不在该实验课允许的教学场地性质范围内。");
var allowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
.Select(x => x.ClassroomId)
.ToHashSet() ?? [];
if (request.Kind == ScheduleEntryKind.Experiment &&
allowedExperimentClassroomIds.Count > 0 &&
!allowedExperimentClassroomIds.Contains(classroom.Id))
return ValidationProblem("所选场地不在该实验课指定的场地范围内。");
} }
var studentCount = task.Classes.Sum(x => var studentCount = task.Classes.Sum(x =>
x.AdministrativeClass!.Students.Count(student => x.AdministrativeClass!.Students.Count(student =>
@@ -57,6 +57,7 @@ public sealed class TeachingTaskScheduleConstraint : EntityBase
public int? LatestPeriod { get; set; } public int? LatestPeriod { get; set; }
public TeachingVenueNature AllowedExperimentVenueNatures { get; set; } public TeachingVenueNature AllowedExperimentVenueNatures { get; set; }
public ICollection<TeachingTaskAllowedClassroom> AllowedClassrooms { get; set; } = []; public ICollection<TeachingTaskAllowedClassroom> AllowedClassrooms { get; set; } = [];
public ICollection<TeachingTaskAllowedExperimentClassroom> AllowedExperimentClassrooms { get; set; } = [];
} }
public sealed class TeachingTaskAllowedClassroom public sealed class TeachingTaskAllowedClassroom
@@ -67,6 +68,14 @@ public sealed class TeachingTaskAllowedClassroom
public Classroom? Classroom { get; set; } public Classroom? Classroom { get; set; }
} }
public sealed class TeachingTaskAllowedExperimentClassroom
{
public Guid TeachingTaskScheduleConstraintId { get; set; }
public TeachingTaskScheduleConstraint? TeachingTaskScheduleConstraint { get; set; }
public Guid ClassroomId { get; set; }
public Classroom? Classroom { get; set; }
}
public sealed class AutomaticScheduleJob : EntityBase public sealed class AutomaticScheduleJob : EntityBase
{ {
public Guid SchedulePlanId { get; set; } public Guid SchedulePlanId { get; set; }
@@ -38,6 +38,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
Set<TeachingTaskScheduleConstraint>(); Set<TeachingTaskScheduleConstraint>();
public DbSet<TeachingTaskAllowedClassroom> TeachingTaskAllowedClassrooms => public DbSet<TeachingTaskAllowedClassroom> TeachingTaskAllowedClassrooms =>
Set<TeachingTaskAllowedClassroom>(); Set<TeachingTaskAllowedClassroom>();
public DbSet<TeachingTaskAllowedExperimentClassroom> TeachingTaskAllowedExperimentClassrooms =>
Set<TeachingTaskAllowedExperimentClassroom>();
public DbSet<AutomaticScheduleJob> AutomaticScheduleJobs => public DbSet<AutomaticScheduleJob> AutomaticScheduleJobs =>
Set<AutomaticScheduleJob>(); Set<AutomaticScheduleJob>();
public DbSet<SchedulePublishJob> SchedulePublishJobs => public DbSet<SchedulePublishJob> SchedulePublishJobs =>
@@ -530,6 +532,19 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
}); });
builder.Entity<TeachingTaskAllowedExperimentClassroom>(entity =>
{
entity.HasKey(x => new { x.TeachingTaskScheduleConstraintId, x.ClassroomId });
entity.HasOne(x => x.TeachingTaskScheduleConstraint)
.WithMany(x => x.AllowedExperimentClassrooms)
.HasForeignKey(x => x.TeachingTaskScheduleConstraintId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(x => x.Classroom)
.WithMany()
.HasForeignKey(x => x.ClassroomId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<AutomaticScheduleJob>(entity => builder.Entity<AutomaticScheduleJob>(entity =>
{ {
entity.Property(x => x.ErrorMessage).HasMaxLength(2000); entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
@@ -92,6 +92,8 @@ public sealed class DevelopmentSqliteMigrator(
"20260809_48_teaching_task_grade_analytics"; "20260809_48_teaching_task_grade_analytics";
private const string SwaggerDocumentationSettingMigration = private const string SwaggerDocumentationSettingMigration =
"20260809_49_swagger_documentation_setting"; "20260809_49_swagger_documentation_setting";
private const string ExperimentClassroomConstraintsMigration =
"20260809_50_experiment_classroom_constraints";
public async Task MigrateAsync(CancellationToken cancellationToken = default) public async Task MigrateAsync(CancellationToken cancellationToken = default)
{ {
@@ -679,6 +681,14 @@ public sealed class DevelopmentSqliteMigrator(
SwaggerDocumentationSettingMigration, SwaggerDocumentationSettingMigration,
swaggerSettingsExist ? [] : SwaggerDocumentationSettingStatements, swaggerSettingsExist ? [] : SwaggerDocumentationSettingStatements,
cancellationToken); cancellationToken);
var experimentClassroomConstraintsExist = await db.Database
.SqlQueryRaw<int>(
"SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'TeachingTaskAllowedExperimentClassrooms'")
.AnyAsync(value => value > 0, cancellationToken);
await ApplyMigrationAsync(
ExperimentClassroomConstraintsMigration,
experimentClassroomConstraintsExist ? [] : ExperimentClassroomConstraintStatements,
cancellationToken);
} }
private async Task ApplyMigrationAsync( private async Task ApplyMigrationAsync(
@@ -2940,4 +2950,25 @@ public sealed class DevelopmentSqliteMigrator(
ON "SystemFeatureSettings" ("Key"); ON "SystemFeatureSettings" ("Key");
""" """
]; ];
private static readonly string[] ExperimentClassroomConstraintStatements =
[
"""
CREATE TABLE "TeachingTaskAllowedExperimentClassrooms" (
"TeachingTaskScheduleConstraintId" TEXT NOT NULL,
"ClassroomId" TEXT NOT NULL,
CONSTRAINT "PK_TeachingTaskAllowedExperimentClassrooms"
PRIMARY KEY ("TeachingTaskScheduleConstraintId", "ClassroomId"),
CONSTRAINT "FK_TeachingTaskAllowedExperimentClassrooms_Constraints"
FOREIGN KEY ("TeachingTaskScheduleConstraintId")
REFERENCES "TeachingTaskScheduleConstraints" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_TeachingTaskAllowedExperimentClassrooms_Classrooms"
FOREIGN KEY ("ClassroomId") REFERENCES "Classrooms" ("Id") ON DELETE RESTRICT
);
""",
"""
CREATE INDEX "IX_TeachingTaskAllowedExperimentClassrooms_ClassroomId"
ON "TeachingTaskAllowedExperimentClassrooms" ("ClassroomId");
"""
];
} }
@@ -0,0 +1,52 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class AddExperimentClassroomConstraints : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TeachingTaskAllowedExperimentClassrooms",
columns: table => new
{
TeachingTaskScheduleConstraintId = table.Column<Guid>(type: "char(36)", nullable: false),
ClassroomId = table.Column<Guid>(type: "char(36)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TeachingTaskAllowedExperimentClassrooms", x => new { x.TeachingTaskScheduleConstraintId, x.ClassroomId });
table.ForeignKey(
name: "FK_TeachingTaskAllowedExperimentClassrooms_Classrooms_Classroom~",
column: x => x.ClassroomId,
principalTable: "Classrooms",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_TeachingTaskAllowedExperimentClassrooms_TeachingTaskSchedule~",
column: x => x.TeachingTaskScheduleConstraintId,
principalTable: "TeachingTaskScheduleConstraints",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_TeachingTaskAllowedExperimentClassrooms_ClassroomId",
table: "TeachingTaskAllowedExperimentClassrooms",
column: "ClassroomId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TeachingTaskAllowedExperimentClassrooms");
}
}
}
@@ -4102,6 +4102,21 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.ToTable("TeachingTaskAllowedClassrooms"); b.ToTable("TeachingTaskAllowedClassrooms");
}); });
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedExperimentClassroom", b =>
{
b.Property<Guid>("TeachingTaskScheduleConstraintId")
.HasColumnType("char(36)");
b.Property<Guid>("ClassroomId")
.HasColumnType("char(36)");
b.HasKey("TeachingTaskScheduleConstraintId", "ClassroomId");
b.HasIndex("ClassroomId");
b.ToTable("TeachingTaskAllowedExperimentClassrooms");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b => modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b =>
{ {
b.Property<Guid>("TeachingTaskId") b.Property<Guid>("TeachingTaskId")
@@ -6198,6 +6213,25 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Navigation("TeachingTaskScheduleConstraint"); b.Navigation("TeachingTaskScheduleConstraint");
}); });
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskAllowedExperimentClassroom", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.Classroom", "Classroom")
.WithMany()
.HasForeignKey("ClassroomId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", "TeachingTaskScheduleConstraint")
.WithMany("AllowedExperimentClassrooms")
.HasForeignKey("TeachingTaskScheduleConstraintId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Classroom");
b.Navigation("TeachingTaskScheduleConstraint");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b => modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskClass", b =>
{ {
b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass") b.HasOne("Jiaowu.Api.Domain.Academic.AdministrativeClass", "AdministrativeClass")
@@ -6585,6 +6619,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b => modelBuilder.Entity("Jiaowu.Api.Domain.Academic.TeachingTaskScheduleConstraint", b =>
{ {
b.Navigation("AllowedClassrooms"); b.Navigation("AllowedClassrooms");
b.Navigation("AllowedExperimentClassrooms");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
@@ -44,6 +44,7 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking() var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
.WhereIn(taskIds, x => x.TeachingTaskId) .WhereIn(taskIds, x => x.TeachingTaskId)
.Include(x => x.AllowedClassrooms) .Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken); .ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
var classrooms = await db.Classrooms.AsNoTracking() var classrooms = await db.Classrooms.AsNoTracking()
.Where(x => x.IsEnabled) .Where(x => x.IsEnabled)
@@ -279,6 +280,9 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
var allowedRoomIds = constraint?.AllowedClassrooms var allowedRoomIds = constraint?.AllowedClassrooms
.Select(x => x.ClassroomId) .Select(x => x.ClassroomId)
.ToHashSet() ?? []; .ToHashSet() ?? [];
var allowedExperimentRoomIds = constraint?.AllowedExperimentClassrooms
.Select(x => x.ClassroomId)
.ToHashSet() ?? [];
var minimumCapacity = Math.Max( var minimumCapacity = Math.Max(
task.Capacity, task.Capacity,
task.Classes.Sum(x => task.Classes.Sum(x =>
@@ -291,11 +295,11 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
(constraint?.RequiredBuildingId is not Guid requiredBuildingId || (constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
room.BuildingId == requiredBuildingId) && room.BuildingId == requiredBuildingId) &&
(allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) && (allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) &&
(kind != ScheduleEntryKind.Experiment ||
TeachingVenueNatureRules.SupportsExperiment(room.TeachingVenueNature)) &&
(kind != ScheduleEntryKind.Experiment || constraint is null || (kind != ScheduleEntryKind.Experiment || constraint is null ||
constraint.AllowedExperimentVenueNatures == 0 || constraint.AllowedExperimentVenueNatures == 0 ||
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0)) (room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0) &&
(kind != ScheduleEntryKind.Experiment || allowedExperimentRoomIds.Count == 0 ||
allowedExperimentRoomIds.Contains(room.Id)))
.ToList(); .ToList();
} }
@@ -167,6 +167,7 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking() var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
.WhereIn(taskIds, x => x.TeachingTaskId) .WhereIn(taskIds, x => x.TeachingTaskId)
.Include(x => x.AllowedClassrooms) .Include(x => x.AllowedClassrooms)
.Include(x => x.AllowedExperimentClassrooms)
.ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken); .ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
foreach (var entry in plan.Entries) foreach (var entry in plan.Entries)
@@ -270,9 +271,6 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
{ {
if (classroom is null || !classroom.IsEnabled) if (classroom is null || !classroom.IsEnabled)
Fail(entry, "所选教室不存在或已停用"); Fail(entry, "所选教室不存在或已停用");
if (entry.Kind == ScheduleEntryKind.Experiment &&
!IsExperimentRoom(classroom.RoomType))
Fail(entry, $"实验课不能安排在“{classroom.RoomType}”类型的场地");
if (constraint?.RequiredCampusId is Guid campusId && if (constraint?.RequiredCampusId is Guid campusId &&
classroom.Building!.CampusId != campusId) classroom.Building!.CampusId != campusId)
Fail(entry, "所选教室不在指定校区"); Fail(entry, "所选教室不在指定校区");
@@ -285,6 +283,18 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
if (allowedClassroomIds.Count > 0 && if (allowedClassroomIds.Count > 0 &&
!allowedClassroomIds.Contains(classroom.Id)) !allowedClassroomIds.Contains(classroom.Id))
Fail(entry, "所选教室不在指定教室范围内"); Fail(entry, "所选教室不在指定教室范围内");
var allowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
.Select(x => x.ClassroomId)
.ToHashSet() ?? [];
if (entry.Kind == ScheduleEntryKind.Experiment &&
allowedExperimentClassroomIds.Count > 0 &&
!allowedExperimentClassroomIds.Contains(classroom.Id))
Fail(entry, "所选场地不在实验课指定场地范围内");
if (entry.Kind == ScheduleEntryKind.Experiment &&
constraint?.AllowedExperimentVenueNatures is { } allowedNatures &&
allowedNatures != 0 &&
(classroom.TeachingVenueNature & allowedNatures) == 0)
Fail(entry, "所选场地不在实验课允许的场地性质范围内");
} }
var studentCount = task.Classes.Sum(x => var studentCount = task.Classes.Sum(x =>
@@ -305,12 +315,6 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
.Select(int.Parse) .Select(int.Parse)
.ToHashSet(); .ToHashSet();
private static bool IsExperimentRoom(string roomType) =>
roomType.Contains("实验", StringComparison.OrdinalIgnoreCase) ||
roomType.Contains("实训", StringComparison.OrdinalIgnoreCase) ||
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase) ||
roomType.Contains("语音", StringComparison.OrdinalIgnoreCase);
[DoesNotReturn] [DoesNotReturn]
private static void Fail(ScheduleEntry entry, string message) private static void Fail(ScheduleEntry entry, string message)
{ {
+3 -3
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<JiaowuBackendVersion>2.3.2-beta.2</JiaowuBackendVersion> <JiaowuBackendVersion>2.3.2-beta.3</JiaowuBackendVersion>
<JiaowuFrontendVersion>2.3.2-beta.2</JiaowuFrontendVersion> <JiaowuFrontendVersion>2.3.2-beta.3</JiaowuFrontendVersion>
<JiaowuSwaggerVersion>2.3.2-beta.2</JiaowuSwaggerVersion> <JiaowuSwaggerVersion>2.3.2-beta.3</JiaowuSwaggerVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
+90 -15
View File
@@ -55,8 +55,10 @@ const weekdays = [
{ value: 7, label: '星期日' }, { value: 7, label: '星期日' },
] ]
const experimentVenueNatures = [ const experimentVenueNatures = [
{ value: 2, label: '实验室' }, { value: 4, label: '实室' }, { value: 1, label: '普通教室' }, { value: 2, label: '实室' },
{ value: 8, label: '计算机机房' }, { value: 16, label: '语音室' }, { value: 4, label: '实训室' }, { value: 8, label: '计算机机房' },
{ value: 16, label: '语音室' }, { value: 32, label: '体育场地' },
{ value: 64, label: '艺术场地' },
] ]
const periods = computed(() => { const periods = computed(() => {
const configured = timeSlots.value const configured = timeSlots.value
@@ -129,8 +131,6 @@ const publishStatusText = computed(() => {
const selectedTaskConstraint = computed(() => const selectedTaskConstraint = computed(() =>
constraints.value.find((item) => item.id === entryForm.teachingTaskId), constraints.value.find((item) => item.id === entryForm.teachingTaskId),
) )
const isExperimentRoom = (room: any) =>
(Number(room.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0
const entryClassrooms = computed(() => const entryClassrooms = computed(() =>
classrooms.value.filter((room) => classrooms.value.filter((room) =>
(!selectedTaskConstraint.value?.requiredCampusId (!selectedTaskConstraint.value?.requiredCampusId
@@ -139,7 +139,9 @@ const entryClassrooms = computed(() =>
|| room.buildingId === selectedTaskConstraint.value.requiredBuildingId) && || room.buildingId === selectedTaskConstraint.value.requiredBuildingId) &&
(!selectedTaskConstraint.value?.allowedClassroomIds?.length (!selectedTaskConstraint.value?.allowedClassroomIds?.length
|| selectedTaskConstraint.value.allowedClassroomIds.includes(room.id)) && || selectedTaskConstraint.value.allowedClassroomIds.includes(room.id)) &&
(entryForm.kind !== 'Experiment' || isExperimentRoom(room)), (entryForm.kind !== 'Experiment' ||
!selectedTaskConstraint.value?.allowedExperimentClassroomIds?.length ||
selectedTaskConstraint.value.allowedExperimentClassroomIds.includes(room.id)),
), ),
) )
const entryWeekdays = computed(() => { const entryWeekdays = computed(() => {
@@ -192,6 +194,13 @@ const filteredClassrooms = computed(() =>
(!constraintForm.requiredBuildingId || item.buildingId === constraintForm.requiredBuildingId), (!constraintForm.requiredBuildingId || item.buildingId === constraintForm.requiredBuildingId),
), ),
) )
const filteredExperimentClassrooms = computed(() =>
filteredClassrooms.value.filter((item) =>
(!(constraintForm.allowedExperimentVenueNatures ?? []).length ||
(Number(item.teachingVenueNature) & (constraintForm.allowedExperimentVenueNatures ?? [])
.reduce((value: number, nature: number) => value | nature, 0)) !== 0),
),
)
const batchFilteredBuildings = computed(() => const batchFilteredBuildings = computed(() =>
constraintBatchForm.requiredCampusId constraintBatchForm.requiredCampusId
? buildings.value.filter((item) => item.campusId === constraintBatchForm.requiredCampusId) ? buildings.value.filter((item) => item.campusId === constraintBatchForm.requiredCampusId)
@@ -205,6 +214,14 @@ const batchFilteredClassrooms = computed(() =>
item.buildingId === constraintBatchForm.requiredBuildingId), item.buildingId === constraintBatchForm.requiredBuildingId),
), ),
) )
const batchFilteredExperimentClassrooms = computed(() =>
batchFilteredClassrooms.value.filter((item) =>
(!(constraintBatchForm.allowedExperimentVenueNatures ?? []).length ||
(Number(item.teachingVenueNature) &
(constraintBatchForm.allowedExperimentVenueNatures ?? [])
.reduce((value: number, nature: number) => value | nature, 0)) !== 0),
),
)
const filteredEntries = computed(() => { const filteredEntries = computed(() => {
const text = keyword.value.trim().toLowerCase() const text = keyword.value.trim().toLowerCase()
if (!text) return selected.value?.entries ?? [] if (!text) return selected.value?.entries ?? []
@@ -293,11 +310,13 @@ function openConstraint(item: any) {
Object.assign(constraintForm, { Object.assign(constraintForm, {
teachingTaskId: item.id, teachingTaskId: item.id,
title: `${item.taskNumber} · ${item.name}`, title: `${item.taskNumber} · ${item.name}`,
coursePracticeHours: item.coursePracticeHours,
schedulingMode: item.schedulingMode, schedulingMode: item.schedulingMode,
requiresClassroom: item.requiresClassroom, requiresClassroom: item.requiresClassroom,
requiredCampusId: item.requiredCampusId, requiredCampusId: item.requiredCampusId,
requiredBuildingId: item.requiredBuildingId, requiredBuildingId: item.requiredBuildingId,
allowedClassroomIds: [...item.allowedClassroomIds], allowedClassroomIds: [...item.allowedClassroomIds],
allowedExperimentClassroomIds: [...item.allowedExperimentClassroomIds],
allowedExperimentVenueNatures: experimentVenueNatures allowedExperimentVenueNatures: experimentVenueNatures
.filter((nature) => (Number(item.allowedExperimentVenueNatures) & nature.value) !== 0) .filter((nature) => (Number(item.allowedExperimentVenueNatures) & nature.value) !== 0)
.map((nature) => nature.value), .map((nature) => nature.value),
@@ -318,6 +337,7 @@ async function saveConstraint() {
requiredCampusId: constraintForm.requiredCampusId || null, requiredCampusId: constraintForm.requiredCampusId || null,
requiredBuildingId: constraintForm.requiredBuildingId || null, requiredBuildingId: constraintForm.requiredBuildingId || null,
allowedClassroomIds: constraintForm.allowedClassroomIds ?? [], allowedClassroomIds: constraintForm.allowedClassroomIds ?? [],
allowedExperimentClassroomIds: constraintForm.allowedExperimentClassroomIds ?? [],
allowedExperimentVenueNatures: (constraintForm.allowedExperimentVenueNatures ?? []) allowedExperimentVenueNatures: (constraintForm.allowedExperimentVenueNatures ?? [])
.reduce((value: number, nature: number) => value | nature, 0), .reduce((value: number, nature: number) => value | nature, 0),
allowedDayOfWeeks: constraintForm.allowedDayOfWeeks ?? [], allowedDayOfWeeks: constraintForm.allowedDayOfWeeks ?? [],
@@ -358,6 +378,9 @@ function openConstraintBatch() {
requiredCampusId: undefined, requiredCampusId: undefined,
requiredBuildingId: undefined, requiredBuildingId: undefined,
allowedClassroomIds: [], allowedClassroomIds: [],
updateExperimentClassroomScope: false,
allowedExperimentVenueNatures: [],
allowedExperimentClassroomIds: [],
updateDays: false, updateDays: false,
allowedDayOfWeeks: [1, 2, 3, 4, 5], allowedDayOfWeeks: [1, 2, 3, 4, 5],
updatePeriodRange: false, updatePeriodRange: false,
@@ -369,8 +392,9 @@ function openConstraintBatch() {
async function saveConstraintBatch() { async function saveConstraintBatch() {
if (!constraintBatchForm.updateSchedulingMode && if (!constraintBatchForm.updateSchedulingMode &&
!constraintBatchForm.updateRequiresClassroom && !constraintBatchForm.updateRequiresClassroom &&
!constraintBatchForm.updateClassroomScope && !constraintBatchForm.updateClassroomScope &&
!constraintBatchForm.updateExperimentClassroomScope &&
!constraintBatchForm.updateDays && !constraintBatchForm.updateDays &&
!constraintBatchForm.updatePeriodRange) { !constraintBatchForm.updatePeriodRange) {
ElMessage.warning('请至少勾选一项需要批量修改的设置。') ElMessage.warning('请至少勾选一项需要批量修改的设置。')
@@ -390,6 +414,9 @@ async function saveConstraintBatch() {
!(constraintBatchForm.updateRequiresClassroom && !(constraintBatchForm.updateRequiresClassroom &&
!constraintBatchForm.requiresClassroom) && !constraintBatchForm.requiresClassroom) &&
constraintBatchForm.updateClassroomScope constraintBatchForm.updateClassroomScope
const updateExperimentClassroomScope = !flexible &&
!(constraintBatchForm.updateRequiresClassroom && !constraintBatchForm.requiresClassroom) &&
constraintBatchForm.updateExperimentClassroomScope
const { data } = await http.put('/schedules/constraints/batch', { const { data } = await http.put('/schedules/constraints/batch', {
academicTermId: termId.value, academicTermId: termId.value,
teachingTaskIds: targets.map((item) => item.id), teachingTaskIds: targets.map((item) => item.id),
@@ -409,6 +436,14 @@ async function saveConstraintBatch() {
allowedClassroomIds: updateClassroomScope allowedClassroomIds: updateClassroomScope
? constraintBatchForm.allowedClassroomIds ? constraintBatchForm.allowedClassroomIds
: null, : null,
updateExperimentClassroomScope,
allowedExperimentVenueNatures: updateExperimentClassroomScope
? (constraintBatchForm.allowedExperimentVenueNatures ?? [])
.reduce((value: number, nature: number) => value | nature, 0)
: null,
allowedExperimentClassroomIds: updateExperimentClassroomScope
? constraintBatchForm.allowedExperimentClassroomIds
: null,
allowedDayOfWeeks: !flexible && constraintBatchForm.updateDays allowedDayOfWeeks: !flexible && constraintBatchForm.updateDays
? constraintBatchForm.allowedDayOfWeeks ? constraintBatchForm.allowedDayOfWeeks
: null, : null,
@@ -711,7 +746,8 @@ function changeEntryTask() {
(task.requiredCampusId && room.campusId !== task.requiredCampusId) || (task.requiredCampusId && room.campusId !== task.requiredCampusId) ||
(task.requiredBuildingId && room.buildingId !== task.requiredBuildingId) || (task.requiredBuildingId && room.buildingId !== task.requiredBuildingId) ||
(task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id)) || (task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id)) ||
(entryForm.kind === 'Experiment' && !isExperimentRoom(room)) (entryForm.kind === 'Experiment' && task.allowedExperimentClassroomIds?.length &&
!task.allowedExperimentClassroomIds.includes(room.id))
)) { )) {
entryForm.classroomId = null entryForm.classroomId = null
} }
@@ -719,7 +755,9 @@ function changeEntryTask() {
function changeEntryKind() { function changeEntryKind() {
const room = classrooms.value.find((item) => item.id === entryForm.classroomId) const room = classrooms.value.find((item) => item.id === entryForm.classroomId)
if (entryForm.kind === 'Experiment' && room && !isExperimentRoom(room)) { const task = selectedTaskConstraint.value
if (entryForm.kind === 'Experiment' && room && task?.allowedExperimentClassroomIds?.length &&
!task.allowedExperimentClassroomIds.includes(room.id)) {
entryForm.classroomId = null entryForm.classroomId = null
} }
} }
@@ -1047,7 +1085,7 @@ onBeforeUnmount(() => {
/> />
<el-form-item <el-form-item
v-else v-else
:label="entryForm.kind === 'Experiment' ? '实验室 / 实训室 / 机房' : '教室'" :label="entryForm.kind === 'Experiment' ? '教学场地' : '教室'"
required required
:hint="selectedTaskConstraint?.requiredBuildingId ? '仅显示约束范围内教室' : ''" :hint="selectedTaskConstraint?.requiredBuildingId ? '仅显示约束范围内教室' : ''"
> >
@@ -1227,12 +1265,23 @@ onBeforeUnmount(() => {
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="实验课允许的场地性质"> <el-form-item label="实验课允许的场地性质">
<el-checkbox-group v-model="constraintForm.allowedExperimentVenueNatures"> <el-checkbox-group v-model="constraintForm.allowedExperimentVenueNatures">
<el-checkbox v-for="nature in experimentVenueNatures" :key="nature.value" :value="nature.value">{{ nature.label }}</el-checkbox> <el-checkbox v-for="nature in experimentVenueNatures" :key="nature.value" :value="nature.value">{{ nature.label }}</el-checkbox>
</el-checkbox-group> </el-checkbox-group>
<small class="field-hint">仅约束实验课不勾选时可使用全部实验教学场地</small> <small class="field-hint">仅约束实验课不勾选时可使用全部实验教学场地</small>
</el-form-item> </el-form-item>
<el-form-item v-if="constraintForm.coursePracticeHours" label="实验课指定可用场地">
<el-select v-model="constraintForm.allowedExperimentClassroomIds" multiple filterable collapse-tags>
<el-option
v-for="item in filteredExperimentClassrooms"
:key="item.id"
:label="`${item.buildingName} / ${item.name}${item.roomType}${item.capacity}人)`"
:value="item.id"
/>
</el-select>
<small class="field-hint">可在上方场地性质范围内指定实验室不选择时按场地性质自动筛选</small>
</el-form-item>
</template> </template>
<el-form-item label="允许上课日"> <el-form-item label="允许上课日">
<el-checkbox-group v-model="constraintForm.allowedDayOfWeeks"> <el-checkbox-group v-model="constraintForm.allowedDayOfWeeks">
@@ -1291,7 +1340,7 @@ onBeforeUnmount(() => {
<el-checkbox v-model="constraintBatchForm.updateClassroomScope"> <el-checkbox v-model="constraintBatchForm.updateClassroomScope">
批量指定教室范围 批量指定教室范围
</el-checkbox> </el-checkbox>
<div v-if="constraintBatchForm.updateClassroomScope" class="batch-classroom-scope"> <div v-if="constraintBatchForm.updateClassroomScope" class="batch-classroom-scope">
<el-alert <el-alert
title="将统一设置为需要教室;校区、教学楼和教室均不选择时,表示清除原有限定并允许系统自动分配。" title="将统一设置为需要教室;校区、教学楼和教室均不选择时,表示清除原有限定并允许系统自动分配。"
type="warning" type="warning"
@@ -1317,7 +1366,33 @@ onBeforeUnmount(() => {
<el-option v-for="item in batchFilteredBuildings" :key="item.id" :label="item.name" :value="item.id" /> <el-option v-for="item in batchFilteredBuildings" :key="item.id" :label="item.name" :value="item.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
</div> </div>
<el-checkbox v-model="constraintBatchForm.updateExperimentClassroomScope">
批量指定实验课场地
</el-checkbox>
<div v-if="constraintBatchForm.updateExperimentClassroomScope" class="batch-classroom-scope">
<el-alert
title="仅作用于含实验学时的教学任务;不选择具体场地时,按场地性质自动分配。"
type="warning"
:closable="false"
show-icon
/>
<el-form-item label="统一实验课允许的场地性质">
<el-checkbox-group v-model="constraintBatchForm.allowedExperimentVenueNatures">
<el-checkbox v-for="nature in experimentVenueNatures" :key="nature.value" :value="nature.value">{{ nature.label }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="统一实验课指定可用场地">
<el-select v-model="constraintBatchForm.allowedExperimentClassroomIds" multiple filterable collapse-tags placeholder="不选择则允许符合性质的任意实验场地">
<el-option
v-for="item in batchFilteredExperimentClassrooms"
:key="item.id"
:label="`${item.buildingName} / ${item.name}${item.roomType}${item.capacity}人)`"
:value="item.id"
/>
</el-select>
</el-form-item>
</div>
<el-form-item label="统一指定可用教室"> <el-form-item label="统一指定可用教室">
<el-select <el-select
v-model="constraintBatchForm.allowedClassroomIds" v-model="constraintBatchForm.allowedClassroomIds"