diff --git a/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs b/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs index 2377af2..13a0b94 100644 --- a/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs +++ b/src/Jiaowu.Api/Controllers/CourseAdjustmentsController.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Jiaowu.Api.Contracts; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Auth; @@ -176,8 +177,13 @@ public sealed class CourseAdjustmentsController( public async Task GetMine( Guid? academicTermId, CourseAdjustmentStatus? status, - CancellationToken cancellationToken) + int page = 1, + int pageSize = 20, + CancellationToken cancellationToken = default) { + if (page < 1 || pageSize is < 1 or > 100) + return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。"); + var userId = currentUserDataScope.Current.UserId; var source = db.CourseAdjustments.AsNoTracking() .Where(x => x.ApplicantUserId == userId); @@ -187,10 +193,15 @@ public sealed class CourseAdjustmentsController( if (status.HasValue) source = source.Where(x => x.Status == status); - return Ok(await source + var total = await source.CountAsync(cancellationToken); + var items = await source .OrderByDescending(x => x.CreatedAt) + .ThenByDescending(x => x.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) .Select(AdjustmentProjection()) - .ToListAsync(cancellationToken)); + .ToListAsync(cancellationToken); + return Ok(new PagedResult(items, total, page, pageSize)); } // ═══════════════ Pending reviews ═══════════════ @@ -199,8 +210,13 @@ public sealed class CourseAdjustmentsController( [Authorize(Roles = Reviewers)] public async Task GetPendingReviews( Guid? academicTermId, - CancellationToken cancellationToken) + int page = 1, + int pageSize = 20, + CancellationToken cancellationToken = default) { + if (page < 1 || pageSize is < 1 or > 100) + return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。"); + var scope = currentUserDataScope.Current; var source = db.CourseAdjustments.AsNoTracking() .Where(x => x.Status == CourseAdjustmentStatus.Submitted); @@ -211,10 +227,15 @@ public sealed class CourseAdjustmentsController( source = source.Where(x => x.TeachingTask!.AcademicTermId == academicTermId); - return Ok(await source + var total = await source.CountAsync(cancellationToken); + var items = await source .OrderByDescending(x => x.SubmittedAt) + .ThenByDescending(x => x.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) .Select(AdjustmentProjection()) - .ToListAsync(cancellationToken)); + .ToListAsync(cancellationToken); + return Ok(new PagedResult(items, total, page, pageSize)); } // ═══════════════ Detail ═══════════════ diff --git a/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs b/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs index 90aabfb..c7de970 100644 --- a/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs +++ b/src/Jiaowu.Api/Controllers/StudentStatusChangesController.cs @@ -17,17 +17,53 @@ public sealed class StudentStatusChangesController( ICurrentUserDataScope currentUserDataScope) : ControllerBase { [HttpGet] - public async Task Get(CancellationToken token) + public async Task> Get( + int page = 1, + int pageSize = 20, + CancellationToken token = default) { + if (page < 1 || pageSize is < 1 or > 100) + return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。"); + var source = ScopedChanges().AsNoTracking(); - return Ok(await source.OrderByDescending(x => x.SubmittedAt).Select(x => new - { - x.Id, x.StudentId, x.Student!.StudentNumber, x.Student.Name, - ClassName = x.Student.AdministrativeClass!.Name, - CollegeName = x.Student.AdministrativeClass.Major!.College!.Name, - x.Type, x.OriginalStatus, x.TargetStatus, x.Reason, x.State, - x.ReviewComment, x.SubmittedAt, x.ReviewedAt, x.ApprovedAt - }).ToListAsync(token)); + var userScope = currentUserDataScope.Current; + var total = await source.CountAsync(token); + var actionableTotal = await source.CountAsync(change => + (change.State == StudentStatusChangeState.Submitted && + userScope.IsInRole(SystemRoles.Counselor)) || + (change.State == StudentStatusChangeState.CounselorApproved && + userScope.IsInRole(SystemRoles.CollegeAdmin)) || + (change.State == StudentStatusChangeState.CollegeApproved && + (userScope.IsInRole(SystemRoles.AcademicAdmin) || + userScope.IsInRole(SystemRoles.SuperAdmin))), token); + var finishedTotal = await source.CountAsync(change => + change.State == StudentStatusChangeState.Approved || + change.State == StudentStatusChangeState.Rejected || + change.State == StudentStatusChangeState.Cancelled, token); + var items = await source + .OrderByDescending(x => x.SubmittedAt) + .ThenByDescending(x => x.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(x => new StudentStatusChangeListItem( + x.Id, + x.StudentId, + x.Student!.StudentNumber, + x.Student.Name, + x.Student.AdministrativeClass!.Name, + x.Student.AdministrativeClass.Major!.College!.Name, + x.Type, + x.OriginalStatus, + x.TargetStatus, + x.Reason, + x.State, + x.ReviewComment, + x.SubmittedAt, + x.ReviewedAt, + x.ApprovedAt)) + .ToListAsync(token); + return Ok(new StudentStatusChangePage( + items, total, page, pageSize, actionableTotal, finishedTotal)); } [HttpGet("options")] @@ -199,6 +235,31 @@ public sealed class StudentStatusChangesController( }); } +public sealed record StudentStatusChangePage( + IReadOnlyCollection Items, + int Total, + int Page, + int PageSize, + int ActionableTotal, + int FinishedTotal); + +public sealed record StudentStatusChangeListItem( + Guid Id, + Guid StudentId, + string StudentNumber, + string Name, + string ClassName, + string CollegeName, + StudentStatusChangeType Type, + StudentStatus OriginalStatus, + StudentStatus TargetStatus, + string Reason, + StudentStatusChangeState State, + string? ReviewComment, + DateTime SubmittedAt, + DateTime? ReviewedAt, + DateTime? ApprovedAt); + public sealed record StudentStatusChangeRequest( StudentStatusChangeType Type, [Required, MinLength(10), MaxLength(1000)] string Reason); diff --git a/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs b/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs index 8d58dab..ce96dbf 100644 --- a/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs +++ b/src/Jiaowu.Api/Controllers/TeacherCourseApplicationsController.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Jiaowu.Api.Contracts; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Auth; @@ -46,17 +47,26 @@ public sealed class TeacherCourseApplicationsController( [Authorize(Roles = SystemRoles.Teacher)] public async Task GetMine( Guid? academicTermId, - CancellationToken cancellationToken) + int page = 1, + int pageSize = 20, + CancellationToken cancellationToken = default) { + if (page < 1 || pageSize is < 1 or > 100) + return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。"); + var teacher = await CurrentTeacherAsync(cancellationToken); if (teacher is null) return ConflictProblem("当前账号尚未关联在职教师档案。"); var source = db.TeacherCourseApplications.AsNoTracking() .Where(x => x.TeacherId == teacher.Id); if (academicTermId.HasValue) source = source.Where(x => x.AcademicTermId == academicTermId); - return Ok(await source + var total = await source.CountAsync(cancellationToken); + var items = await source .OrderByDescending(x => x.AcademicTerm!.StartDate) .ThenBy(x => x.Course!.Code) + .ThenBy(x => x.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) .Select(x => new { x.Id, @@ -72,7 +82,8 @@ public sealed class TeacherCourseApplicationsController( x.SubmittedAt, x.ReviewedAt }) - .ToListAsync(cancellationToken)); + .ToListAsync(cancellationToken); + return Ok(new PagedResult(items, total, page, pageSize)); } [HttpPost("mine")] @@ -144,15 +155,24 @@ public sealed class TeacherCourseApplicationsController( public async Task GetReviews( Guid? academicTermId, TeacherCourseApplicationStatus? status, - CancellationToken cancellationToken) + int page = 1, + int pageSize = 20, + CancellationToken cancellationToken = default) { + if (page < 1 || pageSize is < 1 or > 100) + return ValidationProblem("页码必须大于 0,且每页条数应在 1 至 100 之间。"); + var source = ScopedApplications().AsNoTracking(); if (academicTermId.HasValue) source = source.Where(x => x.AcademicTermId == academicTermId); if (status.HasValue) source = source.Where(x => x.Status == status); - return Ok(await source + var total = await source.CountAsync(cancellationToken); + var items = await source .OrderBy(x => x.Status) .ThenByDescending(x => x.SubmittedAt) + .ThenBy(x => x.Id) + .Skip((page - 1) * pageSize) + .Take(pageSize) .Select(x => new { x.Id, @@ -172,7 +192,8 @@ public sealed class TeacherCourseApplicationsController( x.SubmittedAt, x.ReviewedAt }) - .ToListAsync(cancellationToken)); + .ToListAsync(cancellationToken); + return Ok(new PagedResult(items, total, page, pageSize)); } [HttpGet("assignment-options")] diff --git a/web/src/views/CourseAdjustmentsView.vue b/web/src/views/CourseAdjustmentsView.vue index e7d2473..7202a88 100644 --- a/web/src/views/CourseAdjustmentsView.vue +++ b/web/src/views/CourseAdjustmentsView.vue @@ -27,6 +27,11 @@ const sessionTask = ref(null) const sessionLoading = ref(false) const termId = ref() const tab = ref(isManager.value ? 'reviews' : 'mine') +const minePage = ref(1) +const mineTotal = ref(0) +const reviewPage = ref(1) +const reviewTotal = ref(0) +const pageSize = 20 const form = reactive({ teachingTaskId: undefined as string | undefined, @@ -79,21 +84,30 @@ function sessionLabel(session: any) { return `第 ${session.week} 周 · ${session.date} 周${weekdayLabel(session.dayOfWeek)} · 第 ${session.startPeriod}–${end} 节${room}` } -async function load() { +async function load(resetPages = false) { + if (resetPages) { + minePage.value = 1 + reviewPage.value = 1 + } loading.value = true + reviewLoading.value = isManager.value try { if (isTeacher.value || (!isManager.value)) { - adjustments.value = (await http.get('/course-adjustments/mine', { - params: { academicTermId: termId.value }, - })).data + const response = await http.get('/course-adjustments/mine', { + params: { academicTermId: termId.value, page: minePage.value, pageSize }, + }) + adjustments.value = response.data.items + mineTotal.value = response.data.total } if (isManager.value) { - pendingReviews.value = (await http.get('/course-adjustments/pending-reviews', { - params: { academicTermId: termId.value }, - })).data + const response = await http.get('/course-adjustments/pending-reviews', { + params: { academicTermId: termId.value, page: reviewPage.value, pageSize }, + }) + pendingReviews.value = response.data.items + reviewTotal.value = response.data.total } } catch (e) { ElMessage.error(apiErrorMessage(e)) } - finally { loading.value = false } + finally { loading.value = false; reviewLoading.value = false } } function openCreate() { @@ -255,12 +269,12 @@ function showSub(type: string) { return type === 'Substitute' }
提交申请 - 刷新 + 刷新
- + +
@@ -345,6 +368,15 @@ function showSub(type: string) { return type === 'Substitute' } + diff --git a/web/src/views/StudentStatusChangesView.vue b/web/src/views/StudentStatusChangesView.vue index 23516e2..24ec349 100644 --- a/web/src/views/StudentStatusChangesView.vue +++ b/web/src/views/StudentStatusChangesView.vue @@ -23,6 +23,11 @@ const selected = ref(null) const reviewApproved = ref(true) const applyForm = reactive({ type: '', reason: '' }) const reviewForm = reactive({ comment: '' }) +const page = ref(1) +const total = ref(0) +const actionableTotal = ref(0) +const finishedTotal = ref(0) +const pageSize = 20 const typeLabels: Record = { Suspension: '休学', @@ -48,10 +53,6 @@ const steps = [ { label: '学院审核', state: 'CounselorApproved' }, { label: '校级审批', state: 'CollegeApproved' }, ] -const currentQueue = computed(() => changes.value.filter(canReview)) -const finishedCount = computed(() => - changes.value.filter((x) => ['Approved', 'Rejected', 'Cancelled'].includes(x.state)).length) - function dateText(value?: string) { if (!value) return '—' return new Intl.DateTimeFormat('zh-CN', { @@ -90,14 +91,24 @@ function stateTagType(state: ChangeState) { return 'warning' } -async function load() { +async function load(resetPage = false) { + if (resetPage) page.value = 1 loading.value = true try { - const requests = [http.get('/student-status-changes')] + const requests = [http.get('/student-status-changes', { + params: { page: page.value, pageSize }, + })] if (isStudent.value) requests.push(http.get('/student-status-changes/options')) const [changeResponse, optionResponse] = await Promise.all(requests) - changes.value = changeResponse.data + changes.value = changeResponse.data.items + total.value = changeResponse.data.total + actionableTotal.value = changeResponse.data.actionableTotal + finishedTotal.value = changeResponse.data.finishedTotal if (optionResponse) options.value = optionResponse.data + if (changes.value.length === 0 && page.value > 1) { + page.value-- + await load() + } } catch (error) { ElMessage.error(apiErrorMessage(error)) } finally { @@ -118,7 +129,7 @@ async function submitApply() { await http.post('/student-status-changes', applyForm) applyDialog.value = false ElMessage.success('申请已提交,等待辅导员审核。') - await load() + await load(true) } catch (error) { ElMessage.error(apiErrorMessage(error)) } @@ -177,7 +188,7 @@ onMounted(load) :disabled="options?.hasPending || !options?.types?.length" @click="openApply" >发起申请 - 刷新队列 + 刷新队列
@@ -194,9 +205,9 @@ onMounted(load)
-
当前待我审核{{ currentQueue.length }}
-
辖区申请总数{{ changes.length }}
-
已结束{{ finishedCount }}
+
当前待我审核{{ actionableTotal }}
+
辖区申请总数{{ total }}
+
已结束{{ finishedTotal }}

系统仅开放当前审核层级的操作,所有越级请求都会由服务端拒绝。

@@ -243,6 +254,15 @@ onMounted(load) + diff --git a/web/src/views/TeachingPreferencesView.vue b/web/src/views/TeachingPreferencesView.vue index 7cae51f..59891ba 100644 --- a/web/src/views/TeachingPreferencesView.vue +++ b/web/src/views/TeachingPreferencesView.vue @@ -19,6 +19,9 @@ const teachers = ref([]) const submitDialog = ref(false) const reviewDialog = ref(false) const assignmentDialog = ref(false) +const page = ref(1) +const total = ref(0) +const pageSize = 20 const form = reactive>({}) const reviewForm = reactive>({}) const assignmentForm = reactive>({}) @@ -39,14 +42,22 @@ const statusTypes: Record = { Withdrawn: 'info', } -async function load() { +async function load(resetPage = false) { + if (resetPage) page.value = 1 loading.value = true try { const endpoint = isTeacher.value ? '/teacher-course-applications/mine' : '/teacher-course-applications/reviews' - const { data } = await http.get(endpoint, { params: filters }) - rows.value = data + const { data } = await http.get(endpoint, { + params: { ...filters, page: page.value, pageSize }, + }) + rows.value = data.items + total.value = data.total + if (rows.value.length === 0 && page.value > 1) { + page.value-- + await load() + } } catch (error) { ElMessage.error(apiErrorMessage(error)) } finally { @@ -195,14 +206,14 @@ onMounted(async () => {
- + - + - 刷新 - 共 {{ rows.length }} 条 + 刷新 + 共 {{ total }} 条
@@ -235,6 +246,15 @@ onMounted(async () => { +