diff --git a/README.md b/README.md index 9af7ef5..6d81e5c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ 当前已实现系统登录与角色权限、基础数据、用户管理、教师档案、学生档案、课程库、培养方案、教学任务、排课课表、学生选课、成绩管理、考试考场、学籍异动、毕业审核、学位授予、毕业离校和首页统计。人员及课程列表支持组合筛选、服务端分页和完整增删改查;培养方案支持课程模块、专业年级版本、复制新版本、发布锁定和旧版本归档;教学任务支持学期课程开设、多教师、合班、容量校验、发布与结课,公共课由校级教务负责、专业必修/专业选修/实践课下放课程所属学院管理,并支持教师按学期申报授课科目、学院审核授课资格、公共课按若干行政班合并教学班,以及在审核通过的教师池中随机均衡分配后批量生成草稿;排课支持学期作息维护、单双周与周次节次、课程可用时间、校区/教学楼/指定教室约束、不占用教室课程、教室容量、教师/行政班/教室冲突校验、自动生成、手工微调和版本化发布;选课支持批次时间窗、投放范围、容量与学分上限、重复课程与课表冲突校验、退课截止时间和实时教学班名单;成绩管理支持分项比例、批量录入、特殊考试状态、自动总评与绩点、教师提交、学院审核、校级发布和学生成绩单;考试管理支持考试计划、场次、考场容量、监考教师、考生名单以及考场/监考/学生时间冲突校验;学籍异动支持休学、复学、退学申请,辅导员、学院、学校三级顺序审核,学生撤回,以及最终审批后自动同步学籍状态;毕业审核按入学年级匹配已发布培养方案,以正式成绩计算总学分、必修通过和未解决不及格课程,支持学院范围查看、人工复核、校级锁定发布和学生结果查询;学位授予以已发布毕业资格为来源,按正式成绩加权平均绩点生成规则结论,支持学院人工复核、校级发布锁定和学生结果查询;毕业离校支持自定义事项与责任部门,按校级、学院、辅导员角色分工办理,强制数据范围校验,学生进度查询,以及必办事项全部完成后的批次锁定。 +课程库支持下载标准模板后批量导入 `.xlsx`,按课程编码新增或更新,并在整批校验失败时不写入任何课程;授课资格既支持教师申报后审核,也支持学院在本院教师范围内直接分配;学生可在“我的培养方案”中查看本人适用的已发布方案,并按已完成、在读、重修中、未通过、待完成和未修读状态核对课程与学分进度。 + 权限采用后端强制校验的角色与数据范围模型。多角色账号按 `All > College > Class > Self` 取最高数据范围:校级角色可访问全校数据,院系管理员限定本学院,辅导员通过稳定的账号 ID 绑定所带行政班,教师和学生限定本人及当前教学关系;前端菜单和路由限制仅作为交互辅助,不替代 API 授权。 人员档案与登录账号分开维护。新增或 Excel 导入学生、教师档案时不会自动创建账号,也不会在修改档案时同步账号。学生首次使用时可以在登录页进入“自助激活”,填写姓名、学号、学院、专业、年级和行政班;全部匹配在籍档案后自行设置密码,系统才创建 Identity 登录账号并关联学生角色。`AspNetUsers` 作为 ASP.NET Core Identity 的内部安全存储,负责密码哈希、登录锁定、角色和令牌。 diff --git a/src/Jiaowu.Api/Controllers/StudentCurriculumController.cs b/src/Jiaowu.Api/Controllers/StudentCurriculumController.cs new file mode 100644 index 0000000..204ee22 --- /dev/null +++ b/src/Jiaowu.Api/Controllers/StudentCurriculumController.cs @@ -0,0 +1,246 @@ +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Graduation; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Controllers; + +[ApiController] +[Authorize(Roles = SystemRoles.Student)] +[Route("api/student/curriculum-plan")] +public sealed class StudentCurriculumController( + AppDbContext db, + ICurrentUserDataScope currentUserDataScope) : ControllerBase +{ + [HttpGet] + public async Task GetMine(CancellationToken cancellationToken) + { + var userId = currentUserDataScope.Current.UserId; + var student = await db.Students.AsNoTracking() + .Include(x => x.AdministrativeClass) + .ThenInclude(x => x!.Major) + .ThenInclude(x => x!.College) + .FirstOrDefaultAsync(x => x.UserId == userId, cancellationToken); + if (student is null) + return ConflictProblem("当前账号尚未关联学生档案。"); + + var studentProfile = new + { + student.Id, + student.StudentNumber, + student.Name, + student.EnrollmentYear, + student.Status, + ClassName = student.AdministrativeClass!.Name, + MajorName = student.AdministrativeClass.Major!.Name, + CollegeName = student.AdministrativeClass.Major.College!.Name + }; + + var plan = await db.CurriculumPlans.AsNoTracking() + .AsSplitQuery() + .Include(x => x.Major) + .ThenInclude(x => x!.College) + .Include(x => x.Modules) + .ThenInclude(x => x.Courses) + .ThenInclude(x => x.Course) + .ThenInclude(x => x!.CourseCategory) + .Where(x => + x.MajorId == student.AdministrativeClass.MajorId && + x.EffectiveGrade == student.EnrollmentYear && + x.Status == CurriculumPlanStatus.Published) + .OrderByDescending(x => x.PublishedAt) + .FirstOrDefaultAsync(cancellationToken); + + if (plan is null) + { + return Ok(new + { + Student = studentProfile, + Plan = (object?)null, + Message = + $"{student.EnrollmentYear} 级{student.AdministrativeClass.Major.Name}尚未发布培养方案。" + }); + } + + var planCourseIds = plan.Modules + .SelectMany(x => x.Courses) + .Select(x => x.CourseId) + .Distinct() + .ToArray(); + var gradeAttempts = await db.GradeRecords.AsNoTracking() + .Where(x => + x.StudentId == student.Id && + planCourseIds.Contains(x.GradeSheet!.TeachingTask!.CourseId) && + x.GradeSheet.Status == GradeSheetStatus.Published) + .Select(x => new StudentGradeAttempt( + x.GradeSheet!.TeachingTask!.CourseId, + x.TotalScore, + x.GradePoint, + x.ExamStatus, + x.GradeSheet.TeachingTask.AcademicTerm!.Name, + x.GradeSheet.TeachingTask.TaskNumber, + x.GradeSheet.PublishedAt ?? x.GradeSheet.UpdatedAt)) + .ToListAsync(cancellationToken); + + var currentCourseIds = await db.TeachingTasks.AsNoTracking() + .Where(task => + planCourseIds.Contains(task.CourseId) && + task.Status == TeachingTaskStatus.Published && + task.AcademicTerm!.IsCurrent && + (task.Classes.Any(item => + item.AdministrativeClassId == student.AdministrativeClassId) || + db.CourseEnrollments.Any(enrollment => + enrollment.StudentId == student.Id && + enrollment.Status == CourseEnrollmentStatus.Enrolled && + enrollment.CourseSelectionOffering!.TeachingTaskId == task.Id))) + .Select(x => x.CourseId) + .Distinct() + .ToListAsync(cancellationToken); + var currentCourseIdSet = currentCourseIds.ToHashSet(); + var attemptsByCourse = gradeAttempts + .GroupBy(x => x.CourseId) + .ToDictionary(x => x.Key, x => x.OrderByDescending( + attempt => attempt.PublishedAt).ToList()); + + var progressByCourse = new Dictionary(); + foreach (var courseId in planCourseIds) + { + var attempts = attemptsByCourse.GetValueOrDefault(courseId) ?? []; + var evaluation = StudentCourseProgressRules.Evaluate( + attempts.Select(x => new StudentCourseAttemptSnapshot( + x.TotalScore, + x.ExamStatus)), + currentCourseIdSet.Contains(courseId)); + var displayedAttempt = evaluation.HasPassedAttempt + ? attempts.FirstOrDefault(x => StudentCourseProgressRules.IsPassed( + new StudentCourseAttemptSnapshot(x.TotalScore, x.ExamStatus))) + : attempts.FirstOrDefault(); + progressByCourse[courseId] = new CourseProgress( + evaluation, + displayedAttempt); + } + + var distinctCourses = plan.Modules + .SelectMany(x => x.Courses) + .GroupBy(x => x.CourseId) + .Select(x => x.First()) + .ToList(); + var completedCredits = distinctCourses + .Where(x => + progressByCourse[x.CourseId].Evaluation.Status == + StudentCourseProgressStatus.Completed) + .Sum(x => x.Course?.Credits ?? 0); + var completedCourseCount = distinctCourses.Count(x => + progressByCourse[x.CourseId].Evaluation.Status == + StudentCourseProgressStatus.Completed); + var inProgressCourseCount = distinctCourses.Count(x => + progressByCourse[x.CourseId].Evaluation.Status is + StudentCourseProgressStatus.InProgress or + StudentCourseProgressStatus.Retaking); + var failedCourseCount = distinctCourses.Count(x => + progressByCourse[x.CourseId].Evaluation.HasFailedAttempt && + !progressByCourse[x.CourseId].Evaluation.HasPassedAttempt); + + return Ok(new + { + Student = studentProfile, + Plan = new + { + plan.Id, + plan.Name, + plan.Version, + plan.EffectiveGrade, + plan.TotalCredits, + plan.Description, + plan.PublishedAt, + MajorName = plan.Major!.Name, + CollegeName = plan.Major.College!.Name, + SchoolingYears = plan.Major.SchoolingYears, + Summary = new + { + CourseCount = distinctCourses.Count, + CompletedCourseCount = completedCourseCount, + InProgressCourseCount = inProgressCourseCount, + FailedCourseCount = failedCourseCount, + CompletedCredits = completedCredits, + CompletionRate = plan.TotalCredits <= 0 + ? 0 + : Math.Min(100, Math.Round( + completedCredits / plan.TotalCredits * 100, + 1)) + }, + Modules = plan.Modules + .OrderBy(x => x.SortOrder) + .ThenBy(x => x.Code) + .Select(module => new + { + module.Id, + module.Code, + module.Name, + module.RequiredCredits, + module.SortOrder, + Courses = module.Courses + .OrderBy(x => x.RecommendedSemester) + .ThenBy(x => x.Course!.Code) + .Select(item => + { + var progress = progressByCourse[item.CourseId]; + return new + { + item.Id, + item.CourseId, + CourseCode = item.Course!.Code, + CourseName = item.Course.Name, + item.Course.Credits, + item.Course.TotalHours, + item.Course.Nature, + CategoryName = item.Course.CourseCategory?.Name, + item.RecommendedSemester, + item.Type, + item.Notes, + progress.Evaluation.Status, + progress.Evaluation.HasFailedAttempt, + progress.Evaluation.HasPassedAttempt, + LatestResult = progress.DisplayedAttempt is null + ? null + : new + { + progress.DisplayedAttempt.TotalScore, + progress.DisplayedAttempt.GradePoint, + progress.DisplayedAttempt.ExamStatus, + progress.DisplayedAttempt.TermName, + progress.DisplayedAttempt.TaskNumber, + progress.DisplayedAttempt.PublishedAt + } + }; + }) + }) + } + }); + } + + private ActionResult ConflictProblem(string detail) => + Conflict(new ProblemDetails + { + Title = "无法查看培养方案", + Detail = detail, + Status = StatusCodes.Status409Conflict + }); + + private sealed record StudentGradeAttempt( + Guid CourseId, + decimal? TotalScore, + decimal? GradePoint, + GradeExamStatus ExamStatus, + string TermName, + string TaskNumber, + DateTime PublishedAt); + + private sealed record CourseProgress( + StudentCourseProgressEvaluation Evaluation, + StudentGradeAttempt? DisplayedAttempt); +} diff --git a/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs b/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs index 4be9a3f..8d58dab 100644 --- a/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs +++ b/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs @@ -175,6 +175,102 @@ public sealed class TeacherCourseApplicationsController( .ToListAsync(cancellationToken)); } + [HttpGet("assignment-options")] + [Authorize(Roles = ReviewRoles)] + public async Task GetAssignmentOptions( + CancellationToken cancellationToken) + { + var teachers = db.Teachers.AsNoTracking() + .Where(x => x.Status == TeacherStatus.Active); + var collegeId = currentUserDataScope.Current.RestrictedCollegeId; + if (collegeId.HasValue) + teachers = teachers.Where(x => x.CollegeId == collegeId.Value); + + return Ok(new + { + Teachers = await teachers + .OrderBy(x => x.TeacherNumber) + .Select(x => new + { + x.Id, + x.TeacherNumber, + x.Name, + x.Title, + x.CollegeId, + CollegeName = x.College!.Name + }) + .ToListAsync(cancellationToken), + Courses = await db.Courses.AsNoTracking() + .Where(x => x.IsEnabled) + .OrderBy(x => x.SortOrder) + .ThenBy(x => x.Code) + .Select(x => new + { + x.Id, + x.Code, + x.Name, + x.CollegeId, + CollegeName = x.College!.Name, + x.Credits, + x.Nature + }) + .ToListAsync(cancellationToken) + }); + } + + [HttpPost("assign")] + [Authorize(Roles = ReviewRoles)] + public async Task Assign( + TeacherCourseAssignmentRequest request, + CancellationToken cancellationToken) + { + if (!await db.AcademicTerms.AnyAsync( + x => x.Id == request.AcademicTermId && x.IsEnabled, + cancellationToken)) + return ValidationProblem("所选学期不存在或已停用。"); + if (!await db.Courses.AnyAsync( + x => x.Id == request.CourseId && x.IsEnabled, + cancellationToken)) + return ValidationProblem("所选课程不存在或已停用。"); + + var teachers = db.Teachers.Where(x => + x.Id == request.TeacherId && + x.Status == TeacherStatus.Active); + var collegeId = currentUserDataScope.Current.RestrictedCollegeId; + if (collegeId.HasValue) + teachers = teachers.Where(x => x.CollegeId == collegeId.Value); + if (!await teachers.AnyAsync(cancellationToken)) + return ValidationProblem("所选教师不存在、不在职或不属于当前学院。"); + + var application = await db.TeacherCourseApplications + .FirstOrDefaultAsync(x => + x.AcademicTermId == request.AcademicTermId && + x.TeacherId == request.TeacherId && + x.CourseId == request.CourseId, + cancellationToken); + if (application?.Status == TeacherCourseApplicationStatus.Approved) + return ConflictProblem("该教师在所选学期已具备此课程的授课资格。"); + + if (application is null) + { + application = new TeacherCourseApplication + { + AcademicTermId = request.AcademicTermId, + TeacherId = request.TeacherId, + CourseId = request.CourseId, + SubmittedAt = DateTime.UtcNow + }; + db.TeacherCourseApplications.Add(application); + } + + application.Status = TeacherCourseApplicationStatus.Approved; + application.ReviewComment = Normalize(request.Comment) ?? "学院直接分配"; + application.ReviewedAt = DateTime.UtcNow; + application.ReviewedByUserId = currentUserDataScope.Current.UserId; + await db.SaveChangesAsync(cancellationToken); + return Ok(new { application.Id }); + } + [HttpPost("{id:guid}/review")] [Authorize(Roles = ReviewRoles)] public async Task Review( @@ -262,3 +358,9 @@ public sealed record TeacherCourseApplicationRequest( public sealed record TeacherCourseReviewRequest( TeacherCourseApplicationStatus Status, [MaxLength(500)] string? ReviewComment); + +public sealed record TeacherCourseAssignmentRequest( + Guid AcademicTermId, + Guid TeacherId, + Guid CourseId, + [MaxLength(500)] string? Comment); diff --git a/src/Jiaowu.Api/Infrastructure/Graduation/StudentCourseProgressRules.cs b/src/Jiaowu.Api/Infrastructure/Graduation/StudentCourseProgressRules.cs new file mode 100644 index 0000000..88339ce --- /dev/null +++ b/src/Jiaowu.Api/Infrastructure/Graduation/StudentCourseProgressRules.cs @@ -0,0 +1,62 @@ +using Jiaowu.Api.Domain.Academic; + +namespace Jiaowu.Api.Infrastructure.Graduation; + +public enum StudentCourseProgressStatus +{ + NotStarted = 1, + InProgress = 2, + Retaking = 3, + Completed = 4, + Failed = 5, + Pending = 6 +} + +public sealed record StudentCourseAttemptSnapshot( + decimal? TotalScore, + GradeExamStatus ExamStatus); + +public sealed record StudentCourseProgressEvaluation( + StudentCourseProgressStatus Status, + bool HasPassedAttempt, + bool HasFailedAttempt); + +public static class StudentCourseProgressRules +{ + public static StudentCourseProgressEvaluation Evaluate( + IEnumerable attempts, + bool isInProgress) + { + var attemptList = attempts.ToList(); + var hasPassedAttempt = attemptList.Any(IsPassed); + var hasFailedAttempt = attemptList.Any(IsFailed); + var hasPendingAttempt = attemptList.Any(x => + !IsPassed(x) && + !IsFailed(x)); + + var status = hasPassedAttempt + ? StudentCourseProgressStatus.Completed + : isInProgress && hasFailedAttempt + ? StudentCourseProgressStatus.Retaking + : isInProgress + ? StudentCourseProgressStatus.InProgress + : hasFailedAttempt + ? StudentCourseProgressStatus.Failed + : hasPendingAttempt + ? StudentCourseProgressStatus.Pending + : StudentCourseProgressStatus.NotStarted; + + return new StudentCourseProgressEvaluation( + status, + hasPassedAttempt, + hasFailedAttempt); + } + + public static bool IsPassed(StudentCourseAttemptSnapshot attempt) => + attempt.ExamStatus == GradeExamStatus.Exempt || + attempt.TotalScore is decimal score && score >= 60; + + public static bool IsFailed(StudentCourseAttemptSnapshot attempt) => + attempt.ExamStatus == GradeExamStatus.Absent || + attempt.TotalScore is decimal score && score < 60; +} diff --git a/tests/Jiaowu.Api.Tests/StudentCourseProgressRulesTests.cs b/tests/Jiaowu.Api.Tests/StudentCourseProgressRulesTests.cs new file mode 100644 index 0000000..8d9bc28 --- /dev/null +++ b/tests/Jiaowu.Api.Tests/StudentCourseProgressRulesTests.cs @@ -0,0 +1,58 @@ +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Infrastructure.Graduation; + +namespace Jiaowu.Api.Tests; + +public sealed class StudentCourseProgressRulesTests +{ + [Fact] + public void Passed_attempt_takes_precedence_over_other_states() + { + var result = StudentCourseProgressRules.Evaluate( + [ + new StudentCourseAttemptSnapshot(52, GradeExamStatus.Normal), + new StudentCourseAttemptSnapshot(76, GradeExamStatus.Normal) + ], + true); + + Assert.Equal(StudentCourseProgressStatus.Completed, result.Status); + Assert.True(result.HasPassedAttempt); + Assert.True(result.HasFailedAttempt); + } + + [Fact] + public void Failed_course_becomes_retaking_when_currently_in_progress() + { + var result = StudentCourseProgressRules.Evaluate( + [new StudentCourseAttemptSnapshot(48, GradeExamStatus.Normal)], + true); + + Assert.Equal(StudentCourseProgressStatus.Retaking, result.Status); + Assert.False(result.HasPassedAttempt); + Assert.True(result.HasFailedAttempt); + } + + [Theory] + [InlineData(GradeExamStatus.Absent, StudentCourseProgressStatus.Failed)] + [InlineData(GradeExamStatus.Deferred, StudentCourseProgressStatus.Pending)] + public void Special_exam_states_are_presented_accurately( + GradeExamStatus examStatus, + StudentCourseProgressStatus expected) + { + var result = StudentCourseProgressRules.Evaluate( + [new StudentCourseAttemptSnapshot(null, examStatus)], + false); + + Assert.Equal(expected, result.Status); + } + + [Fact] + public void Current_course_without_result_is_in_progress() + { + var result = StudentCourseProgressRules.Evaluate([], true); + + Assert.Equal(StudentCourseProgressStatus.InProgress, result.Status); + Assert.False(result.HasPassedAttempt); + Assert.False(result.HasFailedAttempt); + } +} diff --git a/web/src/layouts/AdminLayout.vue b/web/src/layouts/AdminLayout.vue index 10b6ada..668800f 100644 --- a/web/src/layouts/AdminLayout.vue +++ b/web/src/layouts/AdminLayout.vue @@ -92,6 +92,7 @@ const navigationGroups = computed(() => [ }, ), ...whenVisible(isTeachingAdmin.value, { path: '/curriculum', label: '培养方案' }), + ...whenVisible(isStudent.value, { path: '/my-curriculum', label: '我的培养方案' }), ...whenVisible(isTeachingAdmin.value, { path: '/teaching-tasks', label: '教学任务' }), ], }, diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 6464e4c..7c5a574 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -106,6 +106,12 @@ const router = createRouter({ component: () => import('../views/CurriculumView.vue'), meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'] }, }, + { + path: 'my-curriculum', + name: 'my-curriculum', + component: () => import('../views/StudentCurriculumView.vue'), + meta: { roles: ['Student'] }, + }, { path: 'teaching-tasks', name: 'teaching-tasks', diff --git a/web/src/views/StudentCurriculumView.vue b/web/src/views/StudentCurriculumView.vue new file mode 100644 index 0000000..92f29c8 --- /dev/null +++ b/web/src/views/StudentCurriculumView.vue @@ -0,0 +1,510 @@ + + + + + diff --git a/web/src/views/TeachingPreferencesView.vue b/web/src/views/TeachingPreferencesView.vue index c19d4a1..0829e33 100644 --- a/web/src/views/TeachingPreferencesView.vue +++ b/web/src/views/TeachingPreferencesView.vue @@ -5,15 +5,22 @@ import http, { apiErrorMessage } from '../api/http' import { useAuthStore } from '../stores/auth' const auth = useAuthStore() -const isTeacher = computed(() => auth.user?.roles.includes('Teacher') ?? false) +const isReviewer = computed(() => + auth.user?.roles.some((role) => + ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role)) ?? false) +const isTeacher = computed(() => + (auth.user?.roles.includes('Teacher') ?? false) && !isReviewer.value) const loading = ref(false) const rows = ref([]) const terms = ref([]) const courses = ref([]) +const teachers = ref([]) const submitDialog = ref(false) const reviewDialog = ref(false) +const assignmentDialog = ref(false) const form = reactive>({}) const reviewForm = reactive>({}) +const assignmentForm = reactive>({}) const filters = reactive({ academicTermId: undefined as string | undefined, status: isTeacher.value ? undefined : 'Pending' as string | undefined, @@ -108,14 +115,49 @@ async function review() { } } +function openAssignment() { + Object.assign(assignmentForm, { + academicTermId: filters.academicTermId ?? + terms.value.find((item) => item.isCurrent)?.id, + teacherId: undefined, + courseId: undefined, + comment: '', + }) + assignmentDialog.value = true +} + +async function assignTeacher() { + if (!assignmentForm.academicTermId || + !assignmentForm.teacherId || + !assignmentForm.courseId) { + ElMessage.warning('请选择学期、教师和课程。') + return + } + try { + await http.post('/teacher-course-applications/assign', assignmentForm) + assignmentDialog.value = false + ElMessage.success('授课资格已直接分配') + await load() + } catch (error) { + ElMessage.error(apiErrorMessage(error)) + } +} + onMounted(async () => { const requests: Promise[] = [http.get('/base-data/terms')] if (isTeacher.value) { requests.push(http.get('/teacher-course-applications/course-options')) + } else { + requests.push(http.get('/teacher-course-applications/assignment-options')) } - const [termRes, courseRes] = await Promise.all(requests) + const [termRes, optionRes] = await Promise.all(requests) terms.value = termRes.data - courses.value = courseRes?.data ?? [] + if (isTeacher.value) { + courses.value = optionRes?.data ?? [] + } else { + courses.value = optionRes?.data?.courses ?? [] + teachers.value = optionRes?.data?.teachers ?? [] + } filters.academicTermId = terms.value.find((item) => item.isCurrent)?.id await load() }) @@ -128,17 +170,22 @@ onMounted(async () => { TEACHING ELIGIBILITY

{{ isTeacher ? '授课科目申报' : '授课资格审核' }}

在排课前申报本学期愿意承担的课程,学院审核通过后才能分配公共课教学任务。

-

审核教师的授课科目申报;通过后,该教师进入相应学期和课程的可分配教师池。

+

审核教师申报,或由学院直接指定教师;通过后进入相应学期和课程的可分配教师池。

+ +
+ + 申报课程 + + + 直接分配教师 +
- - 申报课程 -
教师选择学期与科目
-
学院审核授课资格
+
学院审核或直接分配
教务合班并分配教师
@@ -228,5 +275,82 @@ onMounted(async () => { + + + + + + + + + +
+ + + + {{ item.teacherNumber }} · {{ item.name }} + {{ item.collegeName }}{{ item.title ? ` · ${item.title}` : '' }} + + + + + + + {{ item.code }} · {{ item.name }} + {{ item.collegeName }} · {{ item.credits }} 学分 + + + +
+ + + +
+ +
+ +