From 2d1e2cb697fe547a3a53ebb29a7764d150b8d7ef Mon Sep 17 00:00:00 2001 From: biss Date: Mon, 27 Jul 2026 17:11:51 +0800 Subject: [PATCH] =?UTF-8?q?=E2=80=9C=E6=95=99=E5=8A=A1=E6=80=BB=E8=A7=88?= =?UTF-8?q?=E2=80=9D=E6=94=B9=E6=88=90=E5=8F=AF=E5=AE=9E=E9=99=85=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E7=9A=84=E5=88=86=E7=BA=A7=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=EF=BC=9A=20=E6=A0=A1=E7=BA=A7=E7=AE=A1=E7=90=86=E5=91=98?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B=E5=85=A8=E6=A0=A1=E6=95=B0=E6=8D=AE=EF=BC=8C?= =?UTF-8?q?=E5=AD=A6=E9=99=A2=E7=AE=A1=E7=90=86=E5=91=98=E4=BB=85=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E6=9C=AC=E5=AD=A6=E9=99=A2=E6=95=B0=E6=8D=AE=E3=80=82?= =?UTF-8?q?=20=E5=BE=85=E5=8A=9E=E6=8C=89=E5=BD=93=E5=89=8D=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E9=98=B6=E6=AE=B5=E7=BB=9F=E8=AE=A1=EF=BC=8C=E6=B6=B5?= =?UTF-8?q?=E7=9B=96=E6=8E=88=E8=AF=BE=E8=B5=84=E6=A0=BC=E3=80=81=E6=88=90?= =?UTF-8?q?=E7=BB=A9=E3=80=81=E8=B0=83=E5=81=9C=E8=AF=BE=E3=80=81=E5=AD=A6?= =?UTF-8?q?=E7=B1=8D=E5=BC=82=E5=8A=A8=E3=80=81=E6=95=99=E5=AE=A4=E5=80=9F?= =?UTF-8?q?=E7=94=A8=E7=AD=89=E3=80=82=20=E5=A2=9E=E5=8A=A0=E5=AD=A6?= =?UTF-8?q?=E6=9C=9F=E8=BF=9B=E5=BA=A6=E3=80=81=E6=95=99=E5=AD=A6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=8F=91=E5=B8=83=E3=80=81=E8=AF=BE=E8=A1=A8=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E3=80=81=E6=88=90=E7=BB=A9=E5=8F=91=E5=B8=83=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E3=80=82=20=E5=BF=AB=E6=8D=B7=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E7=AE=A1=E7=90=86=E5=91=98=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B0=83=E6=95=B4=E3=80=82=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=9C=AA=E9=85=8D=E7=BD=AE=E5=AD=A6=E6=9C=9F=E3=80=81?= =?UTF-8?q?=E6=97=A0=E5=BE=85=E5=8A=9E=E3=80=81=E5=8A=A0=E8=BD=BD=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E7=AD=89=E7=8A=B6=E6=80=81=E3=80=82=20=E9=80=82?= =?UTF-8?q?=E9=85=8D=E6=A1=8C=E9=9D=A2=E5=92=8C=20390px=20=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E7=AB=AF=EF=BC=8C=E6=97=A0=E6=A8=AA=E5=90=91=E6=BA=A2?= =?UTF-8?q?=E5=87=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/DashboardController.cs | 367 ++++- .../DashboardControllerTests.cs | 261 ++++ web/src/components.d.ts | 1 + web/src/views/DashboardView.vue | 1381 +++++++++++++++-- 4 files changed, 1789 insertions(+), 221 deletions(-) create mode 100644 tests/Jiaowu.Api.Tests/DashboardControllerTests.cs diff --git a/src/Jiaowu.Api/Controllers/DashboardController.cs b/src/Jiaowu.Api/Controllers/DashboardController.cs index f14e8dc..3478503 100644 --- a/src/Jiaowu.Api/Controllers/DashboardController.cs +++ b/src/Jiaowu.Api/Controllers/DashboardController.cs @@ -1,11 +1,10 @@ -using System.Text.Json; using Jiaowu.Api.Domain.Academic; -using Jiaowu.Api.Infrastructure.Caching; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; namespace Jiaowu.Api.Controllers; @@ -14,89 +13,301 @@ namespace Jiaowu.Api.Controllers; [Route("api/dashboard")] public sealed class DashboardController( AppDbContext db, - IAppCache appCache, - IOptions jsonOptions) : ControllerBase + ICurrentUserDataScope currentUserDataScope) : ControllerBase { [HttpGet] - public async Task> Get(CancellationToken cancellationToken) + public async Task> Get( + CancellationToken cancellationToken) { - var response = await appCache.GetOrCreateAsync( - AppCacheKeys.Dashboard, - LoadAsync, - AppCacheProfile.Analytics, - [AppCacheTags.Analytics], - cancellationToken); - return response; - } + var scope = currentUserDataScope.Current; + Guid? restrictedCollegeId = scope.Scope == DataScope.All + ? null + : scope.CollegeId ?? Guid.Empty; + var collegeName = restrictedCollegeId.HasValue && + restrictedCollegeId.Value != Guid.Empty + ? await db.Colleges.AsNoTracking() + .Where(x => x.Id == restrictedCollegeId.Value) + .Select(x => x.Name) + .FirstOrDefaultAsync(cancellationToken) + : null; - private async Task LoadAsync(CancellationToken cancellationToken) - { var currentTerm = await db.AcademicTerms .AsNoTracking() .Where(x => x.IsCurrent) - .Select(x => new { x.Id, x.Name, x.StartDate, x.EndDate }) + .Select(x => new DashboardTerm( + x.Id, + x.Name, + x.StartDate, + x.EndDate)) .FirstOrDefaultAsync(cancellationToken); + var currentTermId = currentTerm?.Id; - return JsonSerializer.SerializeToElement( - new - { - CurrentTerm = currentTerm, - Counts = new - { - Campuses = await db.Campuses.CountAsync(cancellationToken), - Colleges = await db.Colleges.CountAsync(cancellationToken), - Majors = await db.Majors.CountAsync(cancellationToken), - Classes = await db.AdministrativeClasses.CountAsync(cancellationToken), - Classrooms = await db.Classrooms.CountAsync(cancellationToken), - Teachers = await db.Teachers.CountAsync(cancellationToken), - Students = await db.Students.CountAsync(cancellationToken), - Courses = await db.Courses.CountAsync(cancellationToken), - CurriculumPlans = await db.CurriculumPlans.CountAsync(cancellationToken), - TeachingTasks = await db.TeachingTasks.CountAsync(cancellationToken), - SchedulePlans = await db.SchedulePlans.CountAsync(cancellationToken), - CourseSelectionRounds = await db.CourseSelectionRounds - .CountAsync(cancellationToken), - CourseSelectionOfferings = await db.CourseSelectionOfferings - .CountAsync(cancellationToken), - CourseEnrollments = await db.CourseEnrollments - .CountAsync( - x => x.Status == CourseEnrollmentStatus.Enrolled, - cancellationToken), - GradeSheets = await db.GradeSheets.CountAsync(cancellationToken), - PublishedGradeSheets = await db.GradeSheets.CountAsync( - x => x.Status == GradeSheetStatus.Published, - cancellationToken), - GradeRecords = await db.GradeRecords.CountAsync(cancellationToken), - ExamPlans = await db.ExamPlans.CountAsync(cancellationToken), - ExamSessions = await db.ExamSessions.CountAsync(cancellationToken), - StudentStatusChanges = await db.StudentStatusChanges - .CountAsync(cancellationToken), - PendingStudentStatusChanges = await db.StudentStatusChanges.CountAsync( - x => x.State == StudentStatusChangeState.Submitted || - x.State == StudentStatusChangeState.CounselorApproved || - x.State == StudentStatusChangeState.CollegeApproved, - cancellationToken), - GraduationAuditBatches = await db.GraduationAuditBatches - .CountAsync(cancellationToken), - PublishedGraduationAuditBatches = await db.GraduationAuditBatches - .CountAsync( - x => x.Status == GraduationAuditBatchStatus.Published, - cancellationToken), - DegreeAwardBatches = await db.DegreeAwardBatches - .CountAsync(cancellationToken), - PublishedDegreeAwardBatches = await db.DegreeAwardBatches - .CountAsync( - x => x.Status == DegreeAwardBatchStatus.Published, - cancellationToken), - GraduationClearanceBatches = await db.GraduationClearanceBatches - .CountAsync(cancellationToken), - OpenGraduationClearanceBatches = await db.GraduationClearanceBatches - .CountAsync( - x => x.Status == GraduationClearanceBatchStatus.Open, - cancellationToken), - Users = await db.Users.CountAsync(cancellationToken) - } - }, - jsonOptions.Value.JsonSerializerOptions); + var students = db.Students.AsNoTracking() + .Where(x => + !restrictedCollegeId.HasValue || + x.AdministrativeClass!.Major!.CollegeId == + restrictedCollegeId.Value); + var teachers = db.Teachers.AsNoTracking() + .Where(x => + !restrictedCollegeId.HasValue || + x.CollegeId == restrictedCollegeId.Value); + var courses = db.Courses.AsNoTracking() + .Where(x => + !restrictedCollegeId.HasValue || + x.CollegeId == restrictedCollegeId.Value); + var teachingTasks = db.TeachingTasks.AsNoTracking() + .Where(x => + currentTermId.HasValue && + x.AcademicTermId == currentTermId.Value && + (!restrictedCollegeId.HasValue || + x.Course!.CollegeId == restrictedCollegeId.Value)); + var gradeSheets = db.GradeSheets.AsNoTracking() + .Where(x => + currentTermId.HasValue && + x.TeachingTask!.AcademicTermId == currentTermId.Value && + (!restrictedCollegeId.HasValue || + x.TeachingTask.Course!.CollegeId == + restrictedCollegeId.Value)); + var enrollments = db.CourseEnrollments.AsNoTracking() + .Where(x => + currentTermId.HasValue && + x.Status == CourseEnrollmentStatus.Enrolled && + x.CourseSelectionOffering!.CourseSelectionRound! + .AcademicTermId == currentTermId.Value && + (!restrictedCollegeId.HasValue || + x.CourseSelectionOffering.TeachingTask!.Course!.CollegeId == + restrictedCollegeId.Value)); + + var taskCount = await teachingTasks.CountAsync(cancellationToken); + var publishedTaskCount = await teachingTasks.CountAsync( + x => x.Status == TeachingTaskStatus.Published, + cancellationToken); + var scheduledTaskCount = currentTermId.HasValue + ? await db.ScheduleEntries.AsNoTracking() + .Where(x => + x.SchedulePlan!.AcademicTermId == currentTermId.Value && + x.SchedulePlan.Status == SchedulePlanStatus.Published && + (!restrictedCollegeId.HasValue || + x.TeachingTask!.Course!.CollegeId == + restrictedCollegeId.Value)) + .Select(x => x.TeachingTaskId) + .Distinct() + .CountAsync(cancellationToken) + : 0; + var gradeSheetCount = await gradeSheets.CountAsync(cancellationToken); + var publishedGradeSheetCount = await gradeSheets.CountAsync( + x => x.Status == GradeSheetStatus.Published, + cancellationToken); + + var counts = new DashboardCounts( + await students.CountAsync( + x => x.Status == StudentStatus.Active, + cancellationToken), + await teachers.CountAsync( + x => x.Status == TeacherStatus.Active, + cancellationToken), + await courses.CountAsync( + x => x.IsEnabled, + cancellationToken), + taskCount, + publishedTaskCount, + scheduledTaskCount, + await enrollments.CountAsync(cancellationToken), + gradeSheetCount, + publishedGradeSheetCount, + await gradeSheets.CountAsync( + x => x.Status == GradeSheetStatus.Submitted, + cancellationToken), + currentTermId.HasValue + ? await db.CourseSelectionRounds.AsNoTracking().CountAsync( + x => x.AcademicTermId == currentTermId.Value && + x.Status == CourseSelectionRoundStatus.Open, + cancellationToken) + : 0); + + var pending = await LoadPendingAsync( + scope, + restrictedCollegeId, + cancellationToken); + + return Ok(new DashboardResponse( + BuildAudience(scope, collegeName), + currentTerm, + counts, + pending, + DateTime.UtcNow)); + } + + private async Task LoadPendingAsync( + CurrentUserScope scope, + Guid? restrictedCollegeId, + CancellationToken cancellationToken) + { + var isSchoolManager = + scope.IsInRole(SystemRoles.SuperAdmin) || + scope.IsInRole(SystemRoles.AcademicAdmin); + var isCollegeManager = + !isSchoolManager && scope.IsInRole(SystemRoles.CollegeAdmin); + if (!isSchoolManager && !isCollegeManager) + return new DashboardPending(0, 0, 0, 0, 0, 0, 0); + + var teacherApplications = db.TeacherCourseApplications.AsNoTracking() + .Where(x => + x.Status == TeacherCourseApplicationStatus.Pending && + (!restrictedCollegeId.HasValue || + x.Teacher!.CollegeId == restrictedCollegeId.Value)); + var gradeSheets = db.GradeSheets.AsNoTracking() + .Where(x => + x.Status == GradeSheetStatus.Submitted && + (!restrictedCollegeId.HasValue || + x.TeachingTask!.Course!.CollegeId == + restrictedCollegeId.Value)); + var courseAdjustments = db.CourseAdjustments.AsNoTracking() + .Where(x => + x.Status == CourseAdjustmentStatus.Submitted && + (!restrictedCollegeId.HasValue || + x.TeachingTask!.Course!.CollegeId == + restrictedCollegeId.Value)); + var studentStatusChanges = db.StudentStatusChanges.AsNoTracking() + .Where(x => + x.State == (isCollegeManager + ? StudentStatusChangeState.CounselorApproved + : StudentStatusChangeState.CollegeApproved) && + (!restrictedCollegeId.HasValue || + x.Student!.AdministrativeClass!.Major!.CollegeId == + restrictedCollegeId.Value)); + var gradeModifications = db.GradeModifications.AsNoTracking() + .Where(x => + x.Status == (isCollegeManager + ? GradeModificationStatus.TeacherSubmitted + : GradeModificationStatus.CollegeApproved) && + (!restrictedCollegeId.HasValue || + x.GradeRecord!.GradeSheet!.TeachingTask!.Course!.CollegeId == + restrictedCollegeId.Value)); + + var generalApprovals = + await db.CourseExemptions.AsNoTracking().CountAsync( + x => x.Status == ApprovalStatus.Submitted && + (!restrictedCollegeId.HasValue || + x.TeachingTask!.Course!.CollegeId == + restrictedCollegeId.Value), + cancellationToken) + + await db.DeferredExams.AsNoTracking().CountAsync( + x => x.Status == ApprovalStatus.Submitted && + (!restrictedCollegeId.HasValue || + x.TeachingTask!.Course!.CollegeId == + restrictedCollegeId.Value), + cancellationToken) + + await db.CourseSubstitutions.AsNoTracking().CountAsync( + x => x.Status == ApprovalStatus.Submitted && + (!restrictedCollegeId.HasValue || + x.Student!.AdministrativeClass!.Major!.CollegeId == + restrictedCollegeId.Value), + cancellationToken) + + await db.AttendanceRecords.AsNoTracking().CountAsync( + x => x.AppealStatus == AttendanceAppealStatus.Pending && + (!restrictedCollegeId.HasValue || + x.Student!.AdministrativeClass!.Major!.CollegeId == + restrictedCollegeId.Value), + cancellationToken); + + var classroomReservations = isCollegeManager && + restrictedCollegeId.HasValue + ? await db.ClassroomReservations.AsNoTracking().CountAsync( + x => x.Status == ClassroomReservationStatus.Submitted && + x.ApplicantCollegeId == restrictedCollegeId.Value, + cancellationToken) + : 0; + + return new DashboardPending( + await teacherApplications.CountAsync(cancellationToken), + await gradeSheets.CountAsync(cancellationToken), + await courseAdjustments.CountAsync(cancellationToken), + await studentStatusChanges.CountAsync(cancellationToken), + await gradeModifications.CountAsync(cancellationToken), + classroomReservations, + generalApprovals); + } + + private static DashboardAudience BuildAudience( + CurrentUserScope scope, + string? collegeName) + { + if (scope.IsInRole(SystemRoles.SuperAdmin)) + return new DashboardAudience( + "System", + "全域教务工作台", + "全校", + "统筹基础数据、教学运行与系统治理"); + if (scope.IsInRole(SystemRoles.AcademicAdmin)) + return new DashboardAudience( + "School", + "校级教务工作台", + "全校", + "聚焦跨学院教学运行与校级审核"); + if (scope.IsInRole(SystemRoles.CollegeAdmin)) + return new DashboardAudience( + "College", + "学院教务工作台", + collegeName ?? "本学院", + "聚焦本学院教学准备、过程审核与成绩归档"); + if (scope.IsInRole(SystemRoles.Leader)) + return new DashboardAudience( + "Leadership", + "教学运行观察台", + "全校", + "查看全校教学运行与质量数据"); + if (scope.IsInRole(SystemRoles.Counselor)) + return new DashboardAudience( + "Counselor", + "班级工作台", + collegeName ?? "所辖班级", + "处理学生过程管理与学业支持"); + return new DashboardAudience( + "Teaching", + "教学工作台", + collegeName ?? "个人教学", + "查看课程运行并进入日常教学工作"); } } + +public sealed record DashboardResponse( + DashboardAudience Audience, + DashboardTerm? CurrentTerm, + DashboardCounts Counts, + DashboardPending Pending, + DateTime GeneratedAt); + +public sealed record DashboardAudience( + string Level, + string Title, + string ScopeName, + string Description); + +public sealed record DashboardTerm( + Guid Id, + string Name, + DateOnly StartDate, + DateOnly EndDate); + +public sealed record DashboardCounts( + int Students, + int Teachers, + int Courses, + int TeachingTasks, + int PublishedTeachingTasks, + int ScheduledTeachingTasks, + int CourseEnrollments, + int GradeSheets, + int PublishedGradeSheets, + int SubmittedGradeSheets, + int OpenCourseSelectionRounds); + +public sealed record DashboardPending( + int TeacherApplications, + int GradeSheets, + int CourseAdjustments, + int StudentStatusChanges, + int GradeModifications, + int ClassroomReservations, + int GeneralApprovals); diff --git a/tests/Jiaowu.Api.Tests/DashboardControllerTests.cs b/tests/Jiaowu.Api.Tests/DashboardControllerTests.cs new file mode 100644 index 0000000..206ec9b --- /dev/null +++ b/tests/Jiaowu.Api.Tests/DashboardControllerTests.cs @@ -0,0 +1,261 @@ +using Jiaowu.Api.Controllers; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +using Jiaowu.Api.Infrastructure.Persistence; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Jiaowu.Api.Tests; + +public sealed class DashboardControllerTests +{ + [Fact] + public async Task College_dashboard_is_scoped_and_only_counts_actionable_stage() + { + await using var database = await DashboardDatabase.CreateAsync(); + var controller = new DashboardController( + database.Db, + new TestDataScope( + database.FirstCollegeId, + DataScope.College, + SystemRoles.CollegeAdmin)); + + var result = await controller.Get(CancellationToken.None); + + var response = Assert.IsType( + Assert.IsType(result.Result).Value); + Assert.Equal("College", response.Audience.Level); + Assert.Equal("第一学院", response.Audience.ScopeName); + Assert.Equal(1, response.Counts.Students); + Assert.Equal(1, response.Counts.Teachers); + Assert.Equal(1, response.Counts.TeachingTasks); + Assert.Equal(1, response.Pending.TeacherApplications); + Assert.Equal(1, response.Pending.StudentStatusChanges); + } + + [Fact] + public async Task School_dashboard_uses_school_stage_and_all_colleges() + { + await using var database = await DashboardDatabase.CreateAsync(); + var controller = new DashboardController( + database.Db, + new TestDataScope( + null, + DataScope.All, + SystemRoles.AcademicAdmin)); + + var result = await controller.Get(CancellationToken.None); + + var response = Assert.IsType( + Assert.IsType(result.Result).Value); + Assert.Equal("School", response.Audience.Level); + Assert.Equal(2, response.Counts.Students); + Assert.Equal(2, response.Counts.Teachers); + Assert.Equal(2, response.Counts.TeachingTasks); + Assert.Equal(2, response.Pending.TeacherApplications); + Assert.Equal(1, response.Pending.StudentStatusChanges); + } + + private sealed class DashboardDatabase : IAsyncDisposable + { + private readonly SqliteConnection connection; + + private DashboardDatabase( + SqliteConnection connection, + AppDbContext db, + Guid firstCollegeId) + { + this.connection = connection; + Db = db; + FirstCollegeId = firstCollegeId; + } + + public AppDbContext Db { get; } + public Guid FirstCollegeId { get; } + + public static async Task CreateAsync() + { + var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + var db = new AppDbContext(options); + await db.Database.EnsureCreatedAsync(); + + var term = new AcademicTerm + { + Code = "2026-1", + Name = "2026—2027 学年第一学期", + AcademicYear = "2026-2027", + Season = TermSeason.Autumn, + StartDate = new DateOnly(2026, 9, 1), + EndDate = new DateOnly(2027, 1, 15), + IsCurrent = true + }; + var firstCollege = new College + { + Code = "C01", + Name = "第一学院" + }; + var secondCollege = new College + { + Code = "C02", + Name = "第二学院" + }; + var firstMajor = CreateMajor("M01", firstCollege); + var secondMajor = CreateMajor("M02", secondCollege); + var firstClass = CreateClass("CL01", firstMajor); + var secondClass = CreateClass("CL02", secondMajor); + var firstStudent = CreateStudent("S01", firstClass); + var secondStudent = CreateStudent("S02", secondClass); + var firstTeacher = CreateTeacher("T01", firstCollege); + var secondTeacher = CreateTeacher("T02", secondCollege); + var firstCourse = CreateCourse("COURSE01", firstCollege); + var secondCourse = CreateCourse("COURSE02", secondCollege); + var firstTask = CreateTask("TASK01", term, firstCourse); + var secondTask = CreateTask("TASK02", term, secondCourse); + + db.AddRange( + term, + firstCollege, + secondCollege, + firstMajor, + secondMajor, + firstClass, + secondClass, + firstStudent, + secondStudent, + firstTeacher, + secondTeacher, + firstCourse, + secondCourse, + firstTask, + secondTask, + new TeacherCourseApplication + { + AcademicTerm = term, + Teacher = firstTeacher, + Course = firstCourse, + Status = TeacherCourseApplicationStatus.Pending + }, + new TeacherCourseApplication + { + AcademicTerm = term, + Teacher = secondTeacher, + Course = secondCourse, + Status = TeacherCourseApplicationStatus.Pending + }, + CreateStatusChange( + firstStudent, + StudentStatusChangeState.CounselorApproved), + CreateStatusChange( + firstStudent, + StudentStatusChangeState.CollegeApproved), + CreateStatusChange( + secondStudent, + StudentStatusChangeState.Submitted)); + await db.SaveChangesAsync(); + + return new DashboardDatabase(connection, db, firstCollege.Id); + } + + public async ValueTask DisposeAsync() + { + await Db.DisposeAsync(); + await connection.DisposeAsync(); + } + + private static Major CreateMajor(string code, College college) => new() + { + Code = code, + Name = $"专业 {code}", + College = college, + DegreeType = "本科" + }; + + private static AdministrativeClass CreateClass( + string code, + Major major) => new() + { + Code = code, + Name = $"班级 {code}", + Major = major, + Grade = 2026 + }; + + private static Student CreateStudent( + string number, + AdministrativeClass administrativeClass) => new() + { + StudentNumber = number, + Name = $"学生 {number}", + AdministrativeClass = administrativeClass, + EnrollmentYear = 2026, + EnrollmentDate = new DateOnly(2026, 9, 1), + Status = StudentStatus.Active + }; + + private static Teacher CreateTeacher( + string number, + College college) => new() + { + TeacherNumber = number, + Name = $"教师 {number}", + College = college, + Status = TeacherStatus.Active + }; + + private static Course CreateCourse(string code, College college) => new() + { + Code = code, + Name = $"课程 {code}", + College = college, + Nature = CourseNature.MajorRequired, + Credits = 2, + TotalHours = 32, + LectureHours = 32, + AssessmentMethod = AssessmentMethod.Examination + }; + + private static TeachingTask CreateTask( + string number, + AcademicTerm term, + Course course) => new() + { + TaskNumber = number, + Name = $"教学班 {number}", + AcademicTerm = term, + Course = course, + Capacity = 40, + Status = TeachingTaskStatus.Published + }; + + private static StudentStatusChange CreateStatusChange( + Student student, + StudentStatusChangeState state) => new() + { + Student = student, + Type = StudentStatusChangeType.Suspension, + OriginalStatus = StudentStatus.Active, + TargetStatus = StudentStatus.Suspended, + Reason = "测试学籍异动流程", + State = state + }; + } + + private sealed class TestDataScope( + Guid? collegeId, + DataScope dataScope, + string role) : ICurrentUserDataScope + { + public CurrentUserScope Current { get; } = new( + Guid.NewGuid(), + "测试管理员", + collegeId, + dataScope, + new HashSet { role }); + } +} diff --git a/web/src/components.d.ts b/web/src/components.d.ts index e7c93d2..b1709dd 100644 --- a/web/src/components.d.ts +++ b/web/src/components.d.ts @@ -38,6 +38,7 @@ declare module 'vue' { ElProgress: typeof import('element-plus/es')['ElProgress'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElResult: typeof import('element-plus/es')['ElResult'] ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSlider: typeof import('element-plus/es')['ElSlider'] diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index 715bd30..0064dca 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -1,180 +1,1275 @@ + +