Merge branch 'codex/shiyan' into master
# Conflicts: # web/package-lock.json
This commit is contained in:
@@ -491,6 +491,7 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
|||||||
x.Building.Campus!.Name,
|
x.Building.Campus!.Name,
|
||||||
x.Capacity,
|
x.Capacity,
|
||||||
x.RoomType,
|
x.RoomType,
|
||||||
|
x.TeachingVenueNature,
|
||||||
x.Equipment,
|
x.Equipment,
|
||||||
x.IsEnabled,
|
x.IsEnabled,
|
||||||
x.SortOrder))
|
x.SortOrder))
|
||||||
@@ -557,6 +558,9 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
|||||||
BuildingId = request.BuildingId,
|
BuildingId = request.BuildingId,
|
||||||
Capacity = request.Capacity,
|
Capacity = request.Capacity,
|
||||||
RoomType = request.RoomType.Trim(),
|
RoomType = request.RoomType.Trim(),
|
||||||
|
TeachingVenueNature = request.TeachingVenueNature == 0
|
||||||
|
? TeachingVenueNature.GeneralClassroom
|
||||||
|
: request.TeachingVenueNature,
|
||||||
Equipment = request.Equipment?.Trim(),
|
Equipment = request.Equipment?.Trim(),
|
||||||
SortOrder = request.SortOrder,
|
SortOrder = request.SortOrder,
|
||||||
IsEnabled = request.IsEnabled
|
IsEnabled = request.IsEnabled
|
||||||
@@ -577,6 +581,9 @@ public sealed class BaseDataController(AppDbContext db, IAppCache cache) : Contr
|
|||||||
entity.BuildingId = request.BuildingId;
|
entity.BuildingId = request.BuildingId;
|
||||||
entity.Capacity = request.Capacity;
|
entity.Capacity = request.Capacity;
|
||||||
entity.RoomType = request.RoomType.Trim();
|
entity.RoomType = request.RoomType.Trim();
|
||||||
|
entity.TeachingVenueNature = request.TeachingVenueNature == 0
|
||||||
|
? TeachingVenueNature.GeneralClassroom
|
||||||
|
: request.TeachingVenueNature;
|
||||||
entity.Equipment = request.Equipment?.Trim();
|
entity.Equipment = request.Equipment?.Trim();
|
||||||
await SaveAndInvalidateAsync(cancellationToken);
|
await SaveAndInvalidateAsync(cancellationToken);
|
||||||
return entity;
|
return entity;
|
||||||
@@ -728,6 +735,7 @@ public sealed record ClassroomRequest(
|
|||||||
Guid BuildingId,
|
Guid BuildingId,
|
||||||
[Range(1, 1000)] int Capacity,
|
[Range(1, 1000)] int Capacity,
|
||||||
[Required, MaxLength(40)] string RoomType,
|
[Required, MaxLength(40)] string RoomType,
|
||||||
|
TeachingVenueNature TeachingVenueNature,
|
||||||
[MaxLength(300)] string? Equipment)
|
[MaxLength(300)] string? Equipment)
|
||||||
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
|
: CatalogRequest(Code, Name, SortOrder, IsEnabled);
|
||||||
|
|
||||||
@@ -784,6 +792,7 @@ public sealed record ClassroomListItem(
|
|||||||
string CampusName,
|
string CampusName,
|
||||||
int Capacity,
|
int Capacity,
|
||||||
string RoomType,
|
string RoomType,
|
||||||
|
TeachingVenueNature TeachingVenueNature,
|
||||||
string? Equipment,
|
string? Equipment,
|
||||||
bool IsEnabled,
|
bool IsEnabled,
|
||||||
int SortOrder);
|
int SortOrder);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
|||||||
["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"],
|
["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"],
|
||||||
["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"],
|
["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"],
|
||||||
["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"],
|
["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"],
|
||||||
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "设备", "排序", "状态"],
|
["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "教学场地性质", "设备", "排序", "状态"],
|
||||||
["course-categories"] = ["编码", "名称", "排序", "状态"]
|
["course-categories"] = ["编码", "名称", "排序", "状态"]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +70,10 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
|||||||
IReadOnlyList<ExcelRow> rows;
|
IReadOnlyList<ExcelRow> rows;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
rows = await ExcelWorkbookHelper.ReadAsync(file, headers, cancellationToken);
|
var requiredHeaders = kind.Equals("classrooms", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? headers.Where(x => x != "教学场地性质").ToArray()
|
||||||
|
: headers;
|
||||||
|
rows = await ExcelWorkbookHelper.ReadAsync(file, requiredHeaders, cancellationToken);
|
||||||
}
|
}
|
||||||
catch (InvalidDataException exception)
|
catch (InvalidDataException exception)
|
||||||
{
|
{
|
||||||
@@ -165,6 +168,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
|||||||
"classrooms" => (await db.Classrooms.AsNoTracking().Include(x => x.Building)
|
"classrooms" => (await db.Classrooms.AsNoTracking().Include(x => x.Building)
|
||||||
.OrderBy(x => x.Code).ToListAsync(cancellationToken))
|
.OrderBy(x => x.Code).ToListAsync(cancellationToken))
|
||||||
.Select(x => Row(x.Code, x.Name, x.Building!.Code, x.Capacity, x.RoomType,
|
.Select(x => Row(x.Code, x.Name, x.Building!.Code, x.Capacity, x.RoomType,
|
||||||
|
VenueNatureName(x.TeachingVenueNature),
|
||||||
x.Equipment, x.SortOrder, Status(x.IsEnabled))).ToList(),
|
x.Equipment, x.SortOrder, Status(x.IsEnabled))).ToList(),
|
||||||
"course-categories" => (await db.CourseCategories.AsNoTracking()
|
"course-categories" => (await db.CourseCategories.AsNoTracking()
|
||||||
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
|
.OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
|
||||||
@@ -473,8 +477,9 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
|||||||
var buildingCode = Required(row, "所属教学楼编码", errors);
|
var buildingCode = Required(row, "所属教学楼编码", errors);
|
||||||
var capacity = ParseInt(row, "容量", 1, 1000, errors);
|
var capacity = ParseInt(row, "容量", 1, 1000, errors);
|
||||||
var roomType = Required(row, "教室类型", errors);
|
var roomType = Required(row, "教室类型", errors);
|
||||||
|
var venueNature = ParseVenueNature(row, roomType, errors);
|
||||||
if (code is null || name is null || buildingCode is null ||
|
if (code is null || name is null || buildingCode is null ||
|
||||||
capacity is null || roomType is null) continue;
|
capacity is null || roomType is null || venueNature is null) continue;
|
||||||
if (!buildings.TryGetValue(buildingCode, out var building))
|
if (!buildings.TryGetValue(buildingCode, out var building))
|
||||||
{
|
{
|
||||||
errors.Add($"第 {row.RowNumber} 行:所属教学楼编码“{buildingCode}”不存在。");
|
errors.Add($"第 {row.RowNumber} 行:所属教学楼编码“{buildingCode}”不存在。");
|
||||||
@@ -488,7 +493,8 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
|||||||
Code = code,
|
Code = code,
|
||||||
Name = name,
|
Name = name,
|
||||||
BuildingId = building.Id,
|
BuildingId = building.Id,
|
||||||
RoomType = roomType
|
RoomType = roomType,
|
||||||
|
TeachingVenueNature = venueNature.Value
|
||||||
};
|
};
|
||||||
db.Classrooms.Add(entity);
|
db.Classrooms.Add(entity);
|
||||||
existing[code] = entity;
|
existing[code] = entity;
|
||||||
@@ -499,6 +505,7 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
|||||||
entity.BuildingId = building.Id;
|
entity.BuildingId = building.Id;
|
||||||
entity.Capacity = capacity.Value;
|
entity.Capacity = capacity.Value;
|
||||||
entity.RoomType = roomType;
|
entity.RoomType = roomType;
|
||||||
|
entity.TeachingVenueNature = venueNature.Value;
|
||||||
entity.Equipment = Optional(row, "设备");
|
entity.Equipment = Optional(row, "设备");
|
||||||
}
|
}
|
||||||
return new(created, updated, rows.Count);
|
return new(created, updated, rows.Count);
|
||||||
@@ -597,6 +604,57 @@ public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) :
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TeachingVenueNature? ParseVenueNature(
|
||||||
|
ExcelRow row,
|
||||||
|
string? roomType,
|
||||||
|
List<string> errors)
|
||||||
|
{
|
||||||
|
var value = Optional(row, "教学场地性质");
|
||||||
|
if (value is null) return InferVenueNature(roomType ?? string.Empty);
|
||||||
|
var result = (TeachingVenueNature)0;
|
||||||
|
foreach (var part in value.Split(['、', ',', ',', ';', ';'],
|
||||||
|
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
result |= part switch
|
||||||
|
{
|
||||||
|
"普通教室" => TeachingVenueNature.GeneralClassroom,
|
||||||
|
"实验室" => TeachingVenueNature.Laboratory,
|
||||||
|
"实训室" => TeachingVenueNature.TrainingRoom,
|
||||||
|
"计算机机房" or "机房" => TeachingVenueNature.ComputerLab,
|
||||||
|
"语音室" => TeachingVenueNature.LanguageLab,
|
||||||
|
"体育场地" => TeachingVenueNature.SportsVenue,
|
||||||
|
"艺术场地" => TeachingVenueNature.ArtsVenue,
|
||||||
|
_ => (TeachingVenueNature)0
|
||||||
|
};
|
||||||
|
if (part is not ("普通教室" or "实验室" or "实训室" or "计算机机房" or "机房" or "语音室" or "体育场地" or "艺术场地"))
|
||||||
|
errors.Add($"第 {row.RowNumber} 行:“教学场地性质”包含不支持的值“{part}”。");
|
||||||
|
}
|
||||||
|
return result == 0 ? null : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TeachingVenueNature InferVenueNature(string roomType) =>
|
||||||
|
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? TeachingVenueNature.Laboratory | TeachingVenueNature.ComputerLab
|
||||||
|
: roomType.Contains("语音", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? TeachingVenueNature.Laboratory | TeachingVenueNature.LanguageLab
|
||||||
|
: roomType.Contains("实训", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? TeachingVenueNature.TrainingRoom
|
||||||
|
: roomType.Contains("实验", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? TeachingVenueNature.Laboratory
|
||||||
|
: TeachingVenueNature.GeneralClassroom;
|
||||||
|
|
||||||
|
private static string VenueNatureName(TeachingVenueNature value) => string.Join("、",
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
(TeachingVenueNature.GeneralClassroom, "普通教室"),
|
||||||
|
(TeachingVenueNature.Laboratory, "实验室"),
|
||||||
|
(TeachingVenueNature.TrainingRoom, "实训室"),
|
||||||
|
(TeachingVenueNature.ComputerLab, "计算机机房"),
|
||||||
|
(TeachingVenueNature.LanguageLab, "语音室"),
|
||||||
|
(TeachingVenueNature.SportsVenue, "体育场地"),
|
||||||
|
(TeachingVenueNature.ArtsVenue, "艺术场地")
|
||||||
|
}.Where(x => (value & x.Item1) != 0).Select(x => x.Item2));
|
||||||
|
|
||||||
private static bool ParseBoolean(
|
private static bool ParseBoolean(
|
||||||
ExcelRow row, string header, bool defaultValue, List<string> errors)
|
ExcelRow row, string header, bool defaultValue, List<string> errors)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -91,6 +91,36 @@ public sealed class ExperimentsController(
|
|||||||
.Select(item => item.AdministrativeClass!.Name)
|
.Select(item => item.AdministrativeClass!.Name)
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken),
|
.ToListAsync(cancellationToken),
|
||||||
|
ScheduleEntries = await db.ScheduleEntries.AsNoTracking()
|
||||||
|
.Where(x => x.SchedulePlan!.Status == SchedulePlanStatus.Published &&
|
||||||
|
x.Kind == ScheduleEntryKind.Experiment &&
|
||||||
|
x.ClassroomId.HasValue &&
|
||||||
|
AccessibleTeachingTasks().Select(task => task.Id)
|
||||||
|
.Contains(x.TeachingTaskId))
|
||||||
|
.Where(x => !academicTermId.HasValue ||
|
||||||
|
x.SchedulePlan!.AcademicTermId == academicTermId.Value)
|
||||||
|
.OrderBy(x => x.TeachingTask!.Course!.Code)
|
||||||
|
.ThenBy(x => x.TeachingTask!.TaskNumber)
|
||||||
|
.ThenBy(x => x.DayOfWeek)
|
||||||
|
.ThenBy(x => x.StartPeriod)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.TeachingTaskId,
|
||||||
|
TaskNumber = x.TeachingTask!.TaskNumber,
|
||||||
|
CourseCode = x.TeachingTask.Course!.Code,
|
||||||
|
CourseName = x.TeachingTask.Course.Name,
|
||||||
|
x.DayOfWeek,
|
||||||
|
x.StartPeriod,
|
||||||
|
x.PeriodCount,
|
||||||
|
x.StartWeek,
|
||||||
|
x.EndWeek,
|
||||||
|
x.WeekPattern,
|
||||||
|
ClassroomName = x.Classroom!.Name,
|
||||||
|
BuildingName = x.Classroom.Building!.Name,
|
||||||
|
CampusName = x.Classroom.Building.Campus!.Name
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken),
|
||||||
Classrooms = await db.Classrooms.AsNoTracking()
|
Classrooms = await db.Classrooms.AsNoTracking()
|
||||||
.Where(x => x.IsEnabled)
|
.Where(x => x.IsEnabled)
|
||||||
.OrderBy(x => x.Building!.Campus!.SortOrder)
|
.OrderBy(x => x.Building!.Campus!.SortOrder)
|
||||||
@@ -103,6 +133,7 @@ public sealed class ExperimentsController(
|
|||||||
BuildingName = x.Building!.Name,
|
BuildingName = x.Building!.Name,
|
||||||
CampusName = x.Building.Campus!.Name,
|
CampusName = x.Building.Campus!.Name,
|
||||||
x.Capacity
|
x.Capacity
|
||||||
|
,x.TeachingVenueNature
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken),
|
.ToListAsync(cancellationToken),
|
||||||
Periods = periodItems
|
Periods = periodItems
|
||||||
@@ -135,6 +166,7 @@ public sealed class ExperimentsController(
|
|||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
x.TeachingTaskId,
|
x.TeachingTaskId,
|
||||||
|
x.ScheduleEntryId,
|
||||||
x.Code,
|
x.Code,
|
||||||
x.Name,
|
x.Name,
|
||||||
x.ArrangementMode,
|
x.ArrangementMode,
|
||||||
@@ -157,6 +189,18 @@ public sealed class ExperimentsController(
|
|||||||
ClassNames = x.TeachingTask.Classes
|
ClassNames = x.TeachingTask.Classes
|
||||||
.OrderBy(item => item.AdministrativeClass!.Code)
|
.OrderBy(item => item.AdministrativeClass!.Code)
|
||||||
.Select(item => item.AdministrativeClass!.Name),
|
.Select(item => item.AdministrativeClass!.Name),
|
||||||
|
ScheduleEntry = x.ScheduleEntryId == null ? null : new
|
||||||
|
{
|
||||||
|
x.ScheduleEntry!.DayOfWeek,
|
||||||
|
x.ScheduleEntry.StartPeriod,
|
||||||
|
x.ScheduleEntry.PeriodCount,
|
||||||
|
x.ScheduleEntry.StartWeek,
|
||||||
|
x.ScheduleEntry.EndWeek,
|
||||||
|
x.ScheduleEntry.WeekPattern,
|
||||||
|
ClassroomName = x.ScheduleEntry.Classroom!.Name,
|
||||||
|
BuildingName = x.ScheduleEntry.Classroom.Building!.Name,
|
||||||
|
CampusName = x.ScheduleEntry.Classroom.Building.Campus!.Name
|
||||||
|
},
|
||||||
Sessions = x.Sessions
|
Sessions = x.Sessions
|
||||||
.OrderBy(item => item.SessionDate)
|
.OrderBy(item => item.SessionDate)
|
||||||
.ThenBy(item => item.StartPeriod)
|
.ThenBy(item => item.StartPeriod)
|
||||||
@@ -209,6 +253,7 @@ public sealed class ExperimentsController(
|
|||||||
x.Code,
|
x.Code,
|
||||||
x.Name,
|
x.Name,
|
||||||
x.ArrangementMode,
|
x.ArrangementMode,
|
||||||
|
x.ScheduleEntryId,
|
||||||
x.Description,
|
x.Description,
|
||||||
x.Requirements,
|
x.Requirements,
|
||||||
x.StartDate,
|
x.StartDate,
|
||||||
@@ -222,6 +267,18 @@ public sealed class ExperimentsController(
|
|||||||
TeacherNames = x.TeachingTask.Teachers
|
TeacherNames = x.TeachingTask.Teachers
|
||||||
.OrderByDescending(item => item.IsPrimary)
|
.OrderByDescending(item => item.IsPrimary)
|
||||||
.Select(item => item.Teacher!.Name),
|
.Select(item => item.Teacher!.Name),
|
||||||
|
ScheduleEntry = x.ScheduleEntryId == null ? null : new
|
||||||
|
{
|
||||||
|
x.ScheduleEntry!.DayOfWeek,
|
||||||
|
x.ScheduleEntry.StartPeriod,
|
||||||
|
x.ScheduleEntry.PeriodCount,
|
||||||
|
x.ScheduleEntry.StartWeek,
|
||||||
|
x.ScheduleEntry.EndWeek,
|
||||||
|
x.ScheduleEntry.WeekPattern,
|
||||||
|
ClassroomName = x.ScheduleEntry.Classroom!.Name,
|
||||||
|
BuildingName = x.ScheduleEntry.Classroom.Building!.Name,
|
||||||
|
CampusName = x.ScheduleEntry.Classroom.Building.Campus!.Name
|
||||||
|
},
|
||||||
Sessions = x.Sessions
|
Sessions = x.Sessions
|
||||||
.Where(item => item.Status == ExperimentSessionStatus.Scheduled)
|
.Where(item => item.Status == ExperimentSessionStatus.Scheduled)
|
||||||
.OrderBy(item => item.SessionDate)
|
.OrderBy(item => item.SessionDate)
|
||||||
@@ -272,6 +329,9 @@ public sealed class ExperimentsController(
|
|||||||
|
|
||||||
var problem = ValidateProjectRequest(request, task.AcademicTerm!);
|
var problem = ValidateProjectRequest(request, task.AcademicTerm!);
|
||||||
if (problem is not null) return ValidationProblem(problem);
|
if (problem is not null) return ValidationProblem(problem);
|
||||||
|
var scheduleEntry = await ValidateScheduleEntryAsync(
|
||||||
|
request, task.Id, cancellationToken);
|
||||||
|
if (scheduleEntry.Problem is not null) return ValidationProblem(scheduleEntry.Problem);
|
||||||
|
|
||||||
var code = request.Code.Trim();
|
var code = request.Code.Trim();
|
||||||
if (await db.ExperimentProjects.AnyAsync(x =>
|
if (await db.ExperimentProjects.AnyAsync(x =>
|
||||||
@@ -283,6 +343,7 @@ public sealed class ExperimentsController(
|
|||||||
var project = new ExperimentProject
|
var project = new ExperimentProject
|
||||||
{
|
{
|
||||||
TeachingTaskId = request.TeachingTaskId,
|
TeachingTaskId = request.TeachingTaskId,
|
||||||
|
ScheduleEntryId = scheduleEntry.Entry?.Id,
|
||||||
Code = code,
|
Code = code,
|
||||||
Name = request.Name.Trim(),
|
Name = request.Name.Trim(),
|
||||||
ArrangementMode = request.ArrangementMode,
|
ArrangementMode = request.ArrangementMode,
|
||||||
@@ -302,6 +363,8 @@ public sealed class ExperimentsController(
|
|||||||
ExperimentProjectBatchRequest request,
|
ExperimentProjectBatchRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (request.ArrangementMode == ExperimentArrangementMode.Centralized)
|
||||||
|
return ValidationProblem("集中安排的实验项目请逐项绑定已发布课表中的实验课。");
|
||||||
var taskIds = request.TeachingTaskIds
|
var taskIds = request.TeachingTaskIds
|
||||||
.Where(x => x != Guid.Empty)
|
.Where(x => x != Guid.Empty)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
@@ -387,6 +450,9 @@ public sealed class ExperimentsController(
|
|||||||
request,
|
request,
|
||||||
project.TeachingTask!.AcademicTerm!);
|
project.TeachingTask!.AcademicTerm!);
|
||||||
if (problem is not null) return ValidationProblem(problem);
|
if (problem is not null) return ValidationProblem(problem);
|
||||||
|
var scheduleEntry = await ValidateScheduleEntryAsync(
|
||||||
|
request, project.TeachingTaskId, cancellationToken);
|
||||||
|
if (scheduleEntry.Problem is not null) return ValidationProblem(scheduleEntry.Problem);
|
||||||
|
|
||||||
var code = request.Code.Trim();
|
var code = request.Code.Trim();
|
||||||
if (await db.ExperimentProjects.AnyAsync(x =>
|
if (await db.ExperimentProjects.AnyAsync(x =>
|
||||||
@@ -399,6 +465,7 @@ public sealed class ExperimentsController(
|
|||||||
project.Code = code;
|
project.Code = code;
|
||||||
project.Name = request.Name.Trim();
|
project.Name = request.Name.Trim();
|
||||||
project.ArrangementMode = request.ArrangementMode;
|
project.ArrangementMode = request.ArrangementMode;
|
||||||
|
project.ScheduleEntryId = scheduleEntry.Entry?.Id;
|
||||||
project.Description = Normalize(request.Description);
|
project.Description = Normalize(request.Description);
|
||||||
project.Requirements = Normalize(request.Requirements);
|
project.Requirements = Normalize(request.Requirements);
|
||||||
project.StartDate = request.StartDate;
|
project.StartDate = request.StartDate;
|
||||||
@@ -438,9 +505,14 @@ public sealed class ExperimentsController(
|
|||||||
if (project is null) return NotFound();
|
if (project is null) return NotFound();
|
||||||
if (project.Status != ExperimentProjectStatus.Draft)
|
if (project.Status != ExperimentProjectStatus.Draft)
|
||||||
return ConflictProblem("只有草稿实验项目可以发布。");
|
return ConflictProblem("只有草稿实验项目可以发布。");
|
||||||
if (!project.Sessions.Any(x =>
|
var hasSchedule = project.ArrangementMode == ExperimentArrangementMode.Centralized
|
||||||
x.Status == ExperimentSessionStatus.Scheduled))
|
? project.ScheduleEntryId.HasValue || project.Sessions.Any(x =>
|
||||||
return ConflictProblem("请至少安排一个有效实验场次后再发布。");
|
x.Status == ExperimentSessionStatus.Scheduled)
|
||||||
|
: project.Sessions.Any(x => x.Status == ExperimentSessionStatus.Scheduled);
|
||||||
|
if (!hasSchedule)
|
||||||
|
return ConflictProblem(project.ArrangementMode == ExperimentArrangementMode.Centralized
|
||||||
|
? "请先绑定已发布课表中的实验课后再发布。"
|
||||||
|
: "请至少安排一个有效实验场次后再发布。");
|
||||||
if (project.Sessions.Any(x =>
|
if (project.Sessions.Any(x =>
|
||||||
x.Status == ExperimentSessionStatus.Scheduled &&
|
x.Status == ExperimentSessionStatus.Scheduled &&
|
||||||
(x.SessionDate < project.StartDate ||
|
(x.SessionDate < project.StartDate ||
|
||||||
@@ -506,6 +578,8 @@ public sealed class ExperimentsController(
|
|||||||
if (project is null) return NotFound();
|
if (project is null) return NotFound();
|
||||||
if (project.Status == ExperimentProjectStatus.Closed)
|
if (project.Status == ExperimentProjectStatus.Closed)
|
||||||
return ConflictProblem("已关闭实验项目不能再增加场次。");
|
return ConflictProblem("已关闭实验项目不能再增加场次。");
|
||||||
|
if (project.ArrangementMode == ExperimentArrangementMode.Centralized)
|
||||||
|
return ConflictProblem("集中安排的实验项目直接使用已发布课表中的实验课,不能在此重复排时派地点。");
|
||||||
|
|
||||||
var problem = await ValidateSessionAsync(
|
var problem = await ValidateSessionAsync(
|
||||||
project,
|
project,
|
||||||
@@ -588,6 +662,9 @@ public sealed class ExperimentsController(
|
|||||||
if (project.Status == ExperimentProjectStatus.Closed)
|
if (project.Status == ExperimentProjectStatus.Closed)
|
||||||
return ConflictProblem(
|
return ConflictProblem(
|
||||||
$"实验项目“{project.Name}”已关闭,不能再增加场次。");
|
$"实验项目“{project.Name}”已关闭,不能再增加场次。");
|
||||||
|
if (project.ArrangementMode == ExperimentArrangementMode.Centralized)
|
||||||
|
return ConflictProblem(
|
||||||
|
$"实验项目“{project.Name}”为集中安排,请直接使用已发布课表中的实验课。");
|
||||||
|
|
||||||
var sessionRequest = item.ToSessionRequest();
|
var sessionRequest = item.ToSessionRequest();
|
||||||
var problem = await ValidateSessionAsync(
|
var problem = await ValidateSessionAsync(
|
||||||
@@ -930,6 +1007,8 @@ public sealed class ExperimentsController(
|
|||||||
x.Id == request.ClassroomId && x.IsEnabled,
|
x.Id == request.ClassroomId && x.IsEnabled,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
if (classroom is null) return "实验教室不存在或已停用。";
|
if (classroom is null) return "实验教室不存在或已停用。";
|
||||||
|
if (!TeachingVenueNatureRules.SupportsExperiment(classroom.TeachingVenueNature))
|
||||||
|
return "所选场地未标注实验教学性质。";
|
||||||
if (request.Capacity > classroom.Capacity)
|
if (request.Capacity > classroom.Capacity)
|
||||||
return $"场次容量不能超过教室容量 {classroom.Capacity} 人。";
|
return $"场次容量不能超过教室容量 {classroom.Capacity} 人。";
|
||||||
if (project.ArrangementMode ==
|
if (project.ArrangementMode ==
|
||||||
@@ -1173,6 +1252,30 @@ public sealed class ExperimentsController(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<(ScheduleEntry? Entry, string? Problem)> ValidateScheduleEntryAsync(
|
||||||
|
ExperimentProjectRequest request,
|
||||||
|
Guid teachingTaskId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.ArrangementMode == ExperimentArrangementMode.SelfScheduled)
|
||||||
|
return request.ScheduleEntryId.HasValue
|
||||||
|
? (null, "自行安排的实验项目不能绑定课表实验课。")
|
||||||
|
: (null, null);
|
||||||
|
if (!request.ScheduleEntryId.HasValue)
|
||||||
|
return (null, "集中安排的实验项目必须绑定已发布课表中的实验课。");
|
||||||
|
|
||||||
|
var entry = await db.ScheduleEntries
|
||||||
|
.Include(x => x.SchedulePlan)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.ScheduleEntryId, cancellationToken);
|
||||||
|
if (entry is null || entry.SchedulePlan!.Status != SchedulePlanStatus.Published ||
|
||||||
|
entry.Kind != ScheduleEntryKind.Experiment || !entry.ClassroomId.HasValue ||
|
||||||
|
entry.TeachingTaskId != teachingTaskId)
|
||||||
|
return (null, "只能绑定本教学任务已发布、已安排实验室的实验课。");
|
||||||
|
if (!await AccessibleTeachingTasks().AnyAsync(x => x.Id == teachingTaskId, cancellationToken))
|
||||||
|
return (null, "该教学任务不在当前管理范围内。");
|
||||||
|
return (entry, null);
|
||||||
|
}
|
||||||
|
|
||||||
private static string? Normalize(string? value) =>
|
private static string? Normalize(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
@@ -1193,7 +1296,8 @@ public sealed record ExperimentProjectRequest(
|
|||||||
[MaxLength(1000)] string? Description,
|
[MaxLength(1000)] string? Description,
|
||||||
[MaxLength(1000)] string? Requirements,
|
[MaxLength(1000)] string? Requirements,
|
||||||
DateOnly StartDate,
|
DateOnly StartDate,
|
||||||
DateOnly EndDate);
|
DateOnly EndDate,
|
||||||
|
Guid? ScheduleEntryId = null);
|
||||||
|
|
||||||
public sealed record ExperimentProjectBatchRequest(
|
public sealed record ExperimentProjectBatchRequest(
|
||||||
[Required] IReadOnlyList<Guid> TeachingTaskIds,
|
[Required] IReadOnlyList<Guid> TeachingTaskIds,
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
constraint?.EarliestPeriod,
|
constraint?.EarliestPeriod,
|
||||||
constraint?.LatestPeriod,
|
constraint?.LatestPeriod,
|
||||||
AllowedClassroomIds = constraint?.AllowedClassrooms
|
AllowedClassroomIds = constraint?.AllowedClassrooms
|
||||||
.Select(x => x.ClassroomId) ?? []
|
.Select(x => x.ClassroomId) ?? [],
|
||||||
|
AllowedExperimentVenueNatures = constraint?.AllowedExperimentVenueNatures ?? 0
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -227,6 +228,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
: string.Join(',', request.AllowedDayOfWeeks.Distinct().Order());
|
: string.Join(',', request.AllowedDayOfWeeks.Distinct().Order());
|
||||||
constraint.EarliestPeriod = request.EarliestPeriod;
|
constraint.EarliestPeriod = request.EarliestPeriod;
|
||||||
constraint.LatestPeriod = request.LatestPeriod;
|
constraint.LatestPeriod = request.LatestPeriod;
|
||||||
|
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures;
|
||||||
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
|
||||||
constraint.AllowedClassrooms = request.RequiresClassroom
|
constraint.AllowedClassrooms = request.RequiresClassroom
|
||||||
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
|
? request.AllowedClassroomIds.Distinct().Select(classroomId =>
|
||||||
@@ -438,7 +440,8 @@ public sealed record TeachingTaskScheduleConstraintRequest(
|
|||||||
IReadOnlyList<Guid> AllowedClassroomIds,
|
IReadOnlyList<Guid> AllowedClassroomIds,
|
||||||
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);
|
||||||
|
|
||||||
public sealed record TeachingTaskScheduleConstraintBatchRequest(
|
public sealed record TeachingTaskScheduleConstraintBatchRequest(
|
||||||
Guid AcademicTermId,
|
Guid AcademicTermId,
|
||||||
|
|||||||
@@ -581,9 +581,9 @@ public sealed class SchedulesController(
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
|
if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
|
||||||
if (request.Kind == ScheduleEntryKind.Experiment &&
|
if (request.Kind == ScheduleEntryKind.Experiment &&
|
||||||
!IsExperimentRoom(classroom.RoomType))
|
!TeachingVenueNatureRules.SupportsExperiment(classroom.TeachingVenueNature))
|
||||||
return ValidationProblem(
|
return ValidationProblem(
|
||||||
$"实验课必须安排在实验室、实训室或机房;“{classroom.Name}”的场地类型为“{classroom.RoomType}”。");
|
$"实验课必须安排在具有实验教学性质的场地;“{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("所选教室不在该课程指定的校区。");
|
||||||
@@ -596,6 +596,11 @@ public sealed class SchedulesController(
|
|||||||
if (allowedClassroomIds.Count > 0 &&
|
if (allowedClassroomIds.Count > 0 &&
|
||||||
!allowedClassroomIds.Contains(classroom.Id))
|
!allowedClassroomIds.Contains(classroom.Id))
|
||||||
return ValidationProblem("所选教室不在该课程指定的教室范围内。");
|
return ValidationProblem("所选教室不在该课程指定的教室范围内。");
|
||||||
|
if (request.Kind == ScheduleEntryKind.Experiment &&
|
||||||
|
constraint?.AllowedExperimentVenueNatures is { } allowedNatures &&
|
||||||
|
allowedNatures != 0 &&
|
||||||
|
(classroom.TeachingVenueNature & allowedNatures) == 0)
|
||||||
|
return ValidationProblem("所选场地不在该实验课允许的教学场地性质范围内。");
|
||||||
}
|
}
|
||||||
var studentCount = task.Classes.Sum(x =>
|
var studentCount = task.Classes.Sum(x =>
|
||||||
x.AdministrativeClass!.Students.Count(student =>
|
x.AdministrativeClass!.Students.Count(student =>
|
||||||
@@ -655,11 +660,6 @@ public sealed class SchedulesController(
|
|||||||
.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);
|
|
||||||
|
|
||||||
private async Task<ActionResult> SaveAsync(
|
private async Task<ActionResult> SaveAsync(
|
||||||
Guid id,
|
Guid id,
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ public sealed class ExperimentProject : EntityBase
|
|||||||
{
|
{
|
||||||
public Guid TeachingTaskId { get; set; }
|
public Guid TeachingTaskId { get; set; }
|
||||||
public TeachingTask? TeachingTask { get; set; }
|
public TeachingTask? TeachingTask { get; set; }
|
||||||
|
// 集中安排的项目复用已发布课表中的实验课,不再维护一份重复的场次。
|
||||||
|
public Guid? ScheduleEntryId { get; set; }
|
||||||
|
public ScheduleEntry? ScheduleEntry { get; set; }
|
||||||
public required string Code { get; set; }
|
public required string Code { get; set; }
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
public ExperimentArrangementMode ArrangementMode { get; set; }
|
public ExperimentArrangementMode ArrangementMode { get; set; }
|
||||||
|
|||||||
@@ -46,9 +46,35 @@ public sealed class Classroom : CatalogEntity
|
|||||||
public Building? Building { get; set; }
|
public Building? Building { get; set; }
|
||||||
public int Capacity { get; set; }
|
public int Capacity { get; set; }
|
||||||
public string RoomType { get; set; } = "普通教室";
|
public string RoomType { get; set; } = "普通教室";
|
||||||
|
public TeachingVenueNature TeachingVenueNature { get; set; } =
|
||||||
|
TeachingVenueNature.GeneralClassroom;
|
||||||
public string? Equipment { get; set; }
|
public string? Equipment { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum TeachingVenueNature
|
||||||
|
{
|
||||||
|
GeneralClassroom = 1,
|
||||||
|
Laboratory = 2,
|
||||||
|
TrainingRoom = 4,
|
||||||
|
ComputerLab = 8,
|
||||||
|
LanguageLab = 16,
|
||||||
|
SportsVenue = 32,
|
||||||
|
ArtsVenue = 64
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TeachingVenueNatureRules
|
||||||
|
{
|
||||||
|
public const TeachingVenueNature ExperimentTeaching =
|
||||||
|
TeachingVenueNature.Laboratory |
|
||||||
|
TeachingVenueNature.TrainingRoom |
|
||||||
|
TeachingVenueNature.ComputerLab |
|
||||||
|
TeachingVenueNature.LanguageLab;
|
||||||
|
|
||||||
|
public static bool SupportsExperiment(TeachingVenueNature value) =>
|
||||||
|
(value & ExperimentTeaching) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class AcademicTerm : CatalogEntity
|
public sealed class AcademicTerm : CatalogEntity
|
||||||
{
|
{
|
||||||
public required string AcademicYear { get; set; }
|
public required string AcademicYear { get; set; }
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ public sealed class TeachingTaskScheduleConstraint : EntityBase
|
|||||||
public string? AllowedDayOfWeeks { get; set; }
|
public string? AllowedDayOfWeeks { get; set; }
|
||||||
public int? EarliestPeriod { get; set; }
|
public int? EarliestPeriod { get; set; }
|
||||||
public int? LatestPeriod { get; set; }
|
public int? LatestPeriod { get; set; }
|
||||||
|
public TeachingVenueNature AllowedExperimentVenueNatures { get; set; }
|
||||||
public ICollection<TeachingTaskAllowedClassroom> AllowedClassrooms { get; set; } = [];
|
public ICollection<TeachingTaskAllowedClassroom> AllowedClassrooms { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -619,6 +619,10 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
entity.HasOne(x => x.TeachingTask).WithMany()
|
entity.HasOne(x => x.TeachingTask).WithMany()
|
||||||
.HasForeignKey(x => x.TeachingTaskId)
|
.HasForeignKey(x => x.TeachingTaskId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
entity.HasIndex(x => x.ScheduleEntryId);
|
||||||
|
entity.HasOne(x => x.ScheduleEntry).WithMany()
|
||||||
|
.HasForeignKey(x => x.ScheduleEntryId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Entity<ExperimentSession>(entity =>
|
builder.Entity<ExperimentSession>(entity =>
|
||||||
|
|||||||
+6561
File diff suppressed because it is too large
Load Diff
+50
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class BindCentralizedExperimentProjectsToSchedules : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "ScheduleEntryId",
|
||||||
|
table: "ExperimentProjects",
|
||||||
|
type: "char(36)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ExperimentProjects_ScheduleEntryId",
|
||||||
|
table: "ExperimentProjects",
|
||||||
|
column: "ScheduleEntryId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ExperimentProjects_ScheduleEntries_ScheduleEntryId",
|
||||||
|
table: "ExperimentProjects",
|
||||||
|
column: "ScheduleEntryId",
|
||||||
|
principalTable: "ScheduleEntries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ExperimentProjects_ScheduleEntries_ScheduleEntryId",
|
||||||
|
table: "ExperimentProjects");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_ExperimentProjects_ScheduleEntryId",
|
||||||
|
table: "ExperimentProjects");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ScheduleEntryId",
|
||||||
|
table: "ExperimentProjects");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6564
File diff suppressed because it is too large
Load Diff
+42
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddTeachingVenueNatures : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "TeachingVenueNature",
|
||||||
|
table: "Classrooms",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 1);
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE `Classrooms`
|
||||||
|
SET `TeachingVenueNature` = CASE
|
||||||
|
WHEN `RoomType` LIKE '%机房%' THEN 10
|
||||||
|
WHEN `RoomType` LIKE '%语音%' THEN 18
|
||||||
|
WHEN `RoomType` LIKE '%实训%' THEN 4
|
||||||
|
WHEN `RoomType` LIKE '%实验%' THEN 2
|
||||||
|
WHEN `RoomType` LIKE '%体育%' THEN 32
|
||||||
|
WHEN `RoomType` LIKE '%艺术%' THEN 64
|
||||||
|
ELSE 1
|
||||||
|
END;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "TeachingVenueNature",
|
||||||
|
table: "Classrooms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6567
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddExperimentVenueNatureConstraints : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "AllowedExperimentVenueNatures",
|
||||||
|
table: "TeachingTaskScheduleConstraints",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AllowedExperimentVenueNatures",
|
||||||
|
table: "TeachingTaskScheduleConstraints");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -525,6 +525,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.Property<int>("SortOrder")
|
b.Property<int>("SortOrder")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<int>("TeachingVenueNature")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
b.Property<DateTime>("UpdatedAt")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
@@ -2364,6 +2367,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
.HasMaxLength(1000)
|
.HasMaxLength(1000)
|
||||||
.HasColumnType("varchar(1000)");
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ScheduleEntryId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<DateTime>("StartDate")
|
b.Property<DateTime>("StartDate")
|
||||||
.HasColumnType("date");
|
.HasColumnType("date");
|
||||||
|
|
||||||
@@ -2378,6 +2384,8 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ScheduleEntryId");
|
||||||
|
|
||||||
b.HasIndex("TeachingTaskId", "Code")
|
b.HasIndex("TeachingTaskId", "Code")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
@@ -4240,6 +4248,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
.HasMaxLength(20)
|
.HasMaxLength(20)
|
||||||
.HasColumnType("varchar(20)");
|
.HasColumnType("varchar(20)");
|
||||||
|
|
||||||
|
b.Property<int>("AllowedExperimentVenueNatures")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
@@ -5649,12 +5660,19 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
|
|
||||||
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b =>
|
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("Jiaowu.Api.Domain.Academic.ScheduleEntry", "ScheduleEntry")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ScheduleEntryId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask")
|
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("TeachingTaskId")
|
.HasForeignKey("TeachingTaskId")
|
||||||
.OnDelete(DeleteBehavior.Restrict)
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("ScheduleEntry");
|
||||||
|
|
||||||
b.Navigation("TeachingTask");
|
b.Navigation("TeachingTask");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -291,15 +291,14 @@ 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 || IsExperimentRoom(room.RoomType)))
|
(kind != ScheduleEntryKind.Experiment ||
|
||||||
|
TeachingVenueNatureRules.SupportsExperiment(room.TeachingVenueNature)) &&
|
||||||
|
(kind != ScheduleEntryKind.Experiment || constraint is null ||
|
||||||
|
constraint.AllowedExperimentVenueNatures == 0 ||
|
||||||
|
(room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsExperimentRoom(string roomType) =>
|
|
||||||
roomType.Contains("实验", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
roomType.Contains("实训", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
roomType.Contains("机房", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
roomType.Contains("语音", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
private static int[] ParseAllowedDays(string? value)
|
private static int[] ParseAllowedDays(string? value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -171,7 +171,8 @@ public sealed class AutomaticScheduleGeneratorTests
|
|||||||
Name = "实验室 101",
|
Name = "实验室 101",
|
||||||
Building = building,
|
Building = building,
|
||||||
Capacity = 40,
|
Capacity = 40,
|
||||||
RoomType = "实验室"
|
RoomType = "实验室",
|
||||||
|
TeachingVenueNature = TeachingVenueNature.Laboratory
|
||||||
};
|
};
|
||||||
var course = new Course
|
var course = new Course
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed class ExperimentsControllerTests
|
|||||||
|
|
||||||
var result = await controller.CreateProjects(
|
var result = await controller.CreateProjects(
|
||||||
fixture.BatchProjectRequest(
|
fixture.BatchProjectRequest(
|
||||||
ExperimentArrangementMode.Centralized),
|
ExperimentArrangementMode.SelfScheduled),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
Assert.IsType<CreatedResult>(result);
|
Assert.IsType<CreatedResult>(result);
|
||||||
@@ -119,7 +119,7 @@ public sealed class ExperimentsControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CentralizedProject_PublishesAndUsesTeachingTaskRoster()
|
public async Task CentralizedProject_PublishesWhenBoundToPublishedExperimentSchedule()
|
||||||
{
|
{
|
||||||
await using var fixture = await ExperimentFixture.CreateAsync();
|
await using var fixture = await ExperimentFixture.CreateAsync();
|
||||||
var controller = fixture.Controller(fixture.ManagerScope);
|
var controller = fixture.Controller(fixture.ManagerScope);
|
||||||
@@ -133,10 +133,7 @@ public sealed class ExperimentsControllerTests
|
|||||||
project.Id,
|
project.Id,
|
||||||
fixture.SessionRequest(1, 2, 10),
|
fixture.SessionRequest(1, 2, 10),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
Assert.IsType<CreatedResult>(sessionResult);
|
Assert.IsType<ConflictObjectResult>(sessionResult);
|
||||||
Assert.Equal(
|
|
||||||
1,
|
|
||||||
(await fixture.Db.ExperimentSessions.SingleAsync()).Capacity);
|
|
||||||
|
|
||||||
var published = await controller.PublishProject(
|
var published = await controller.PublishProject(
|
||||||
project.Id,
|
project.Id,
|
||||||
@@ -146,13 +143,7 @@ public sealed class ExperimentsControllerTests
|
|||||||
ExperimentProjectStatus.Published,
|
ExperimentProjectStatus.Published,
|
||||||
(await fixture.Db.ExperimentProjects.SingleAsync()).Status);
|
(await fixture.Db.ExperimentProjects.SingleAsync()).Status);
|
||||||
|
|
||||||
var session = await fixture.Db.ExperimentSessions.SingleAsync();
|
Assert.Empty(await fixture.Db.ExperimentSessions.ToListAsync());
|
||||||
var participants = await controller.GetParticipants(
|
|
||||||
session.Id,
|
|
||||||
CancellationToken.None);
|
|
||||||
var ok = Assert.IsType<OkObjectResult>(participants);
|
|
||||||
var rows = Assert.IsAssignableFrom<IEnumerable<object>>(ok.Value);
|
|
||||||
Assert.Single(rows);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -333,11 +324,10 @@ public sealed class ExperimentsControllerTests
|
|||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(timetable);
|
Assert.NotNull(timetable);
|
||||||
Assert.Equal(2, timetable.ExperimentEntries.Count);
|
Assert.Single(timetable.ExperimentEntries);
|
||||||
Assert.Contains(
|
Assert.Equal(
|
||||||
timetable.ExperimentEntries,
|
ExperimentArrangementMode.SelfScheduled,
|
||||||
x => x.ExperimentArrangementMode ==
|
timetable.ExperimentEntries[0].ExperimentArrangementMode);
|
||||||
ExperimentArrangementMode.Centralized);
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
timetable.ExperimentEntries,
|
timetable.ExperimentEntries,
|
||||||
x => x.Id == selfSession.Id &&
|
x => x.Id == selfSession.Id &&
|
||||||
@@ -380,10 +370,7 @@ public sealed class ExperimentsControllerTests
|
|||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(timetable);
|
Assert.NotNull(timetable);
|
||||||
Assert.Single(timetable.ExperimentEntries);
|
Assert.Empty(timetable.ExperimentEntries);
|
||||||
Assert.Equal(
|
|
||||||
ExperimentArrangementMode.Centralized,
|
|
||||||
timetable.ExperimentEntries[0].ExperimentArrangementMode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ExperimentFixture : IAsyncDisposable
|
private sealed class ExperimentFixture : IAsyncDisposable
|
||||||
@@ -395,6 +382,7 @@ public sealed class ExperimentsControllerTests
|
|||||||
TeachingTask task,
|
TeachingTask task,
|
||||||
Classroom classroom,
|
Classroom classroom,
|
||||||
Classroom secondClassroom,
|
Classroom secondClassroom,
|
||||||
|
ScheduleEntry scheduleEntry,
|
||||||
ICurrentUserDataScope managerScope,
|
ICurrentUserDataScope managerScope,
|
||||||
ICurrentUserDataScope studentScope)
|
ICurrentUserDataScope studentScope)
|
||||||
{
|
{
|
||||||
@@ -404,6 +392,7 @@ public sealed class ExperimentsControllerTests
|
|||||||
Task = task;
|
Task = task;
|
||||||
Classroom = classroom;
|
Classroom = classroom;
|
||||||
SecondClassroom = secondClassroom;
|
SecondClassroom = secondClassroom;
|
||||||
|
ScheduleEntry = scheduleEntry;
|
||||||
ManagerScope = managerScope;
|
ManagerScope = managerScope;
|
||||||
StudentScope = studentScope;
|
StudentScope = studentScope;
|
||||||
}
|
}
|
||||||
@@ -414,6 +403,7 @@ public sealed class ExperimentsControllerTests
|
|||||||
public TeachingTask Task { get; }
|
public TeachingTask Task { get; }
|
||||||
public Classroom Classroom { get; }
|
public Classroom Classroom { get; }
|
||||||
public Classroom SecondClassroom { get; }
|
public Classroom SecondClassroom { get; }
|
||||||
|
public ScheduleEntry ScheduleEntry { get; }
|
||||||
public ICurrentUserDataScope ManagerScope { get; }
|
public ICurrentUserDataScope ManagerScope { get; }
|
||||||
public ICurrentUserDataScope StudentScope { get; }
|
public ICurrentUserDataScope StudentScope { get; }
|
||||||
|
|
||||||
@@ -441,14 +431,16 @@ public sealed class ExperimentsControllerTests
|
|||||||
Code = "LAB101",
|
Code = "LAB101",
|
||||||
Name = "实验室 101",
|
Name = "实验室 101",
|
||||||
BuildingId = building.Id,
|
BuildingId = building.Id,
|
||||||
Capacity = 40
|
Capacity = 40,
|
||||||
|
TeachingVenueNature = TeachingVenueNature.Laboratory
|
||||||
};
|
};
|
||||||
var secondClassroom = new Classroom
|
var secondClassroom = new Classroom
|
||||||
{
|
{
|
||||||
Code = "LAB102",
|
Code = "LAB102",
|
||||||
Name = "实验室 102",
|
Name = "实验室 102",
|
||||||
BuildingId = building.Id,
|
BuildingId = building.Id,
|
||||||
Capacity = 40
|
Capacity = 40,
|
||||||
|
TeachingVenueNature = TeachingVenueNature.Laboratory
|
||||||
};
|
};
|
||||||
var college = new College { Code = "CS", Name = "计算机学院" };
|
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||||
manager.CollegeId = college.Id;
|
manager.CollegeId = college.Id;
|
||||||
@@ -564,6 +556,27 @@ public sealed class ExperimentsControllerTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
var scheduleEntry = new ScheduleEntry
|
||||||
|
{
|
||||||
|
SchedulePlan = new SchedulePlan
|
||||||
|
{
|
||||||
|
AcademicTermId = term.Id,
|
||||||
|
Name = "已发布实验课表",
|
||||||
|
Version = "LAB-1",
|
||||||
|
Status = SchedulePlanStatus.Published
|
||||||
|
},
|
||||||
|
TeachingTaskId = task.Id,
|
||||||
|
Kind = ScheduleEntryKind.Experiment,
|
||||||
|
ClassroomId = classroom.Id,
|
||||||
|
DayOfWeek = 1,
|
||||||
|
StartPeriod = 10,
|
||||||
|
PeriodCount = 2,
|
||||||
|
StartWeek = 1,
|
||||||
|
EndWeek = 8,
|
||||||
|
WeekPattern = WeekPattern.All
|
||||||
|
};
|
||||||
|
db.ScheduleEntries.Add(scheduleEntry);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return new ExperimentFixture(
|
return new ExperimentFixture(
|
||||||
connection,
|
connection,
|
||||||
@@ -572,6 +585,7 @@ public sealed class ExperimentsControllerTests
|
|||||||
task,
|
task,
|
||||||
classroom,
|
classroom,
|
||||||
secondClassroom,
|
secondClassroom,
|
||||||
|
scheduleEntry,
|
||||||
Scope(
|
Scope(
|
||||||
manager,
|
manager,
|
||||||
SystemRoles.CollegeAdmin,
|
SystemRoles.CollegeAdmin,
|
||||||
@@ -600,7 +614,8 @@ public sealed class ExperimentsControllerTests
|
|||||||
"完成规定实验项目。",
|
"完成规定实验项目。",
|
||||||
"携带校园卡。",
|
"携带校园卡。",
|
||||||
Term.StartDate,
|
Term.StartDate,
|
||||||
Term.StartDate.AddDays(14));
|
Term.StartDate.AddDays(14),
|
||||||
|
mode == ExperimentArrangementMode.Centralized ? ScheduleEntry.Id : null);
|
||||||
|
|
||||||
public ExperimentProjectBatchRequest BatchProjectRequest(
|
public ExperimentProjectBatchRequest BatchProjectRequest(
|
||||||
ExperimentArrangementMode mode) =>
|
ExperimentArrangementMode mode) =>
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ public sealed class SchedulesControllerTests
|
|||||||
Name = "实验室 201",
|
Name = "实验室 201",
|
||||||
Building = building,
|
Building = building,
|
||||||
Capacity = 40,
|
Capacity = 40,
|
||||||
RoomType = "实验室"
|
RoomType = "实验室",
|
||||||
|
TeachingVenueNature = TeachingVenueNature.Laboratory
|
||||||
};
|
};
|
||||||
var course = new Course
|
var course = new Course
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+1069
-1123
File diff suppressed because it is too large
Load Diff
Vendored
+2
@@ -1,5 +1,7 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
|
/* oxlint-disable */
|
||||||
|
/* oxfmt-ignore */
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
// Generated by unplugin-auto-import
|
// Generated by unplugin-auto-import
|
||||||
|
|||||||
@@ -90,6 +90,34 @@ const filteredRows = computed(() => {
|
|||||||
.filter(Boolean).some((value) => String(value).toLowerCase().includes(q)),
|
.filter(Boolean).some((value) => String(value).toLowerCase().includes(q)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const pagedRows = computed(() => filteredRows.value.slice(
|
||||||
|
(currentPage.value - 1) * pageSize.value,
|
||||||
|
currentPage.value * pageSize.value,
|
||||||
|
))
|
||||||
|
const venueNatureOptions = [
|
||||||
|
{ value: 1, label: '普通教室' },
|
||||||
|
{ value: 2, label: '实验室' },
|
||||||
|
{ value: 4, label: '实训室' },
|
||||||
|
{ value: 8, label: '计算机机房' },
|
||||||
|
{ value: 16, label: '语音室' },
|
||||||
|
{ value: 32, label: '体育场地' },
|
||||||
|
{ value: 64, label: '艺术场地' },
|
||||||
|
]
|
||||||
|
const venueNatureValue = (value: unknown) => {
|
||||||
|
if (typeof value === 'number') return value
|
||||||
|
if (typeof value !== 'string') return 0
|
||||||
|
const names: Record<string, number> = {
|
||||||
|
GeneralClassroom: 1, Laboratory: 2, TrainingRoom: 4, ComputerLab: 8,
|
||||||
|
LanguageLab: 16, SportsVenue: 32, ArtsVenue: 64,
|
||||||
|
}
|
||||||
|
return value.split(',').reduce((sum, name) => sum | (names[name.trim()] ?? 0), 0)
|
||||||
|
}
|
||||||
|
const venueNatureLabel = (value: unknown) => venueNatureOptions
|
||||||
|
.filter((item) => (venueNatureValue(value) & item.value) !== 0)
|
||||||
|
.map((item) => item.label)
|
||||||
|
.join('、') || '未设置'
|
||||||
|
|
||||||
function resetForm(row?: Row) {
|
function resetForm(row?: Row) {
|
||||||
Object.keys(form).forEach((key) => delete form[key])
|
Object.keys(form).forEach((key) => delete form[key])
|
||||||
@@ -100,12 +128,19 @@ function resetForm(row?: Row) {
|
|||||||
grade: new Date().getFullYear(), counselorUserId: undefined,
|
grade: new Date().getFullYear(), counselorUserId: undefined,
|
||||||
academicYear: `${new Date().getFullYear()}-${new Date().getFullYear() + 1}`,
|
academicYear: `${new Date().getFullYear()}-${new Date().getFullYear() + 1}`,
|
||||||
season: 'Autumn', startDate: '', endDate: '', isCurrent: false,
|
season: 'Autumn', startDate: '', endDate: '', isCurrent: false,
|
||||||
capacity: 60, roomType: '普通教室', equipment: '',
|
capacity: 60, roomType: '普通教室', teachingVenueNatures: [1], equipment: '',
|
||||||
}, row ?? {})
|
}, row ?? {})
|
||||||
|
if (active.value === 'classrooms') {
|
||||||
|
const value = venueNatureValue((row as any)?.teachingVenueNature ?? 1)
|
||||||
|
form.teachingVenueNatures = venueNatureOptions
|
||||||
|
.filter((item) => (value & item.value) !== 0)
|
||||||
|
.map((item) => item.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
currentPage.value = 1
|
||||||
try {
|
try {
|
||||||
rows.value = (await http.get(`/base-data/${active.value}`)).data
|
rows.value = (await http.get(`/base-data/${active.value}`)).data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -192,10 +227,16 @@ async function save() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const payload = active.value === 'classrooms'
|
||||||
|
? {
|
||||||
|
...form,
|
||||||
|
teachingVenueNature: form.teachingVenueNatures.reduce((value: number, item: number) => value | item, 0),
|
||||||
|
}
|
||||||
|
: form
|
||||||
if (editingId.value) {
|
if (editingId.value) {
|
||||||
await http.put(`/base-data/${active.value}/${editingId.value}`, form)
|
await http.put(`/base-data/${active.value}/${editingId.value}`, payload)
|
||||||
} else {
|
} else {
|
||||||
await http.post(`/base-data/${active.value}`, form)
|
await http.post(`/base-data/${active.value}`, payload)
|
||||||
}
|
}
|
||||||
ElMessage.success(editingId.value ? '已保存修改' : `已新增${title.value}`)
|
ElMessage.success(editingId.value ? '已保存修改' : `已新增${title.value}`)
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
@@ -265,6 +306,11 @@ onMounted(async () => {
|
|||||||
await Promise.all([load(), loadReferences()])
|
await Promise.all([load(), loadReferences()])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => keyword.value,
|
||||||
|
() => { currentPage.value = 1 },
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.meta.baseGroup,
|
() => route.meta.baseGroup,
|
||||||
async () => {
|
async () => {
|
||||||
@@ -338,7 +384,7 @@ watch(
|
|||||||
|
|
||||||
<div v-if="active === 'terms'" class="term-mobile-list">
|
<div v-if="active === 'terms'" class="term-mobile-list">
|
||||||
<article
|
<article
|
||||||
v-for="row in filteredRows"
|
v-for="row in pagedRows"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
:class="academicTermRowClass(row)"
|
:class="academicTermRowClass(row)"
|
||||||
>
|
>
|
||||||
@@ -381,7 +427,7 @@ watch(
|
|||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="filteredRows"
|
:data="pagedRows"
|
||||||
:row-class-name="termTableRowClass"
|
:row-class-name="termTableRowClass"
|
||||||
class="data-table"
|
class="data-table"
|
||||||
:class="{ 'term-desktop-table': active === 'terms' }"
|
:class="{ 'term-desktop-table': active === 'terms' }"
|
||||||
@@ -407,6 +453,9 @@ watch(
|
|||||||
<el-table-column v-if="active === 'buildings'" prop="campusName" label="所属校区" min-width="140" />
|
<el-table-column v-if="active === 'buildings'" prop="campusName" label="所属校区" min-width="140" />
|
||||||
<el-table-column v-if="active === 'classrooms'" prop="buildingName" label="教学楼" min-width="130" />
|
<el-table-column v-if="active === 'classrooms'" prop="buildingName" label="教学楼" min-width="130" />
|
||||||
<el-table-column v-if="active === 'classrooms'" prop="capacity" label="容量" width="90" />
|
<el-table-column v-if="active === 'classrooms'" prop="capacity" label="容量" width="90" />
|
||||||
|
<el-table-column v-if="active === 'classrooms'" label="场地性质" min-width="180">
|
||||||
|
<template #default="{ row }">{{ venueNatureLabel(row.teachingVenueNature) }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="90">
|
<el-table-column label="状态" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="table-status" :class="{ off: !row.isEnabled }">{{ row.isEnabled ? '启用' : '停用' }}</span>
|
<span class="table-status" :class="{ off: !row.isEnabled }">{{ row.isEnabled ? '启用' : '停用' }}</span>
|
||||||
@@ -444,6 +493,15 @@ watch(
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<template #empty><el-empty description="暂无数据,点击右上角开始新增" /></template>
|
<template #empty><el-empty description="暂无数据,点击右上角开始新增" /></template>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-if="filteredRows.length > pageSize"
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
class="table-pagination"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
:total="filteredRows.length"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${title}`" width="560px">
|
<el-dialog v-model="dialogVisible" :title="`${editingId ? '编辑' : '新增'}${title}`" width="560px">
|
||||||
@@ -519,6 +577,11 @@ watch(
|
|||||||
<el-form-item label="容量"><el-input-number v-model="form.capacity" :min="1" :max="1000" /></el-form-item>
|
<el-form-item label="容量"><el-input-number v-model="form.capacity" :min="1" :max="1000" /></el-form-item>
|
||||||
<el-form-item label="教室类型"><el-input v-model="form.roomType" /></el-form-item>
|
<el-form-item label="教室类型"><el-input v-model="form.roomType" /></el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
<el-form-item v-if="active === 'classrooms'" label="教学场地性质" required>
|
||||||
|
<el-checkbox-group v-model="form.teachingVenueNatures">
|
||||||
|
<el-checkbox v-for="item in venueNatureOptions" :key="item.value" :value="item.value">{{ item.label }}</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item v-if="active === 'classrooms'" label="设备"><el-input v-model="form.equipment" /></el-form-item>
|
<el-form-item v-if="active === 'classrooms'" label="设备"><el-input v-model="form.equipment" /></el-form-item>
|
||||||
<div class="form-grid compact">
|
<div class="form-grid compact">
|
||||||
<el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" /></el-form-item>
|
<el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" /></el-form-item>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const termId = ref('')
|
|||||||
const projects = ref<any[]>([])
|
const projects = ref<any[]>([])
|
||||||
const options = reactive({
|
const options = reactive({
|
||||||
tasks: [] as any[],
|
tasks: [] as any[],
|
||||||
|
scheduleEntries: [] as any[],
|
||||||
classrooms: [] as any[],
|
classrooms: [] as any[],
|
||||||
periods: [] as any[],
|
periods: [] as any[],
|
||||||
})
|
})
|
||||||
@@ -28,6 +29,7 @@ const projectDialog = ref(false)
|
|||||||
const editingProjectId = ref('')
|
const editingProjectId = ref('')
|
||||||
const projectForm = reactive({
|
const projectForm = reactive({
|
||||||
teachingTaskIds: [] as string[],
|
teachingTaskIds: [] as string[],
|
||||||
|
scheduleEntryId: '',
|
||||||
code: '',
|
code: '',
|
||||||
name: '',
|
name: '',
|
||||||
arrangementMode: 'Centralized',
|
arrangementMode: 'Centralized',
|
||||||
@@ -59,8 +61,12 @@ const participantsLoading = ref(false)
|
|||||||
const selectedTask = computed(() =>
|
const selectedTask = computed(() =>
|
||||||
options.tasks.find((task) => task.id === projectForm.teachingTaskIds[0]),
|
options.tasks.find((task) => task.id === projectForm.teachingTaskIds[0]),
|
||||||
)
|
)
|
||||||
|
const selectedScheduleEntry = computed(() =>
|
||||||
|
options.scheduleEntries.find((entry) => entry.id === projectForm.scheduleEntryId),
|
||||||
|
)
|
||||||
const batchProjectOptions = computed(() =>
|
const batchProjectOptions = computed(() =>
|
||||||
projects.value.filter((project) => project.status !== 'Closed'),
|
projects.value.filter((project) =>
|
||||||
|
project.status !== 'Closed' && project.arrangementMode === 'SelfScheduled'),
|
||||||
)
|
)
|
||||||
const activePeriods = computed(() =>
|
const activePeriods = computed(() =>
|
||||||
options.periods.filter((period) =>
|
options.periods.filter((period) =>
|
||||||
@@ -90,6 +96,7 @@ function resetProjectForm() {
|
|||||||
editingProjectId.value = ''
|
editingProjectId.value = ''
|
||||||
Object.assign(projectForm, {
|
Object.assign(projectForm, {
|
||||||
teachingTaskIds: [],
|
teachingTaskIds: [],
|
||||||
|
scheduleEntryId: '',
|
||||||
code: '',
|
code: '',
|
||||||
name: '',
|
name: '',
|
||||||
arrangementMode: 'Centralized',
|
arrangementMode: 'Centralized',
|
||||||
@@ -105,6 +112,13 @@ function onTaskChange() {
|
|||||||
projectForm.dates = [task.termStartDate, task.termEndDate]
|
projectForm.dates = [task.termStartDate, task.termEndDate]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onScheduleEntryChange() {
|
||||||
|
const entry = selectedScheduleEntry.value
|
||||||
|
if (!entry) return
|
||||||
|
projectForm.teachingTaskIds = [entry.teachingTaskId]
|
||||||
|
onTaskChange()
|
||||||
|
}
|
||||||
|
|
||||||
function openCreateProject() {
|
function openCreateProject() {
|
||||||
resetProjectForm()
|
resetProjectForm()
|
||||||
projectDialog.value = true
|
projectDialog.value = true
|
||||||
@@ -114,6 +128,7 @@ function openEditProject(project: any) {
|
|||||||
editingProjectId.value = project.id
|
editingProjectId.value = project.id
|
||||||
Object.assign(projectForm, {
|
Object.assign(projectForm, {
|
||||||
teachingTaskIds: [project.teachingTaskId],
|
teachingTaskIds: [project.teachingTaskId],
|
||||||
|
scheduleEntryId: project.scheduleEntryId ?? '',
|
||||||
code: project.code,
|
code: project.code,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
arrangementMode: project.arrangementMode,
|
arrangementMode: project.arrangementMode,
|
||||||
@@ -125,13 +140,18 @@ function openEditProject(project: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveProject() {
|
async function saveProject() {
|
||||||
if (!projectForm.teachingTaskIds.length || !projectForm.code.trim()
|
if (!projectForm.teachingTaskIds.length ||
|
||||||
|
(projectForm.arrangementMode === 'Centralized' && !projectForm.scheduleEntryId) ||
|
||||||
|
!projectForm.code.trim()
|
||||||
|| !projectForm.name.trim() || projectForm.dates.length !== 2) {
|
|| !projectForm.name.trim() || projectForm.dates.length !== 2) {
|
||||||
ElMessage.warning('请填写教学任务、项目编码、名称和开放日期')
|
ElMessage.warning(projectForm.arrangementMode === 'Centralized'
|
||||||
|
? '请选择课表实验课,并填写项目编码、名称和开放日期'
|
||||||
|
: '请填写教学任务、项目编码、名称和开放日期')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
teachingTaskId: projectForm.teachingTaskIds[0],
|
teachingTaskId: projectForm.teachingTaskIds[0],
|
||||||
|
scheduleEntryId: projectForm.scheduleEntryId || null,
|
||||||
code: projectForm.code,
|
code: projectForm.code,
|
||||||
name: projectForm.name,
|
name: projectForm.name,
|
||||||
arrangementMode: projectForm.arrangementMode,
|
arrangementMode: projectForm.arrangementMode,
|
||||||
@@ -144,6 +164,9 @@ async function saveProject() {
|
|||||||
if (editingProjectId.value) {
|
if (editingProjectId.value) {
|
||||||
await http.put(`/experiments/${editingProjectId.value}`, payload)
|
await http.put(`/experiments/${editingProjectId.value}`, payload)
|
||||||
ElMessage.success('实验项目已更新')
|
ElMessage.success('实验项目已更新')
|
||||||
|
} else if (projectForm.arrangementMode === 'Centralized') {
|
||||||
|
await http.post('/experiments', payload)
|
||||||
|
ElMessage.success('实验项目已绑定课表实验课')
|
||||||
} else {
|
} else {
|
||||||
await http.post('/experiments/batch', {
|
await http.post('/experiments/batch', {
|
||||||
...payload,
|
...payload,
|
||||||
@@ -404,6 +427,7 @@ async function loadOptions() {
|
|||||||
params: { academicTermId: termId.value || undefined },
|
params: { academicTermId: termId.value || undefined },
|
||||||
})
|
})
|
||||||
options.tasks = data.tasks
|
options.tasks = data.tasks
|
||||||
|
options.scheduleEntries = data.scheduleEntries
|
||||||
options.classrooms = data.classrooms
|
options.classrooms = data.classrooms
|
||||||
options.periods = data.periods
|
options.periods = data.periods
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -462,11 +486,11 @@ onMounted(async () => {
|
|||||||
<span class="section-kicker">LABORATORY OPERATIONS</span>
|
<span class="section-kicker">LABORATORY OPERATIONS</span>
|
||||||
<h2>{{ isStudent ? '我的实验' : '实验管理' }}</h2>
|
<h2>{{ isStudent ? '我的实验' : '实验管理' }}</h2>
|
||||||
<p v-if="isStudent">查看统一安排的实验课次,或为规定实验项目选择适合自己的开放时段。</p>
|
<p v-if="isStudent">查看统一安排的实验课次,或为规定实验项目选择适合自己的开放时段。</p>
|
||||||
<p v-else>把实验项目分成两条运行轨道:集中排入固定课次,或开放场次供学生自主预约。</p>
|
<p v-else>集中实验直接绑定已发布课表;仅自主预约实验需要在此开放场次。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="intro-actions">
|
<div class="intro-actions">
|
||||||
<el-button v-if="!isStudent" :icon="Calendar" @click="openBatchSession">
|
<el-button v-if="!isStudent" :icon="Calendar" @click="openBatchSession">
|
||||||
批量排课
|
批量开放场次
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="!isStudent" type="primary" :icon="Plus" @click="openCreateProject">
|
<el-button v-if="!isStudent" type="primary" :icon="Plus" @click="openCreateProject">
|
||||||
批量设置实验任务
|
批量设置实验任务
|
||||||
@@ -563,7 +587,7 @@ onMounted(async () => {
|
|||||||
<b>{{ modeMeta[project.arrangementMode].note }}</b>
|
<b>{{ modeMeta[project.arrangementMode].note }}</b>
|
||||||
</div>
|
</div>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="!isStudent && project.status !== 'Closed'"
|
v-if="!isStudent && project.status !== 'Closed' && project.arrangementMode === 'SelfScheduled'"
|
||||||
size="small"
|
size="small"
|
||||||
:icon="Calendar"
|
:icon="Calendar"
|
||||||
@click="openSession(project)"
|
@click="openSession(project)"
|
||||||
@@ -572,7 +596,17 @@ onMounted(async () => {
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="project.sessions.length" class="session-grid">
|
<div v-if="project.arrangementMode === 'Centralized' && project.scheduleEntry" class="session-grid">
|
||||||
|
<article class="session-ticket">
|
||||||
|
<div class="ticket-date"><strong>课表</strong><span>固定</span></div>
|
||||||
|
<div class="ticket-body">
|
||||||
|
<b>星期 {{ project.scheduleEntry.dayOfWeek }} · 第 {{ project.scheduleEntry.startPeriod }}–{{ project.scheduleEntry.startPeriod + project.scheduleEntry.periodCount - 1 }} 节</b>
|
||||||
|
<span>第 {{ project.scheduleEntry.startWeek }}–{{ project.scheduleEntry.endWeek }} 周 · {{ project.scheduleEntry.campusName }} · {{ project.scheduleEntry.buildingName }} {{ project.scheduleEntry.classroomName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ticket-action"><el-tag type="primary" size="small" effect="plain">课表已安排</el-tag></div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="project.sessions.length" class="session-grid">
|
||||||
<article
|
<article
|
||||||
v-for="session in project.sessions"
|
v-for="session in project.sessions"
|
||||||
:key="session.id"
|
:key="session.id"
|
||||||
@@ -653,7 +687,7 @@ onMounted(async () => {
|
|||||||
<el-empty
|
<el-empty
|
||||||
v-else
|
v-else
|
||||||
:image-size="58"
|
:image-size="58"
|
||||||
:description="project.status === 'Draft' ? '尚未安排场次,安排后才能发布' : '暂无有效实验场次'"
|
:description="project.arrangementMode === 'Centralized' ? '尚未绑定已发布课表中的实验课' : (project.status === 'Draft' ? '尚未开放场次,安排后才能发布' : '暂无有效实验场次')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -664,7 +698,9 @@ onMounted(async () => {
|
|||||||
<el-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!project.sessions.some((item: any) => item.status !== 'Cancelled')"
|
:disabled="project.arrangementMode === 'Centralized'
|
||||||
|
? !project.scheduleEntry
|
||||||
|
: !project.sessions.some((item: any) => item.status !== 'Cancelled')"
|
||||||
@click="publishProject(project)"
|
@click="publishProject(project)"
|
||||||
>
|
>
|
||||||
发布给学生
|
发布给学生
|
||||||
@@ -720,11 +756,22 @@ onMounted(async () => {
|
|||||||
<el-form label-position="top" class="experiment-form">
|
<el-form label-position="top" class="experiment-form">
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<header><span>PROJECT</span><b>规定实验项目</b></header>
|
<header><span>PROJECT</span><b>规定实验项目</b></header>
|
||||||
<el-form-item :label="editingProjectId ? '所属教学任务' : '适用教学任务(可多选)'" required>
|
<el-form-item v-if="projectForm.arrangementMode === 'Centralized'" label="已发布课表中的实验课" required>
|
||||||
|
<el-select v-model="projectForm.scheduleEntryId" filterable :disabled="!!editingProjectId" placeholder="选择已安排实验室的实验课" @change="onScheduleEntryChange">
|
||||||
|
<el-option
|
||||||
|
v-for="entry in options.scheduleEntries"
|
||||||
|
:key="entry.id"
|
||||||
|
:label="`${entry.courseCode} · ${entry.courseName} · ${entry.taskNumber} · 星期 ${entry.dayOfWeek} 第 ${entry.startPeriod}–${entry.startPeriod + entry.periodCount - 1} 节 · ${entry.campusName} ${entry.buildingName} ${entry.classroomName}`"
|
||||||
|
:value="entry.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<small class="form-help">集中实验复用课表的时间和实验室,不会再生成独立实验场次。</small>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-else :label="editingProjectId ? '所属教学任务' : '适用教学任务(可多选)'" required>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="projectForm.teachingTaskIds"
|
v-model="projectForm.teachingTaskIds"
|
||||||
filterable
|
filterable
|
||||||
multiple
|
:multiple="!editingProjectId"
|
||||||
collapse-tags
|
collapse-tags
|
||||||
collapse-tags-tooltip
|
collapse-tags-tooltip
|
||||||
:disabled="!!editingProjectId"
|
:disabled="!!editingProjectId"
|
||||||
@@ -757,9 +804,9 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<header><span>ROUTE</span><b>选择运行轨道</b></header>
|
<header><span>ROUTE</span><b>选择运行轨道</b></header>
|
||||||
<el-radio-group v-model="projectForm.arrangementMode" class="mode-choice">
|
<el-radio-group v-model="projectForm.arrangementMode" class="mode-choice" :disabled="!!editingProjectId">
|
||||||
<el-radio-button value="Centralized">
|
<el-radio-button value="Centralized">
|
||||||
<b>集中安排</b><small>统一时间,像上课一样到场</small>
|
<b>集中安排</b><small>复用已发布课表的实验课</small>
|
||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
<el-radio-button value="SelfScheduled">
|
<el-radio-button value="SelfScheduled">
|
||||||
<b>自行安排</b><small>开放多个场次,学生自主预约</small>
|
<b>自行安排</b><small>开放多个场次,学生自主预约</small>
|
||||||
@@ -858,7 +905,7 @@ onMounted(async () => {
|
|||||||
<el-form-item label="实验室" required>
|
<el-form-item label="实验室" required>
|
||||||
<el-select v-model="sessionForm.classroomId" filterable placeholder="选择实验室或教学场所">
|
<el-select v-model="sessionForm.classroomId" filterable placeholder="选择实验室或教学场所">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="room in options.classrooms"
|
v-for="room in options.classrooms.filter((item: any) => (Number(item.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0)"
|
||||||
:key="room.id"
|
:key="room.id"
|
||||||
:label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
|
:label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
|
||||||
:value="room.id"
|
:value="room.id"
|
||||||
@@ -940,7 +987,7 @@ onMounted(async () => {
|
|||||||
<el-input-number v-model="row.periodCount" :min="1" :max="12" controls-position="right" />
|
<el-input-number v-model="row.periodCount" :min="1" :max="12" controls-position="right" />
|
||||||
<el-select v-model="row.classroomId" filterable placeholder="实验室">
|
<el-select v-model="row.classroomId" filterable placeholder="实验室">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="room in options.classrooms"
|
v-for="room in options.classrooms.filter((item: any) => (Number(item.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0)"
|
||||||
:key="room.id"
|
:key="room.id"
|
||||||
:label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
|
:label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
|
||||||
:value="room.id"
|
:value="room.id"
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ const weekdays = [
|
|||||||
{ value: 6, label: '星期六' },
|
{ value: 6, label: '星期六' },
|
||||||
{ value: 7, label: '星期日' },
|
{ value: 7, label: '星期日' },
|
||||||
]
|
]
|
||||||
|
const experimentVenueNatures = [
|
||||||
|
{ value: 2, label: '实验室' }, { value: 4, label: '实训室' },
|
||||||
|
{ value: 8, label: '计算机机房' }, { value: 16, label: '语音室' },
|
||||||
|
]
|
||||||
const periods = computed(() => {
|
const periods = computed(() => {
|
||||||
const configured = timeSlots.value
|
const configured = timeSlots.value
|
||||||
.filter((item) => item.isEnabled)
|
.filter((item) => item.isEnabled)
|
||||||
@@ -126,7 +130,7 @@ const selectedTaskConstraint = computed(() =>
|
|||||||
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
|
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
|
||||||
)
|
)
|
||||||
const isExperimentRoom = (room: any) =>
|
const isExperimentRoom = (room: any) =>
|
||||||
['实验', '实训', '机房', '语音'].some((keyword) => room.roomType?.includes(keyword))
|
(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
|
||||||
@@ -294,6 +298,9 @@ function openConstraint(item: any) {
|
|||||||
requiredCampusId: item.requiredCampusId,
|
requiredCampusId: item.requiredCampusId,
|
||||||
requiredBuildingId: item.requiredBuildingId,
|
requiredBuildingId: item.requiredBuildingId,
|
||||||
allowedClassroomIds: [...item.allowedClassroomIds],
|
allowedClassroomIds: [...item.allowedClassroomIds],
|
||||||
|
allowedExperimentVenueNatures: experimentVenueNatures
|
||||||
|
.filter((nature) => (Number(item.allowedExperimentVenueNatures) & nature.value) !== 0)
|
||||||
|
.map((nature) => nature.value),
|
||||||
allowedDayOfWeeks: item.allowedDayOfWeeks.length
|
allowedDayOfWeeks: item.allowedDayOfWeeks.length
|
||||||
? [...item.allowedDayOfWeeks]
|
? [...item.allowedDayOfWeeks]
|
||||||
: [1, 2, 3, 4, 5],
|
: [1, 2, 3, 4, 5],
|
||||||
@@ -311,6 +318,8 @@ 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 ?? [],
|
||||||
|
allowedExperimentVenueNatures: (constraintForm.allowedExperimentVenueNatures ?? [])
|
||||||
|
.reduce((value: number, nature: number) => value | nature, 0),
|
||||||
allowedDayOfWeeks: constraintForm.allowedDayOfWeeks ?? [],
|
allowedDayOfWeeks: constraintForm.allowedDayOfWeeks ?? [],
|
||||||
earliestPeriod: constraintForm.earliestPeriod || null,
|
earliestPeriod: constraintForm.earliestPeriod || null,
|
||||||
latestPeriod: constraintForm.latestPeriod || null,
|
latestPeriod: constraintForm.latestPeriod || null,
|
||||||
@@ -1208,7 +1217,7 @@ onBeforeUnmount(() => {
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
<el-form-item label="指定可用教室">
|
<el-form-item label="指定可用教室">
|
||||||
<el-select v-model="constraintForm.allowedClassroomIds" multiple filterable collapse-tags>
|
<el-select v-model="constraintForm.allowedClassroomIds" multiple filterable collapse-tags>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in filteredClassrooms"
|
v-for="item in filteredClassrooms"
|
||||||
@@ -1217,7 +1226,13 @@ onBeforeUnmount(() => {
|
|||||||
:value="item.id"
|
:value="item.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="实验课允许的场地性质">
|
||||||
|
<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-group>
|
||||||
|
<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">
|
||||||
|
|||||||
Reference in New Issue
Block a user