已改成“普通排课与实验学时分离”:
普通课表学时 = 总学时 − 实践学时。 自动排课、手工排课、课表发布都会阻止把实践学时重复排入。 全部为实践学时的课程应设为“非排时课程”,再由实验管理安排。 教学任务和排课页面会显示“常规 / 实践学时”。 已有教学任务即使仍保存旧周学时,排课时也会按拆分后的课程学时计算。 无需数据库迁移。
This commit is contained in:
@@ -145,6 +145,8 @@ public sealed class CoursesController(
|
|||||||
CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
|
CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
|
||||||
x.Credits,
|
x.Credits,
|
||||||
x.TotalHours,
|
x.TotalHours,
|
||||||
|
x.LectureHours,
|
||||||
|
x.PracticeHours,
|
||||||
x.Nature,
|
x.Nature,
|
||||||
x.AssessmentMethod
|
x.AssessmentMethod
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Jiaowu.Api.Domain.Academic;
|
|||||||
using Jiaowu.Api.Domain.Identity;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
using Jiaowu.Api.Infrastructure.Caching;
|
using Jiaowu.Api.Infrastructure.Caching;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Jiaowu.Api.Infrastructure.Teaching;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -96,6 +97,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
x.StartWeek,
|
x.StartWeek,
|
||||||
x.EndWeek,
|
x.EndWeek,
|
||||||
x.WeeklyHours,
|
x.WeeklyHours,
|
||||||
|
CourseTotalHours = x.Course.TotalHours,
|
||||||
|
CoursePracticeHours = x.Course.PracticeHours,
|
||||||
x.SchedulingMode
|
x.SchedulingMode
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
@@ -107,6 +110,15 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
return Ok(tasks.Select(task =>
|
return Ok(tasks.Select(task =>
|
||||||
{
|
{
|
||||||
constraints.TryGetValue(task.Id, out var constraint);
|
constraints.TryGetValue(task.Id, out var constraint);
|
||||||
|
var weeklyHours = task.WeeklyHours;
|
||||||
|
if (task.SchedulingMode == TeachingTaskSchedulingMode.Standard &&
|
||||||
|
TeachingTaskHours.TryResolveRegularWeeklyHours(
|
||||||
|
task.CourseTotalHours,
|
||||||
|
task.CoursePracticeHours,
|
||||||
|
task.StartWeek,
|
||||||
|
task.EndWeek,
|
||||||
|
out var regularWeeklyHours))
|
||||||
|
weeklyHours = regularWeeklyHours;
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
task.Id,
|
task.Id,
|
||||||
@@ -120,7 +132,9 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
task.Capacity,
|
task.Capacity,
|
||||||
task.StartWeek,
|
task.StartWeek,
|
||||||
task.EndWeek,
|
task.EndWeek,
|
||||||
task.WeeklyHours,
|
WeeklyHours = weeklyHours,
|
||||||
|
task.CourseTotalHours,
|
||||||
|
task.CoursePracticeHours,
|
||||||
task.SchedulingMode,
|
task.SchedulingMode,
|
||||||
HasCustomConstraint = constraint is not null,
|
HasCustomConstraint = constraint is not null,
|
||||||
RequiresClassroom = task.SchedulingMode == TeachingTaskSchedulingMode.Flexible
|
RequiresClassroom = task.SchedulingMode == TeachingTaskSchedulingMode.Flexible
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Jiaowu.Api.Domain.Identity;
|
|||||||
using Jiaowu.Api.Domain.System;
|
using Jiaowu.Api.Domain.System;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
using Jiaowu.Api.Infrastructure.Scheduling;
|
using Jiaowu.Api.Infrastructure.Scheduling;
|
||||||
|
using Jiaowu.Api.Infrastructure.Teaching;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -497,6 +498,7 @@ public sealed class SchedulesController(
|
|||||||
.Include(x => x.Classes)
|
.Include(x => x.Classes)
|
||||||
.ThenInclude(x => x.AdministrativeClass)
|
.ThenInclude(x => x.AdministrativeClass)
|
||||||
.ThenInclude(x => x!.Students)
|
.ThenInclude(x => x!.Students)
|
||||||
|
.Include(x => x.Course)
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.TeachingTaskId, cancellationToken);
|
.FirstOrDefaultAsync(x => x.Id == request.TeachingTaskId, cancellationToken);
|
||||||
if (task is null ||
|
if (task is null ||
|
||||||
task.Status != TeachingTaskStatus.Published ||
|
task.Status != TeachingTaskStatus.Published ||
|
||||||
@@ -506,6 +508,27 @@ public sealed class SchedulesController(
|
|||||||
return ValidationProblem("非排时课程不进入正常课表,无需设置星期、节次或教室。");
|
return ValidationProblem("非排时课程不进入正常课表,无需设置星期、节次或教室。");
|
||||||
if (request.StartWeek < task.StartWeek || request.EndWeek > task.EndWeek)
|
if (request.StartWeek < task.StartWeek || request.EndWeek > task.EndWeek)
|
||||||
return ValidationProblem("排课周次必须位于教学任务的授课周次内。");
|
return ValidationProblem("排课周次必须位于教学任务的授课周次内。");
|
||||||
|
if (!TeachingTaskHours.TryResolveRegularWeeklyHours(
|
||||||
|
task.Course!,
|
||||||
|
task.StartWeek,
|
||||||
|
task.EndWeek,
|
||||||
|
out var requiredWeeklyHours))
|
||||||
|
return ValidationProblem(
|
||||||
|
"该课程的普通排课学时不能按授课周次整除,请先调整教学任务周次。");
|
||||||
|
if (requiredWeeklyHours == 0)
|
||||||
|
return ValidationProblem(
|
||||||
|
"该课程全部为实践学时,无需进入普通课表,请在实验管理中安排。");
|
||||||
|
var existingHours = await db.ScheduleEntries.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.SchedulePlanId == plan.Id &&
|
||||||
|
x.TeachingTaskId == request.TeachingTaskId &&
|
||||||
|
x.Id != entryId)
|
||||||
|
.SumAsync(x => x.PeriodCount, cancellationToken);
|
||||||
|
if (existingHours + request.PeriodCount > requiredWeeklyHours)
|
||||||
|
return ValidationProblem(
|
||||||
|
$"该教学任务普通课表每周只需 {requiredWeeklyHours} 学时;" +
|
||||||
|
$"当前操作后将达到 {existingHours + request.PeriodCount} 学时," +
|
||||||
|
"实践学时请在实验管理中安排。");
|
||||||
|
|
||||||
var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking()
|
||||||
.Include(x => x.AllowedClassrooms)
|
.Include(x => x.AllowedClassrooms)
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ public sealed class TeachingTasksController(
|
|||||||
x.WeeklyHours,
|
x.WeeklyHours,
|
||||||
x.SchedulingMode,
|
x.SchedulingMode,
|
||||||
CourseTotalHours = x.Course.TotalHours,
|
CourseTotalHours = x.Course.TotalHours,
|
||||||
|
CoursePracticeHours = x.Course.PracticeHours,
|
||||||
|
CourseRegularScheduleHours =
|
||||||
|
x.Course.TotalHours - x.Course.PracticeHours,
|
||||||
x.GenerationBatchCode,
|
x.GenerationBatchCode,
|
||||||
x.Status,
|
x.Status,
|
||||||
TeacherNames = x.Teachers
|
TeacherNames = x.Teachers
|
||||||
@@ -127,6 +130,10 @@ public sealed class TeachingTasksController(
|
|||||||
x.EndWeek,
|
x.EndWeek,
|
||||||
x.WeeklyHours,
|
x.WeeklyHours,
|
||||||
x.SchedulingMode,
|
x.SchedulingMode,
|
||||||
|
CourseTotalHours = x.Course!.TotalHours,
|
||||||
|
CoursePracticeHours = x.Course.PracticeHours,
|
||||||
|
CourseRegularScheduleHours =
|
||||||
|
x.Course.TotalHours - x.Course.PracticeHours,
|
||||||
TeacherNames = x.Teachers
|
TeacherNames = x.Teachers
|
||||||
.OrderByDescending(item => item.IsPrimary)
|
.OrderByDescending(item => item.IsPrimary)
|
||||||
.Select(item => item.Teacher!.Name),
|
.Select(item => item.Teacher!.Name),
|
||||||
@@ -163,6 +170,9 @@ public sealed class TeachingTasksController(
|
|||||||
x.WeeklyHours,
|
x.WeeklyHours,
|
||||||
x.SchedulingMode,
|
x.SchedulingMode,
|
||||||
CourseTotalHours = x.Course.TotalHours,
|
CourseTotalHours = x.Course.TotalHours,
|
||||||
|
CoursePracticeHours = x.Course.PracticeHours,
|
||||||
|
CourseRegularScheduleHours =
|
||||||
|
x.Course.TotalHours - x.Course.PracticeHours,
|
||||||
x.GenerationBatchCode,
|
x.GenerationBatchCode,
|
||||||
x.Status,
|
x.Status,
|
||||||
x.Notes,
|
x.Notes,
|
||||||
@@ -423,7 +433,8 @@ public sealed class TeachingTasksController(
|
|||||||
course,
|
course,
|
||||||
request.StartWeek,
|
request.StartWeek,
|
||||||
request.EndWeek,
|
request.EndWeek,
|
||||||
request.WeeklyHours);
|
request.WeeklyHours,
|
||||||
|
TeachingTaskSchedulingMode.Standard);
|
||||||
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
|
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
|
||||||
if (course.Nature is not (CourseNature.GeneralRequired or CourseNature.GeneralElective))
|
if (course.Nature is not (CourseNature.GeneralRequired or CourseNature.GeneralElective))
|
||||||
return ValidationProblem("批量合班生成仅用于公共必修课或公共选修课。");
|
return ValidationProblem("批量合班生成仅用于公共必修课或公共选修课。");
|
||||||
@@ -598,7 +609,8 @@ public sealed class TeachingTasksController(
|
|||||||
course,
|
course,
|
||||||
request.StartWeek,
|
request.StartWeek,
|
||||||
request.EndWeek,
|
request.EndWeek,
|
||||||
request.WeeklyHours);
|
request.WeeklyHours,
|
||||||
|
request.SchedulingMode);
|
||||||
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
|
if (hoursProblem is not null) return ValidationProblem(hoursProblem);
|
||||||
var collegeId = ScopedCollegeId();
|
var collegeId = ScopedCollegeId();
|
||||||
if (!await db.AcademicTerms.AnyAsync(
|
if (!await db.AcademicTerms.AnyAsync(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Jiaowu.Api.Infrastructure.Teaching;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Jiaowu.Api.Infrastructure.Scheduling;
|
namespace Jiaowu.Api.Infrastructure.Scheduling;
|
||||||
@@ -34,6 +35,7 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
|||||||
.Include(x => x.Classes)
|
.Include(x => x.Classes)
|
||||||
.ThenInclude(x => x.AdministrativeClass)
|
.ThenInclude(x => x.AdministrativeClass)
|
||||||
.ThenInclude(x => x!.Students)
|
.ThenInclude(x => x!.Students)
|
||||||
|
.Include(x => x.Course)
|
||||||
.OrderByDescending(x => x.Classes.Count + x.Teachers.Count)
|
.OrderByDescending(x => x.Classes.Count + x.Teachers.Count)
|
||||||
.ThenByDescending(x => x.Capacity)
|
.ThenByDescending(x => x.Capacity)
|
||||||
.ThenBy(x => x.TaskNumber)
|
.ThenBy(x => x.TaskNumber)
|
||||||
@@ -72,10 +74,43 @@ public sealed class AutomaticScheduleGenerator(AppDbContext db)
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
constraints.TryGetValue(task.Id, out var constraint);
|
constraints.TryGetValue(task.Id, out var constraint);
|
||||||
|
if (!TeachingTaskHours.TryResolveRegularWeeklyHours(
|
||||||
|
task.Course!,
|
||||||
|
task.StartWeek,
|
||||||
|
task.EndWeek,
|
||||||
|
out var requiredWeeklyHours))
|
||||||
|
{
|
||||||
|
messages.Add(
|
||||||
|
$"{task.TaskNumber} · {task.Name} 的普通排课学时不能按授课周次整除,请调整教学任务周次。");
|
||||||
|
processedTasks++;
|
||||||
|
if (reportProgress is not null)
|
||||||
|
{
|
||||||
|
await reportProgress(
|
||||||
|
new(tasks.Count, processedTasks, created, completedTasks),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var scheduledHours = entries
|
var scheduledHours = entries
|
||||||
.Where(x => x.TeachingTaskId == task.Id)
|
.Where(x => x.TeachingTaskId == task.Id)
|
||||||
.Sum(x => x.PeriodCount);
|
.Sum(x => x.PeriodCount);
|
||||||
var remainingHours = Math.Max(0, task.WeeklyHours - scheduledHours);
|
if (scheduledHours > requiredWeeklyHours)
|
||||||
|
{
|
||||||
|
messages.Add(
|
||||||
|
$"{task.TaskNumber} · {task.Name} 已安排每周 {scheduledHours} 学时," +
|
||||||
|
$"普通课表只需 {requiredWeeklyHours} 学时;请删除已包含的实践学时。");
|
||||||
|
processedTasks++;
|
||||||
|
if (reportProgress is not null)
|
||||||
|
{
|
||||||
|
await reportProgress(
|
||||||
|
new(tasks.Count, processedTasks, created, completedTasks),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingHours = requiredWeeklyHours - scheduledHours;
|
||||||
if (remainingHours == 0)
|
if (remainingHours == 0)
|
||||||
{
|
{
|
||||||
completedTasks++;
|
completedTasks++;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Infrastructure.Caching;
|
using Jiaowu.Api.Infrastructure.Caching;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Jiaowu.Api.Infrastructure.Teaching;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Jiaowu.Api.Infrastructure.Scheduling;
|
namespace Jiaowu.Api.Infrastructure.Scheduling;
|
||||||
@@ -179,19 +180,68 @@ public sealed class SchedulePlanPublisher(AppDbContext db)
|
|||||||
x.AcademicTermId == plan.AcademicTermId &&
|
x.AcademicTermId == plan.AcademicTermId &&
|
||||||
x.Status == TeachingTaskStatus.Published &&
|
x.Status == TeachingTaskStatus.Published &&
|
||||||
x.SchedulingMode == TeachingTaskSchedulingMode.Standard)
|
x.SchedulingMode == TeachingTaskSchedulingMode.Standard)
|
||||||
.Select(x => new { x.Id, x.TaskNumber, x.Name, x.WeeklyHours })
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.TaskNumber,
|
||||||
|
x.Name,
|
||||||
|
x.StartWeek,
|
||||||
|
x.EndWeek,
|
||||||
|
CourseTotalHours = x.Course!.TotalHours,
|
||||||
|
CoursePracticeHours = x.Course.PracticeHours
|
||||||
|
})
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
var invalidHours = requiredTasks.FirstOrDefault(task =>
|
||||||
|
!TeachingTaskHours.TryResolveRegularWeeklyHours(
|
||||||
|
task.CourseTotalHours,
|
||||||
|
task.CoursePracticeHours,
|
||||||
|
task.StartWeek,
|
||||||
|
task.EndWeek,
|
||||||
|
out _));
|
||||||
|
if (invalidHours is not null)
|
||||||
|
{
|
||||||
|
throw new SchedulePublishValidationException(
|
||||||
|
$"{invalidHours.TaskNumber} · {invalidHours.Name} 的普通排课学时" +
|
||||||
|
"不能按授课周次整除,请先调整教学任务周次。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiredWeeklyHours = requiredTasks
|
||||||
|
.Select(task =>
|
||||||
|
{
|
||||||
|
TeachingTaskHours.TryResolveRegularWeeklyHours(
|
||||||
|
task.CourseTotalHours,
|
||||||
|
task.CoursePracticeHours,
|
||||||
|
task.StartWeek,
|
||||||
|
task.EndWeek,
|
||||||
|
out var hours);
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Task = task,
|
||||||
|
Hours = hours
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
var scheduledHours = plan.Entries
|
var scheduledHours = plan.Entries
|
||||||
.GroupBy(x => x.TeachingTaskId)
|
.GroupBy(x => x.TeachingTaskId)
|
||||||
.ToDictionary(group => group.Key, group => group.Sum(x => x.PeriodCount));
|
.ToDictionary(group => group.Key, group => group.Sum(x => x.PeriodCount));
|
||||||
var incomplete = requiredTasks.FirstOrDefault(task =>
|
var incomplete = requiredWeeklyHours.FirstOrDefault(item =>
|
||||||
!scheduledHours.TryGetValue(task.Id, out var hours) ||
|
scheduledHours.GetValueOrDefault(item.Task.Id) < item.Hours);
|
||||||
hours < task.WeeklyHours);
|
|
||||||
if (incomplete is not null)
|
if (incomplete is not null)
|
||||||
{
|
{
|
||||||
throw new SchedulePublishValidationException(
|
throw new SchedulePublishValidationException(
|
||||||
$"{incomplete.TaskNumber} · {incomplete.Name} 尚未达到每周 " +
|
$"{incomplete.Task.TaskNumber} · {incomplete.Task.Name} 尚未达到每周 " +
|
||||||
$"{incomplete.WeeklyHours} 学时,不能发布。");
|
$"{incomplete.Hours} 个普通排课学时,不能发布。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var excessive = requiredWeeklyHours.FirstOrDefault(item =>
|
||||||
|
scheduledHours.GetValueOrDefault(item.Task.Id) > item.Hours);
|
||||||
|
if (excessive is not null)
|
||||||
|
{
|
||||||
|
var actualHours = scheduledHours.GetValueOrDefault(excessive.Task.Id);
|
||||||
|
throw new SchedulePublishValidationException(
|
||||||
|
$"{excessive.Task.TaskNumber} · {excessive.Task.Name} 已安排每周 " +
|
||||||
|
$"{actualHours} 学时,普通课表应为 {excessive.Hours} 学时;" +
|
||||||
|
"请删除已包含的实践学时后再发布。");
|
||||||
}
|
}
|
||||||
|
|
||||||
await reportProgress(3, "检查教师、行政班和教室冲突", cancellationToken);
|
await reportProgress(3, "检查教师、行政班和教室冲突", cancellationToken);
|
||||||
|
|||||||
@@ -7,15 +7,75 @@ public static class TeachingTaskHours
|
|||||||
public static int Calculate(int startWeek, int endWeek, int weeklyHours) =>
|
public static int Calculate(int startWeek, int endWeek, int weeklyHours) =>
|
||||||
endWeek < startWeek ? 0 : (endWeek - startWeek + 1) * weeklyHours;
|
endWeek < startWeek ? 0 : (endWeek - startWeek + 1) * weeklyHours;
|
||||||
|
|
||||||
|
public static int RegularScheduleHours(Course course) =>
|
||||||
|
RegularScheduleHours(course.TotalHours, course.PracticeHours);
|
||||||
|
|
||||||
|
public static int RegularScheduleHours(int totalHours, int practiceHours) =>
|
||||||
|
Math.Max(0, totalHours - practiceHours);
|
||||||
|
|
||||||
|
public static int TargetHours(
|
||||||
|
Course course,
|
||||||
|
TeachingTaskSchedulingMode schedulingMode) =>
|
||||||
|
schedulingMode == TeachingTaskSchedulingMode.Flexible
|
||||||
|
? course.TotalHours
|
||||||
|
: RegularScheduleHours(course);
|
||||||
|
|
||||||
|
public static bool TryResolveRegularWeeklyHours(
|
||||||
|
Course course,
|
||||||
|
int startWeek,
|
||||||
|
int endWeek,
|
||||||
|
out int weeklyHours) =>
|
||||||
|
TryResolveRegularWeeklyHours(
|
||||||
|
course.TotalHours,
|
||||||
|
course.PracticeHours,
|
||||||
|
startWeek,
|
||||||
|
endWeek,
|
||||||
|
out weeklyHours);
|
||||||
|
|
||||||
|
public static bool TryResolveRegularWeeklyHours(
|
||||||
|
int totalHours,
|
||||||
|
int practiceHours,
|
||||||
|
int startWeek,
|
||||||
|
int endWeek,
|
||||||
|
out int weeklyHours)
|
||||||
|
{
|
||||||
|
weeklyHours = 0;
|
||||||
|
var weekCount = endWeek - startWeek + 1;
|
||||||
|
var regularHours = RegularScheduleHours(totalHours, practiceHours);
|
||||||
|
if (weekCount <= 0 || regularHours % weekCount != 0) return false;
|
||||||
|
|
||||||
|
weeklyHours = regularHours / weekCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static string? Validate(
|
public static string? Validate(
|
||||||
Course course,
|
Course course,
|
||||||
int startWeek,
|
int startWeek,
|
||||||
int endWeek,
|
int endWeek,
|
||||||
int weeklyHours)
|
int weeklyHours,
|
||||||
|
TeachingTaskSchedulingMode schedulingMode =
|
||||||
|
TeachingTaskSchedulingMode.Standard)
|
||||||
{
|
{
|
||||||
var plannedHours = Calculate(startWeek, endWeek, weeklyHours);
|
var plannedHours = Calculate(startWeek, endWeek, weeklyHours);
|
||||||
return plannedHours == course.TotalHours
|
var targetHours = TargetHours(course, schedulingMode);
|
||||||
? null
|
if (plannedHours == targetHours) return null;
|
||||||
: $"课程“{course.Name}”总学时为 {course.TotalHours};当前第 {startWeek}—{endWeek} 周、每周 {weeklyHours} 学时,共 {plannedHours} 学时。请调整授课周次或周学时。";
|
|
||||||
|
if (schedulingMode == TeachingTaskSchedulingMode.Standard)
|
||||||
|
{
|
||||||
|
if (targetHours == 0)
|
||||||
|
{
|
||||||
|
return $"课程“{course.Name}”的 {course.TotalHours} 学时均为实践学时," +
|
||||||
|
"无需进入普通课表;请将授课方式设为“非排时课程”,并在实验管理中安排。";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"课程“{course.Name}”总学时为 {course.TotalHours},其中实践学时 " +
|
||||||
|
$"{course.PracticeHours},普通课表应安排 {targetHours} 学时;当前第 " +
|
||||||
|
$"{startWeek}—{endWeek} 周、每周 {weeklyHours} 学时,共 " +
|
||||||
|
$"{plannedHours} 学时。请调整授课周次或周学时。";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"课程“{course.Name}”总学时为 {course.TotalHours};当前第 " +
|
||||||
|
$"{startWeek}—{endWeek} 周、每周 {weeklyHours} 学时,共 " +
|
||||||
|
$"{plannedHours} 学时。请调整授课周次或周学时。";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,91 @@ public sealed class AutomaticScheduleGeneratorTests
|
|||||||
Assert.Empty(await db.ScheduleEntries.ToListAsync());
|
Assert.Empty(await db.ScheduleEntries.ToListAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Generator_excludes_practice_hours_from_regular_schedule()
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(connection)
|
||||||
|
.Options;
|
||||||
|
await using var db = new AppDbContext(options);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var term = new AcademicTerm
|
||||||
|
{
|
||||||
|
Code = "2026-LAB",
|
||||||
|
Name = "2026 实验学时测试",
|
||||||
|
AcademicYear = "2026-2027",
|
||||||
|
Season = TermSeason.Autumn,
|
||||||
|
StartDate = new DateOnly(2026, 9, 1),
|
||||||
|
EndDate = new DateOnly(2027, 1, 15)
|
||||||
|
};
|
||||||
|
var college = new College { Code = "LAB", Name = "实验学院" };
|
||||||
|
var course = new Course
|
||||||
|
{
|
||||||
|
Code = "LAB-01",
|
||||||
|
Name = "实验混合课程",
|
||||||
|
College = college,
|
||||||
|
Credits = 2,
|
||||||
|
TotalHours = 32,
|
||||||
|
LectureHours = 16,
|
||||||
|
PracticeHours = 16
|
||||||
|
};
|
||||||
|
var task = new TeachingTask
|
||||||
|
{
|
||||||
|
TaskNumber = "TASK-LAB",
|
||||||
|
Name = "实验混合教学班",
|
||||||
|
AcademicTerm = term,
|
||||||
|
Course = course,
|
||||||
|
Capacity = 30,
|
||||||
|
StartWeek = 1,
|
||||||
|
EndWeek = 16,
|
||||||
|
WeeklyHours = 2,
|
||||||
|
Status = TeachingTaskStatus.Published
|
||||||
|
};
|
||||||
|
var plan = new SchedulePlan
|
||||||
|
{
|
||||||
|
AcademicTerm = term,
|
||||||
|
Name = "实验学时拆分测试",
|
||||||
|
Version = "V1"
|
||||||
|
};
|
||||||
|
db.AddRange(term, college, course, task, plan);
|
||||||
|
db.ScheduleTimeSlots.AddRange(
|
||||||
|
new ScheduleTimeSlot
|
||||||
|
{
|
||||||
|
AcademicTerm = term,
|
||||||
|
PeriodNumber = 1,
|
||||||
|
Name = "第 1 节",
|
||||||
|
StartsAt = new TimeOnly(8, 0),
|
||||||
|
EndsAt = new TimeOnly(8, 45)
|
||||||
|
},
|
||||||
|
new ScheduleTimeSlot
|
||||||
|
{
|
||||||
|
AcademicTerm = term,
|
||||||
|
PeriodNumber = 2,
|
||||||
|
Name = "第 2 节",
|
||||||
|
StartsAt = new TimeOnly(8, 55),
|
||||||
|
EndsAt = new TimeOnly(9, 40)
|
||||||
|
});
|
||||||
|
db.TeachingTaskScheduleConstraints.Add(
|
||||||
|
new TeachingTaskScheduleConstraint
|
||||||
|
{
|
||||||
|
TeachingTask = task,
|
||||||
|
RequiresClassroom = false
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await new AutomaticScheduleGenerator(db)
|
||||||
|
.GenerateAsync(plan, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(1, result.CreatedEntries);
|
||||||
|
Assert.Equal(1, result.CompletedTasks);
|
||||||
|
Assert.Equal(
|
||||||
|
1,
|
||||||
|
(await db.ScheduleEntries.SingleAsync()).PeriodCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Generator_schedules_roomless_course_within_allowed_time()
|
public async Task Generator_schedules_roomless_course_within_allowed_time()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public sealed class SchedulePublishJobProcessorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Processor_validates_and_publishes_plan_atomically()
|
public async Task Processor_validates_and_publishes_plan_atomically()
|
||||||
{
|
{
|
||||||
var result = await RunPublishAsync(weeklyHours: 2);
|
var result = await RunPublishAsync(weeklyHours: 2, scheduledHours: 2);
|
||||||
|
|
||||||
Assert.Equal(SchedulePublishJobStatus.Succeeded, result.JobStatus);
|
Assert.Equal(SchedulePublishJobStatus.Succeeded, result.JobStatus);
|
||||||
Assert.Equal(SchedulePlanStatus.Published, result.PlanStatus);
|
Assert.Equal(SchedulePlanStatus.Published, result.PlanStatus);
|
||||||
@@ -26,17 +26,46 @@ public sealed class SchedulePublishJobProcessorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Processor_keeps_draft_when_validation_fails()
|
public async Task Processor_keeps_draft_when_validation_fails()
|
||||||
{
|
{
|
||||||
var result = await RunPublishAsync(weeklyHours: 4);
|
var result = await RunPublishAsync(weeklyHours: 2, scheduledHours: 1);
|
||||||
|
|
||||||
Assert.Equal(SchedulePublishJobStatus.Failed, result.JobStatus);
|
Assert.Equal(SchedulePublishJobStatus.Failed, result.JobStatus);
|
||||||
Assert.Equal(SchedulePlanStatus.Draft, result.PlanStatus);
|
Assert.Equal(SchedulePlanStatus.Draft, result.PlanStatus);
|
||||||
Assert.Null(result.ActiveAcademicTermId);
|
Assert.Null(result.ActiveAcademicTermId);
|
||||||
Assert.Equal("检查未通过", result.CurrentStep);
|
Assert.Equal("检查未通过", result.CurrentStep);
|
||||||
Assert.Contains("尚未达到每周 4 学时", result.ErrorMessage);
|
Assert.Contains("尚未达到每周 2 个普通排课学时", result.ErrorMessage);
|
||||||
Assert.Null(result.PublishedAt);
|
Assert.Null(result.PublishedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<PublishResult> RunPublishAsync(int weeklyHours)
|
[Fact]
|
||||||
|
public async Task Processor_excludes_practice_hours_for_existing_tasks()
|
||||||
|
{
|
||||||
|
var result = await RunPublishAsync(
|
||||||
|
weeklyHours: 2,
|
||||||
|
scheduledHours: 1,
|
||||||
|
practiceHours: 16);
|
||||||
|
|
||||||
|
Assert.Equal(SchedulePublishJobStatus.Succeeded, result.JobStatus);
|
||||||
|
Assert.Equal(SchedulePlanStatus.Published, result.PlanStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Processor_rejects_practice_hours_already_added_to_draft()
|
||||||
|
{
|
||||||
|
var result = await RunPublishAsync(
|
||||||
|
weeklyHours: 2,
|
||||||
|
scheduledHours: 2,
|
||||||
|
practiceHours: 16);
|
||||||
|
|
||||||
|
Assert.Equal(SchedulePublishJobStatus.Failed, result.JobStatus);
|
||||||
|
Assert.Equal(SchedulePlanStatus.Draft, result.PlanStatus);
|
||||||
|
Assert.Contains("普通课表应为 1 学时", result.ErrorMessage);
|
||||||
|
Assert.Contains("删除已包含的实践学时", result.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<PublishResult> RunPublishAsync(
|
||||||
|
int weeklyHours,
|
||||||
|
int scheduledHours,
|
||||||
|
int practiceHours = 0)
|
||||||
{
|
{
|
||||||
var databasePath = Path.Combine(
|
var databasePath = Path.Combine(
|
||||||
Path.GetTempPath(),
|
Path.GetTempPath(),
|
||||||
@@ -75,7 +104,8 @@ public sealed class SchedulePublishJobProcessorTests
|
|||||||
College = college,
|
College = college,
|
||||||
Credits = 1,
|
Credits = 1,
|
||||||
TotalHours = 32,
|
TotalHours = 32,
|
||||||
LectureHours = 32
|
LectureHours = 32 - practiceHours,
|
||||||
|
PracticeHours = practiceHours
|
||||||
};
|
};
|
||||||
var task = new TeachingTask
|
var task = new TeachingTask
|
||||||
{
|
{
|
||||||
@@ -100,7 +130,7 @@ public sealed class SchedulePublishJobProcessorTests
|
|||||||
TeachingTask = task,
|
TeachingTask = task,
|
||||||
DayOfWeek = 1,
|
DayOfWeek = 1,
|
||||||
StartPeriod = 1,
|
StartPeriod = 1,
|
||||||
PeriodCount = 2,
|
PeriodCount = scheduledHours,
|
||||||
StartWeek = 1,
|
StartWeek = 1,
|
||||||
EndWeek = 16,
|
EndWeek = 16,
|
||||||
WeekPattern = WeekPattern.All
|
WeekPattern = WeekPattern.All
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ public sealed class ScheduleSettingsControllerTests
|
|||||||
});
|
});
|
||||||
Assert.Contains("测试教师", json);
|
Assert.Contains("测试教师", json);
|
||||||
Assert.Contains(classroom.Id.ToString(), json);
|
Assert.Contains(classroom.Id.ToString(), json);
|
||||||
|
Assert.Contains("\"WeeklyHours\":3", json);
|
||||||
|
Assert.Contains("\"CoursePracticeHours\":16", json);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -12,6 +12,114 @@ namespace Jiaowu.Api.Tests;
|
|||||||
|
|
||||||
public sealed class SchedulesControllerTests
|
public sealed class SchedulesControllerTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Manual_entry_rejects_hours_reserved_for_experiments()
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(connection)
|
||||||
|
.Options;
|
||||||
|
await using var db = new AppDbContext(options);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var term = new AcademicTerm
|
||||||
|
{
|
||||||
|
Code = "2026-LAB",
|
||||||
|
Name = "2026 实验学时测试",
|
||||||
|
AcademicYear = "2026-2027",
|
||||||
|
Season = TermSeason.Autumn,
|
||||||
|
StartDate = new DateOnly(2026, 9, 7),
|
||||||
|
EndDate = new DateOnly(2027, 1, 17)
|
||||||
|
};
|
||||||
|
var college = new College { Code = "LAB", Name = "实验学院" };
|
||||||
|
var course = new Course
|
||||||
|
{
|
||||||
|
Code = "LAB-01",
|
||||||
|
Name = "实验混合课程",
|
||||||
|
College = college,
|
||||||
|
Credits = 2,
|
||||||
|
TotalHours = 32,
|
||||||
|
LectureHours = 16,
|
||||||
|
PracticeHours = 16
|
||||||
|
};
|
||||||
|
var task = new TeachingTask
|
||||||
|
{
|
||||||
|
TaskNumber = "TASK-LAB",
|
||||||
|
Name = "实验混合教学班",
|
||||||
|
AcademicTerm = term,
|
||||||
|
Course = course,
|
||||||
|
Capacity = 30,
|
||||||
|
StartWeek = 1,
|
||||||
|
EndWeek = 16,
|
||||||
|
WeeklyHours = 2,
|
||||||
|
Status = TeachingTaskStatus.Published
|
||||||
|
};
|
||||||
|
var plan = new SchedulePlan
|
||||||
|
{
|
||||||
|
AcademicTerm = term,
|
||||||
|
Name = "排课草稿",
|
||||||
|
Version = "V1"
|
||||||
|
};
|
||||||
|
plan.Entries.Add(new ScheduleEntry
|
||||||
|
{
|
||||||
|
TeachingTask = task,
|
||||||
|
DayOfWeek = 1,
|
||||||
|
StartPeriod = 1,
|
||||||
|
PeriodCount = 1,
|
||||||
|
StartWeek = 1,
|
||||||
|
EndWeek = 16,
|
||||||
|
WeekPattern = WeekPattern.All
|
||||||
|
});
|
||||||
|
db.AddRange(term, college, course, task, plan);
|
||||||
|
db.ScheduleTimeSlots.AddRange(
|
||||||
|
new ScheduleTimeSlot
|
||||||
|
{
|
||||||
|
AcademicTerm = term,
|
||||||
|
PeriodNumber = 1,
|
||||||
|
Name = "第 1 节",
|
||||||
|
StartsAt = new TimeOnly(8, 0),
|
||||||
|
EndsAt = new TimeOnly(8, 45)
|
||||||
|
},
|
||||||
|
new ScheduleTimeSlot
|
||||||
|
{
|
||||||
|
AcademicTerm = term,
|
||||||
|
PeriodNumber = 2,
|
||||||
|
Name = "第 2 节",
|
||||||
|
StartsAt = new TimeOnly(8, 55),
|
||||||
|
EndsAt = new TimeOnly(9, 40)
|
||||||
|
});
|
||||||
|
db.TeachingTaskScheduleConstraints.Add(
|
||||||
|
new TeachingTaskScheduleConstraint
|
||||||
|
{
|
||||||
|
TeachingTask = task,
|
||||||
|
RequiresClassroom = false
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var controller = new SchedulesController(db);
|
||||||
|
var result = await controller.CreateEntry(
|
||||||
|
plan.Id,
|
||||||
|
new ScheduleEntryRequest(
|
||||||
|
task.Id,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
16,
|
||||||
|
WeekPattern.All,
|
||||||
|
null),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
var problem = Assert.IsType<ObjectResult>(result);
|
||||||
|
var details = Assert.IsType<ValidationProblemDetails>(problem.Value);
|
||||||
|
Assert.Contains(
|
||||||
|
"实践学时请在实验管理中安排",
|
||||||
|
details.Detail);
|
||||||
|
Assert.Single(await db.ScheduleEntries.ToListAsync());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Publish_returns_accepted_before_background_validation_runs()
|
public async Task Publish_returns_accepted_before_background_validation_runs()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,4 +30,57 @@ public sealed class TeachingTaskHoursTests
|
|||||||
Assert.Contains("总学时为 32", result);
|
Assert.Contains("总学时为 32", result);
|
||||||
Assert.Contains("共 16 学时", result);
|
Assert.Contains("共 16 学时", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Standard_schedule_excludes_practice_hours()
|
||||||
|
{
|
||||||
|
var course = new Course
|
||||||
|
{
|
||||||
|
Code = "TEST-LAB",
|
||||||
|
Name = "实验混合课程",
|
||||||
|
CollegeId = Guid.NewGuid(),
|
||||||
|
Credits = 4,
|
||||||
|
TotalHours = 64,
|
||||||
|
LectureHours = 48,
|
||||||
|
PracticeHours = 16
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(48, TeachingTaskHours.RegularScheduleHours(course));
|
||||||
|
Assert.True(TeachingTaskHours.TryResolveRegularWeeklyHours(
|
||||||
|
course,
|
||||||
|
1,
|
||||||
|
16,
|
||||||
|
out var weeklyHours));
|
||||||
|
Assert.Equal(3, weeklyHours);
|
||||||
|
Assert.Null(TeachingTaskHours.Validate(course, 1, 16, 3));
|
||||||
|
|
||||||
|
var result = TeachingTaskHours.Validate(course, 1, 16, 4);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Contains("普通课表应安排 48 学时", result);
|
||||||
|
Assert.Contains("实践学时 16", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Flexible_schedule_keeps_total_workload_hours()
|
||||||
|
{
|
||||||
|
var course = new Course
|
||||||
|
{
|
||||||
|
Code = "TEST-PRACTICE",
|
||||||
|
Name = "实践课程",
|
||||||
|
CollegeId = Guid.NewGuid(),
|
||||||
|
Credits = 1,
|
||||||
|
TotalHours = 16,
|
||||||
|
PracticeHours = 16
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Null(TeachingTaskHours.Validate(
|
||||||
|
course,
|
||||||
|
1,
|
||||||
|
16,
|
||||||
|
1,
|
||||||
|
TeachingTaskSchedulingMode.Flexible));
|
||||||
|
Assert.Contains(
|
||||||
|
"无需进入普通课表",
|
||||||
|
TeachingTaskHours.Validate(course, 1, 16, 1)!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -988,7 +988,7 @@ onBeforeUnmount(() => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="selectedTaskConstraint"
|
v-if="selectedTaskConstraint"
|
||||||
:title="`可排第 ${selectedTaskConstraint.startWeek}—${selectedTaskConstraint.endWeek} 周;允许上课日:${entryWeekdays.map((day) => day.label).join('、')}`"
|
:title="`普通课表每周 ${selectedTaskConstraint.weeklyHours} 学时(实践 ${selectedTaskConstraint.coursePracticeHours} 学时另由实验管理安排);可排第 ${selectedTaskConstraint.startWeek}—${selectedTaskConstraint.endWeek} 周;允许上课日:${entryWeekdays.map((day) => day.label).join('、')}`"
|
||||||
type="info"
|
type="info"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
show-icon
|
show-icon
|
||||||
|
|||||||
@@ -77,13 +77,22 @@ const availableClasses = computed(() => {
|
|||||||
const selectedCourse = computed(() =>
|
const selectedCourse = computed(() =>
|
||||||
courses.value.find((item) => item.id === form.courseId),
|
courses.value.find((item) => item.id === form.courseId),
|
||||||
)
|
)
|
||||||
|
const regularScheduleHours = (course: any) =>
|
||||||
|
Math.max(0, Number(course?.totalHours ?? 0) - Number(course?.practiceHours ?? 0))
|
||||||
|
const targetCourseHours = (course: any, schedulingMode: string) =>
|
||||||
|
schedulingMode === 'Flexible'
|
||||||
|
? Number(course?.totalHours ?? 0)
|
||||||
|
: regularScheduleHours(course)
|
||||||
|
const selectedCourseTargetHours = computed(() =>
|
||||||
|
targetCourseHours(selectedCourse.value, form.schedulingMode),
|
||||||
|
)
|
||||||
const plannedHours = computed(() =>
|
const plannedHours = computed(() =>
|
||||||
form.startWeek && form.endWeek && form.weeklyHours && form.endWeek >= form.startWeek
|
form.startWeek && form.endWeek && form.weeklyHours && form.endWeek >= form.startWeek
|
||||||
? (form.endWeek - form.startWeek + 1) * form.weeklyHours
|
? (form.endWeek - form.startWeek + 1) * form.weeklyHours
|
||||||
: 0,
|
: 0,
|
||||||
)
|
)
|
||||||
const hoursMatch = computed(() =>
|
const hoursMatch = computed(() =>
|
||||||
!selectedCourse.value || plannedHours.value === selectedCourse.value.totalHours,
|
!selectedCourse.value || plannedHours.value === selectedCourseTargetHours.value,
|
||||||
)
|
)
|
||||||
const assignableTeachers = computed(() =>
|
const assignableTeachers = computed(() =>
|
||||||
manualEligibleTeachers.value.map((item) => ({
|
manualEligibleTeachers.value.map((item) => ({
|
||||||
@@ -115,7 +124,7 @@ const generationPlannedHours = computed(() =>
|
|||||||
)
|
)
|
||||||
const generationHoursMatch = computed(() =>
|
const generationHoursMatch = computed(() =>
|
||||||
!selectedGenerationCourse.value ||
|
!selectedGenerationCourse.value ||
|
||||||
generationPlannedHours.value === selectedGenerationCourse.value.totalHours,
|
generationPlannedHours.value === regularScheduleHours(selectedGenerationCourse.value),
|
||||||
)
|
)
|
||||||
const manageableCourses = computed(() => {
|
const manageableCourses = computed(() => {
|
||||||
if (isSuperAdmin.value) return courses.value
|
if (isSuperAdmin.value) return courses.value
|
||||||
@@ -355,7 +364,7 @@ async function save() {
|
|||||||
}
|
}
|
||||||
if (!hoursMatch.value) {
|
if (!hoursMatch.value) {
|
||||||
ElMessage.warning(
|
ElMessage.warning(
|
||||||
`课程总学时为 ${selectedCourse.value.totalHours},当前授课安排合计 ${plannedHours.value} 学时,请调整周次或周学时。`,
|
`该课程普通课表应安排 ${selectedCourseTargetHours.value} 学时,当前安排合计 ${plannedHours.value} 学时;实践学时请在实验管理中安排。`,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -542,7 +551,7 @@ async function generatePublicTasks() {
|
|||||||
}
|
}
|
||||||
if (!generationHoursMatch.value) {
|
if (!generationHoursMatch.value) {
|
||||||
ElMessage.warning(
|
ElMessage.warning(
|
||||||
`课程总学时为 ${selectedGenerationCourse.value.totalHours},当前授课安排合计 ${generationPlannedHours.value} 学时,请调整周次或周学时。`,
|
`该课程普通课表应安排 ${regularScheduleHours(selectedGenerationCourse.value)} 学时,当前安排合计 ${generationPlannedHours.value} 学时;实践学时不进入普通课表。`,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -683,6 +692,9 @@ onMounted(async () => {
|
|||||||
<el-table-column label="行政班" min-width="150"><template #default="{ row }">{{ row.classNames.join('、') || '开放选课' }}</template></el-table-column>
|
<el-table-column label="行政班" min-width="150"><template #default="{ row }">{{ row.classNames.join('、') || '开放选课' }}</template></el-table-column>
|
||||||
<el-table-column label="人数 / 容量" width="105"><template #default="{ row }">{{ row.studentCount }} / {{ row.capacity }}</template></el-table-column>
|
<el-table-column label="人数 / 容量" width="105"><template #default="{ row }">{{ row.studentCount }} / {{ row.capacity }}</template></el-table-column>
|
||||||
<el-table-column label="周次" width="105"><template #default="{ row }">{{ row.startWeek }}—{{ row.endWeek }} 周</template></el-table-column>
|
<el-table-column label="周次" width="105"><template #default="{ row }">{{ row.startWeek }}—{{ row.endWeek }} 周</template></el-table-column>
|
||||||
|
<el-table-column label="常规 / 实践学时" width="135">
|
||||||
|
<template #default="{ row }">{{ row.courseRegularScheduleHours }} / {{ row.coursePracticeHours }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="90"><template #default="{ row }"><span class="table-status" :class="{ off: row.status === 'Closed' }">{{ statusLabels[row.status] }}</span></template></el-table-column>
|
<el-table-column label="状态" width="90"><template #default="{ row }"><span class="table-status" :class="{ off: row.status === 'Closed' }">{{ statusLabels[row.status] }}</span></template></el-table-column>
|
||||||
<el-table-column label="操作" width="210" fixed="right">
|
<el-table-column label="操作" width="210" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -714,7 +726,12 @@ onMounted(async () => {
|
|||||||
<el-form-item label="开课学期" required><el-select v-model="form.academicTermId" @change="loadManualEligibleTeachers"><el-option v-for="item in terms" :key="item.id" :label="academicTermLabel(item)" :value="item.id" :class="academicTermOptionClass(item)" /></el-select></el-form-item>
|
<el-form-item label="开课学期" required><el-select v-model="form.academicTermId" @change="loadManualEligibleTeachers"><el-option v-for="item in terms" :key="item.id" :label="academicTermLabel(item)" :value="item.id" :class="academicTermOptionClass(item)" /></el-select></el-form-item>
|
||||||
<el-form-item label="课程" required>
|
<el-form-item label="课程" required>
|
||||||
<el-select v-model="form.courseId" filterable @change="form.teacherIds = []; form.primaryTeacherId = undefined; loadManualEligibleTeachers()">
|
<el-select v-model="form.courseId" filterable @change="form.teacherIds = []; form.primaryTeacherId = undefined; loadManualEligibleTeachers()">
|
||||||
<el-option v-for="item in filteredManageableCourses" :key="item.id" :label="`${item.code} · ${item.name}(${item.totalHours} 学时)`" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in filteredManageableCourses"
|
||||||
|
:key="item.id"
|
||||||
|
:label="`${item.code} · ${item.name}(常规 ${regularScheduleHours(item)} / 实践 ${item.practiceHours} 学时)`"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -744,7 +761,10 @@ onMounted(async () => {
|
|||||||
<b>{{ hoursMatch ? '学时匹配' : '学时不匹配' }}</b>
|
<b>{{ hoursMatch ? '学时匹配' : '学时不匹配' }}</b>
|
||||||
<span>
|
<span>
|
||||||
第 {{ form.startWeek }}—{{ form.endWeek }} 周 × 每周 {{ form.weeklyHours }} 学时
|
第 {{ form.startWeek }}—{{ form.endWeek }} 周 × 每周 {{ form.weeklyHours }} 学时
|
||||||
= {{ plannedHours }} 学时;课程库总学时 {{ selectedCourse.totalHours }}。
|
= {{ plannedHours }} 学时;当前授课方式应计 {{ selectedCourseTargetHours }} 学时。
|
||||||
|
<template v-if="form.schedulingMode === 'Standard'">
|
||||||
|
实践 {{ selectedCourse.practiceHours }} 学时由实验管理单独安排。
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<el-form-item label="授课方式">
|
<el-form-item label="授课方式">
|
||||||
@@ -753,7 +773,7 @@ onMounted(async () => {
|
|||||||
<el-radio-button value="Flexible">非排时课程</el-radio-button>
|
<el-radio-button value="Flexible">非排时课程</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<small class="field-hint">
|
<small class="field-hint">
|
||||||
非排时课程不进入自动排课,不占用星期、节次和场地,并在课表中单独列出。
|
正常排课只安排非实践学时;全部由实验模块安排的课程请选择“非排时课程”。
|
||||||
</small>
|
</small>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="授课教师">
|
<el-form-item label="授课教师">
|
||||||
@@ -881,8 +901,9 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
<b>{{ generationHoursMatch ? '学时匹配' : '学时不匹配' }}</b>
|
<b>{{ generationHoursMatch ? '学时匹配' : '学时不匹配' }}</b>
|
||||||
<span>
|
<span>
|
||||||
当前合计 {{ generationPlannedHours }} 学时;课程库总学时
|
当前合计 {{ generationPlannedHours }} 学时;普通课表应安排
|
||||||
{{ selectedGenerationCourse.totalHours }}。
|
{{ regularScheduleHours(selectedGenerationCourse) }} 学时,实践
|
||||||
|
{{ selectedGenerationCourse.practiceHours }} 学时由实验管理单独安排。
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="generation-preview">
|
<div class="generation-preview">
|
||||||
|
|||||||
Reference in New Issue
Block a user