优化
This commit is contained in:
@@ -60,8 +60,16 @@ public sealed class DegreeAwardsController(
|
|||||||
|
|
||||||
[HttpGet("batches/{id:guid}")]
|
[HttpGet("batches/{id:guid}")]
|
||||||
[Authorize(Roles = Reviewers)]
|
[Authorize(Roles = Reviewers)]
|
||||||
public async Task<ActionResult> GetBatch(Guid id, CancellationToken token)
|
public async Task<ActionResult> GetBatch(
|
||||||
|
Guid id,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20,
|
||||||
|
[FromQuery] string? keyword = null,
|
||||||
|
[FromQuery] DegreeAwardConclusion? conclusion = null,
|
||||||
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
page = Math.Max(page, 1);
|
||||||
|
pageSize = Math.Clamp(pageSize, 10, 100);
|
||||||
var batch = await db.DegreeAwardBatches.AsNoTracking()
|
var batch = await db.DegreeAwardBatches.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id, token);
|
.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
if (batch is null) return NotFound();
|
if (batch is null) return NotFound();
|
||||||
@@ -71,6 +79,52 @@ public sealed class DegreeAwardsController(
|
|||||||
if (collegeId.HasValue)
|
if (collegeId.HasValue)
|
||||||
source = source.Where(x =>
|
source = source.Where(x =>
|
||||||
x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
||||||
|
|
||||||
|
var summary = await source
|
||||||
|
.GroupBy(_ => 1)
|
||||||
|
.Select(group => new
|
||||||
|
{
|
||||||
|
ResultCount = group.Count(),
|
||||||
|
GrantedCount = group.Count(x =>
|
||||||
|
x.Conclusion == DegreeAwardConclusion.Granted),
|
||||||
|
NotGrantedCount = group.Count(x =>
|
||||||
|
x.Conclusion == DegreeAwardConclusion.NotGranted),
|
||||||
|
OverrideCount = group.Count(x => x.IsOverridden)
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(token);
|
||||||
|
|
||||||
|
var normalizedKeyword = keyword?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
|
||||||
|
source = source.Where(x =>
|
||||||
|
x.Student!.StudentNumber.Contains(normalizedKeyword) ||
|
||||||
|
x.Student.Name.Contains(normalizedKeyword) ||
|
||||||
|
x.Student.AdministrativeClass!.Name.Contains(normalizedKeyword) ||
|
||||||
|
x.Student.AdministrativeClass.Major!.Name.Contains(normalizedKeyword));
|
||||||
|
if (conclusion.HasValue)
|
||||||
|
source = source.Where(x => x.Conclusion == conclusion.Value);
|
||||||
|
|
||||||
|
var total = await source.CountAsync(token);
|
||||||
|
var items = await source.OrderBy(x => x.Student!.StudentNumber)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.StudentId,
|
||||||
|
x.Student!.StudentNumber,
|
||||||
|
x.Student.Name,
|
||||||
|
ClassName = x.Student.AdministrativeClass!.Name,
|
||||||
|
MajorName = x.Student.AdministrativeClass.Major!.Name,
|
||||||
|
CollegeName = x.Student.AdministrativeClass.Major.College!.Name,
|
||||||
|
x.AverageGradePoint,
|
||||||
|
x.CalculatedConclusion,
|
||||||
|
x.Conclusion,
|
||||||
|
x.ExceptionReason,
|
||||||
|
x.IsOverridden,
|
||||||
|
x.ReviewComment,
|
||||||
|
x.ReviewedAt
|
||||||
|
}).ToListAsync(token);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
batch.Id,
|
batch.Id,
|
||||||
@@ -82,24 +136,17 @@ public sealed class DegreeAwardsController(
|
|||||||
batch.Notes,
|
batch.Notes,
|
||||||
batch.CalculatedAt,
|
batch.CalculatedAt,
|
||||||
batch.PublishedAt,
|
batch.PublishedAt,
|
||||||
Results = await source.OrderBy(x => x.Student!.StudentNumber)
|
ResultCount = summary?.ResultCount ?? 0,
|
||||||
.Select(x => new
|
GrantedCount = summary?.GrantedCount ?? 0,
|
||||||
{
|
NotGrantedCount = summary?.NotGrantedCount ?? 0,
|
||||||
x.Id,
|
OverrideCount = summary?.OverrideCount ?? 0,
|
||||||
x.StudentId,
|
Results = new
|
||||||
x.Student!.StudentNumber,
|
{
|
||||||
x.Student.Name,
|
Items = items,
|
||||||
ClassName = x.Student.AdministrativeClass!.Name,
|
Total = total,
|
||||||
MajorName = x.Student.AdministrativeClass.Major!.Name,
|
Page = page,
|
||||||
CollegeName = x.Student.AdministrativeClass.Major.College!.Name,
|
PageSize = pageSize
|
||||||
x.AverageGradePoint,
|
}
|
||||||
x.CalculatedConclusion,
|
|
||||||
x.Conclusion,
|
|
||||||
x.ExceptionReason,
|
|
||||||
x.IsOverridden,
|
|
||||||
x.ReviewComment,
|
|
||||||
x.ReviewedAt
|
|
||||||
}).ToListAsync(token)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,25 +180,30 @@ public sealed class DegreeAwardsController(
|
|||||||
return ConflictProblem("已发布批次不能重新计算。");
|
return ConflictProblem("已发布批次不能重新计算。");
|
||||||
|
|
||||||
var auditCandidates = await db.GraduationAuditResults.AsNoTracking()
|
var auditCandidates = await db.GraduationAuditResults.AsNoTracking()
|
||||||
.Include(x => x.GraduationAuditBatch)
|
|
||||||
.Include(x => x.Student)
|
|
||||||
.ThenInclude(x => x!.AdministrativeClass)
|
|
||||||
.ThenInclude(x => x!.Major)
|
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
x.GraduationAuditBatch!.GraduationYear == batch.GraduationYear &&
|
x.GraduationAuditBatch!.GraduationYear == batch.GraduationYear &&
|
||||||
x.GraduationAuditBatch.Status == GraduationAuditBatchStatus.Published &&
|
x.GraduationAuditBatch.Status == GraduationAuditBatchStatus.Published &&
|
||||||
x.Conclusion == GraduationAuditConclusion.Eligible)
|
x.Conclusion == GraduationAuditConclusion.Eligible)
|
||||||
.OrderByDescending(x => x.GraduationAuditBatch!.PublishedAt)
|
.OrderByDescending(x => x.GraduationAuditBatch!.PublishedAt)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.StudentId,
|
||||||
|
StudentStatus = x.Student!.Status
|
||||||
|
})
|
||||||
.ToListAsync(token);
|
.ToListAsync(token);
|
||||||
var audits = auditCandidates
|
var audits = auditCandidates
|
||||||
.GroupBy(x => x.StudentId)
|
.GroupBy(x => x.StudentId)
|
||||||
.Select(x => x.First())
|
.Select(x => x.First())
|
||||||
.ToList();
|
.ToList();
|
||||||
var studentIds = audits.Select(x => x.StudentId).ToArray();
|
|
||||||
var gradePoints = await db.GradeRecords.AsNoTracking()
|
var gradePoints = await db.GradeRecords.AsNoTracking()
|
||||||
.Where(x => x.GradeSheet!.Status == GradeSheetStatus.Published &&
|
.Where(x => x.GradeSheet!.Status == GradeSheetStatus.Published &&
|
||||||
x.GradePoint.HasValue)
|
x.GradePoint.HasValue)
|
||||||
.WhereIn(studentIds, x => x.StudentId)
|
.Where(x => db.GraduationAuditResults.Any(audit =>
|
||||||
|
audit.StudentId == x.StudentId &&
|
||||||
|
audit.GraduationAuditBatch!.GraduationYear == batch.GraduationYear &&
|
||||||
|
audit.GraduationAuditBatch.Status == GraduationAuditBatchStatus.Published &&
|
||||||
|
audit.Conclusion == GraduationAuditConclusion.Eligible))
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.StudentId,
|
x.StudentId,
|
||||||
@@ -159,14 +211,17 @@ public sealed class DegreeAwardsController(
|
|||||||
x.GradeSheet!.TeachingTask!.Course!.Credits
|
x.GradeSheet!.TeachingTask!.Course!.Credits
|
||||||
}).ToListAsync(token);
|
}).ToListAsync(token);
|
||||||
|
|
||||||
var oldResults = await db.DegreeAwardResults
|
var gradePointsByStudent = gradePoints
|
||||||
.Where(x => x.DegreeAwardBatchId == id).ToListAsync(token);
|
.GroupBy(x => x.StudentId)
|
||||||
db.DegreeAwardResults.RemoveRange(oldResults);
|
.ToDictionary(group => group.Key, group => group.ToList());
|
||||||
await db.SaveChangesAsync(token);
|
|
||||||
|
await db.DegreeAwardResults
|
||||||
|
.Where(x => x.DegreeAwardBatchId == id)
|
||||||
|
.ExecuteDeleteAsync(token);
|
||||||
|
|
||||||
foreach (var audit in audits)
|
foreach (var audit in audits)
|
||||||
{
|
{
|
||||||
var grades = gradePoints.Where(x => x.StudentId == audit.StudentId).ToList();
|
var grades = gradePointsByStudent.GetValueOrDefault(audit.StudentId) ?? [];
|
||||||
var credits = grades.Sum(x => x.Credits);
|
var credits = grades.Sum(x => x.Credits);
|
||||||
var average = credits == 0
|
var average = credits == 0
|
||||||
? 0
|
? 0
|
||||||
@@ -175,8 +230,8 @@ public sealed class DegreeAwardsController(
|
|||||||
2,
|
2,
|
||||||
MidpointRounding.AwayFromZero);
|
MidpointRounding.AwayFromZero);
|
||||||
var conclusion = DegreeAwardRules.Evaluate(
|
var conclusion = DegreeAwardRules.Evaluate(
|
||||||
true, audit.Student!.Status, average, batch.MinimumGradePoint);
|
true, audit.StudentStatus, average, batch.MinimumGradePoint);
|
||||||
var reason = audit.Student.Status != StudentStatus.Graduated
|
var reason = audit.StudentStatus != StudentStatus.Graduated
|
||||||
? "学籍状态尚未转为毕业"
|
? "学籍状态尚未转为毕业"
|
||||||
: average < batch.MinimumGradePoint
|
: average < batch.MinimumGradePoint
|
||||||
? $"平均绩点 {average:0.00},低于批次要求 {batch.MinimumGradePoint:0.00}"
|
? $"平均绩点 {average:0.00},低于批次要求 {batch.MinimumGradePoint:0.00}"
|
||||||
|
|||||||
@@ -61,8 +61,16 @@ public sealed class GraduationAuditsController(
|
|||||||
|
|
||||||
[HttpGet("batches/{id:guid}")]
|
[HttpGet("batches/{id:guid}")]
|
||||||
[Authorize(Roles = Reviewers)]
|
[Authorize(Roles = Reviewers)]
|
||||||
public async Task<ActionResult> GetBatch(Guid id, CancellationToken token)
|
public async Task<ActionResult> GetBatch(
|
||||||
|
Guid id,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20,
|
||||||
|
[FromQuery] string? keyword = null,
|
||||||
|
[FromQuery] GraduationAuditConclusion? conclusion = null,
|
||||||
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
page = Math.Max(page, 1);
|
||||||
|
pageSize = Math.Clamp(pageSize, 10, 100);
|
||||||
var batch = await db.GraduationAuditBatches.AsNoTracking()
|
var batch = await db.GraduationAuditBatches.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(x => x.Id == id, token);
|
.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
if (batch is null) return NotFound();
|
if (batch is null) return NotFound();
|
||||||
@@ -73,6 +81,59 @@ public sealed class GraduationAuditsController(
|
|||||||
if (collegeId.HasValue)
|
if (collegeId.HasValue)
|
||||||
results = results.Where(x =>
|
results = results.Where(x =>
|
||||||
x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);
|
||||||
|
|
||||||
|
var summary = await results
|
||||||
|
.GroupBy(_ => 1)
|
||||||
|
.Select(group => new
|
||||||
|
{
|
||||||
|
ResultCount = group.Count(),
|
||||||
|
EligibleCount = group.Count(x =>
|
||||||
|
x.Conclusion == GraduationAuditConclusion.Eligible),
|
||||||
|
IneligibleCount = group.Count(x =>
|
||||||
|
x.Conclusion == GraduationAuditConclusion.Ineligible),
|
||||||
|
OverrideCount = group.Count(x => x.IsOverridden)
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(token);
|
||||||
|
|
||||||
|
var normalizedKeyword = keyword?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
|
||||||
|
results = results.Where(x =>
|
||||||
|
x.Student!.StudentNumber.Contains(normalizedKeyword) ||
|
||||||
|
x.Student.Name.Contains(normalizedKeyword) ||
|
||||||
|
x.Student.AdministrativeClass!.Name.Contains(normalizedKeyword) ||
|
||||||
|
x.Student.AdministrativeClass.Major!.Name.Contains(normalizedKeyword));
|
||||||
|
if (conclusion.HasValue)
|
||||||
|
results = results.Where(x => x.Conclusion == conclusion.Value);
|
||||||
|
|
||||||
|
var total = await results.CountAsync(token);
|
||||||
|
var items = await results
|
||||||
|
.OrderBy(x => x.Student!.StudentNumber)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
x.Id,
|
||||||
|
x.StudentId,
|
||||||
|
x.Student!.StudentNumber,
|
||||||
|
x.Student.Name,
|
||||||
|
ClassName = x.Student.AdministrativeClass!.Name,
|
||||||
|
MajorName = x.Student.AdministrativeClass.Major!.Name,
|
||||||
|
CollegeName = x.Student.AdministrativeClass.Major.College!.Name,
|
||||||
|
x.StudentStatusSnapshot,
|
||||||
|
PlanName = x.CurriculumPlan != null ? x.CurriculumPlan.Name : null,
|
||||||
|
x.RequiredCredits,
|
||||||
|
x.EarnedCredits,
|
||||||
|
x.RequiredCourseCount,
|
||||||
|
x.PassedRequiredCourseCount,
|
||||||
|
x.FailedCourseCount,
|
||||||
|
x.MissingCourseNames,
|
||||||
|
x.CalculatedConclusion,
|
||||||
|
x.Conclusion,
|
||||||
|
x.IsOverridden,
|
||||||
|
x.ReviewComment,
|
||||||
|
x.ReviewedAt
|
||||||
|
}).ToListAsync(token);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
batch.Id,
|
batch.Id,
|
||||||
@@ -83,31 +144,17 @@ public sealed class GraduationAuditsController(
|
|||||||
batch.Notes,
|
batch.Notes,
|
||||||
batch.CalculatedAt,
|
batch.CalculatedAt,
|
||||||
batch.PublishedAt,
|
batch.PublishedAt,
|
||||||
Results = await results
|
ResultCount = summary?.ResultCount ?? 0,
|
||||||
.OrderBy(x => x.Student!.StudentNumber)
|
EligibleCount = summary?.EligibleCount ?? 0,
|
||||||
.Select(x => new
|
IneligibleCount = summary?.IneligibleCount ?? 0,
|
||||||
{
|
OverrideCount = summary?.OverrideCount ?? 0,
|
||||||
x.Id,
|
Results = new
|
||||||
x.StudentId,
|
{
|
||||||
x.Student!.StudentNumber,
|
Items = items,
|
||||||
x.Student.Name,
|
Total = total,
|
||||||
ClassName = x.Student.AdministrativeClass!.Name,
|
Page = page,
|
||||||
MajorName = x.Student.AdministrativeClass.Major!.Name,
|
PageSize = pageSize
|
||||||
CollegeName = x.Student.AdministrativeClass.Major.College!.Name,
|
}
|
||||||
x.StudentStatusSnapshot,
|
|
||||||
PlanName = x.CurriculumPlan != null ? x.CurriculumPlan.Name : null,
|
|
||||||
x.RequiredCredits,
|
|
||||||
x.EarnedCredits,
|
|
||||||
x.RequiredCourseCount,
|
|
||||||
x.PassedRequiredCourseCount,
|
|
||||||
x.FailedCourseCount,
|
|
||||||
x.MissingCourseNames,
|
|
||||||
x.CalculatedConclusion,
|
|
||||||
x.Conclusion,
|
|
||||||
x.IsOverridden,
|
|
||||||
x.ReviewComment,
|
|
||||||
x.ReviewedAt
|
|
||||||
}).ToListAsync(token)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,10 +203,11 @@ public sealed class GraduationAuditsController(
|
|||||||
.Where(x => x.EffectiveGrade == batch.EnrollmentYear &&
|
.Where(x => x.EffectiveGrade == batch.EnrollmentYear &&
|
||||||
x.Status == CurriculumPlanStatus.Published)
|
x.Status == CurriculumPlanStatus.Published)
|
||||||
.ToListAsync(token);
|
.ToListAsync(token);
|
||||||
var studentIds = students.Select(x => x.Id).ToArray();
|
|
||||||
var grades = await db.GradeRecords.AsNoTracking()
|
var grades = await db.GradeRecords.AsNoTracking()
|
||||||
.Where(x => x.GradeSheet!.Status == GradeSheetStatus.Published)
|
.Where(x => x.GradeSheet!.Status == GradeSheetStatus.Published)
|
||||||
.WhereIn(studentIds, x => x.StudentId)
|
.Where(x => x.Student!.EnrollmentYear == batch.EnrollmentYear &&
|
||||||
|
(x.Student.Status == StudentStatus.Active ||
|
||||||
|
x.Student.Status == StudentStatus.Suspended))
|
||||||
.Select(x => new GradeSnapshot(
|
.Select(x => new GradeSnapshot(
|
||||||
x.StudentId,
|
x.StudentId,
|
||||||
x.GradeSheet!.TeachingTask!.CourseId,
|
x.GradeSheet!.TeachingTask!.CourseId,
|
||||||
@@ -169,16 +217,23 @@ public sealed class GraduationAuditsController(
|
|||||||
x.ExamStatus))
|
x.ExamStatus))
|
||||||
.ToListAsync(token);
|
.ToListAsync(token);
|
||||||
|
|
||||||
var oldResults = await db.GraduationAuditResults
|
var plansByMajor = plans
|
||||||
.Where(x => x.GraduationAuditBatchId == id).ToListAsync(token);
|
.GroupBy(x => x.MajorId)
|
||||||
db.GraduationAuditResults.RemoveRange(oldResults);
|
.ToDictionary(group => group.Key, group => group.First());
|
||||||
await db.SaveChangesAsync(token);
|
var gradesByStudent = grades
|
||||||
|
.GroupBy(x => x.StudentId)
|
||||||
|
.ToDictionary(group => group.Key, group => group.ToList());
|
||||||
|
|
||||||
|
await db.GraduationAuditResults
|
||||||
|
.Where(x => x.GraduationAuditBatchId == id)
|
||||||
|
.ExecuteDeleteAsync(token);
|
||||||
|
|
||||||
foreach (var student in students)
|
foreach (var student in students)
|
||||||
{
|
{
|
||||||
var plan = plans.FirstOrDefault(x =>
|
plansByMajor.TryGetValue(
|
||||||
x.MajorId == student.AdministrativeClass!.MajorId);
|
student.AdministrativeClass!.MajorId,
|
||||||
var studentGrades = grades.Where(x => x.StudentId == student.Id).ToList();
|
out var plan);
|
||||||
|
var studentGrades = gradesByStudent.GetValueOrDefault(student.Id) ?? [];
|
||||||
var passedCourseIds = studentGrades
|
var passedCourseIds = studentGrades
|
||||||
.Where(IsPassed)
|
.Where(IsPassed)
|
||||||
.Select(x => x.CourseId)
|
.Select(x => x.CourseId)
|
||||||
|
|||||||
@@ -34,31 +34,46 @@ public sealed class GraduationClearanceController(
|
|||||||
x.Id, x.Name, x.GraduationYear, x.Status, x.Notes,
|
x.Id, x.Name, x.GraduationYear, x.Status, x.Notes,
|
||||||
x.ClosedAt, x.CreatedAt, ItemCount = x.Items.Count
|
x.ClosedAt, x.CreatedAt, ItemCount = x.Items.Count
|
||||||
}).ToListAsync(token);
|
}).ToListAsync(token);
|
||||||
var records = await ScopedRecords().AsNoTracking()
|
var statistics = await ScopedRecords().AsNoTracking()
|
||||||
.Select(x => new
|
.GroupBy(x => x.GraduationClearanceItem!.GraduationClearanceBatchId)
|
||||||
|
.Select(group => new
|
||||||
{
|
{
|
||||||
BatchId = x.GraduationClearanceItem!.GraduationClearanceBatchId,
|
BatchId = group.Key,
|
||||||
x.StudentId, x.Status
|
StudentCount = group.Select(x => x.StudentId).Distinct().Count(),
|
||||||
}).ToListAsync(token);
|
RecordCount = group.Count(),
|
||||||
|
CompletedCount = group.Count(x =>
|
||||||
|
x.Status != GraduationClearanceRecordStatus.Pending)
|
||||||
|
}).ToDictionaryAsync(x => x.BatchId, token);
|
||||||
return Ok(batches.Select(batch =>
|
return Ok(batches.Select(batch =>
|
||||||
{
|
{
|
||||||
var scoped = records.Where(x => x.BatchId == batch.Id).ToList();
|
statistics.TryGetValue(batch.Id, out var summary);
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
batch.Id, batch.Name, batch.GraduationYear, batch.Status,
|
batch.Id, batch.Name, batch.GraduationYear, batch.Status,
|
||||||
batch.Notes, batch.ClosedAt, batch.CreatedAt, batch.ItemCount,
|
batch.Notes, batch.ClosedAt, batch.CreatedAt, batch.ItemCount,
|
||||||
StudentCount = scoped.Select(x => x.StudentId).Distinct().Count(),
|
StudentCount = summary?.StudentCount ?? 0,
|
||||||
RecordCount = scoped.Count,
|
RecordCount = summary?.RecordCount ?? 0,
|
||||||
CompletedCount = scoped.Count(x =>
|
CompletedCount = summary?.CompletedCount ?? 0
|
||||||
x.Status != GraduationClearanceRecordStatus.Pending)
|
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("batches/{id:guid}")]
|
[HttpGet("batches/{id:guid}")]
|
||||||
[Authorize(Roles = Workers)]
|
[Authorize(Roles = Workers)]
|
||||||
public async Task<ActionResult> GetBatch(Guid id, CancellationToken token)
|
public async Task<ActionResult> GetBatch(
|
||||||
|
Guid id,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20,
|
||||||
|
[FromQuery] string? keyword = null,
|
||||||
|
[FromQuery] string? progress = null,
|
||||||
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
page = Math.Max(page, 1);
|
||||||
|
pageSize = Math.Clamp(pageSize, 10, 100);
|
||||||
|
var normalizedProgress = progress?.Trim();
|
||||||
|
if (normalizedProgress is not null and not "Pending" and not "Completed")
|
||||||
|
return ValidationProblem("离校办理进度筛选值无效。");
|
||||||
|
|
||||||
var batch = await db.GraduationClearanceBatches.AsNoTracking()
|
var batch = await db.GraduationClearanceBatches.AsNoTracking()
|
||||||
.Where(x => x.Id == id)
|
.Where(x => x.Id == id)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
@@ -72,8 +87,56 @@ public sealed class GraduationClearanceController(
|
|||||||
})
|
})
|
||||||
}).FirstOrDefaultAsync(token);
|
}).FirstOrDefaultAsync(token);
|
||||||
if (batch is null) return NotFound();
|
if (batch is null) return NotFound();
|
||||||
var records = await ScopedRecords().AsNoTracking()
|
|
||||||
.Where(x => x.GraduationClearanceItem!.GraduationClearanceBatchId == id)
|
var records = ScopedRecords().AsNoTracking()
|
||||||
|
.Where(x => x.GraduationClearanceItem!.GraduationClearanceBatchId == id);
|
||||||
|
var summary = await records
|
||||||
|
.GroupBy(_ => 1)
|
||||||
|
.Select(group => new
|
||||||
|
{
|
||||||
|
StudentCount = group.Select(x => x.StudentId).Distinct().Count(),
|
||||||
|
RecordCount = group.Count(),
|
||||||
|
CompletedCount = group.Count(x =>
|
||||||
|
x.Status != GraduationClearanceRecordStatus.Pending)
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(token);
|
||||||
|
|
||||||
|
var students = db.Students.AsNoTracking()
|
||||||
|
.Where(student => records.Any(record => record.StudentId == student.Id));
|
||||||
|
var normalizedKeyword = keyword?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
|
||||||
|
students = students.Where(student =>
|
||||||
|
student.StudentNumber.Contains(normalizedKeyword) ||
|
||||||
|
student.Name.Contains(normalizedKeyword) ||
|
||||||
|
student.AdministrativeClass!.Name.Contains(normalizedKeyword) ||
|
||||||
|
student.AdministrativeClass.Major!.Name.Contains(normalizedKeyword));
|
||||||
|
if (normalizedProgress == "Pending")
|
||||||
|
students = students.Where(student => records.Any(record =>
|
||||||
|
record.StudentId == student.Id &&
|
||||||
|
record.GraduationClearanceItem!.IsRequired &&
|
||||||
|
record.Status == GraduationClearanceRecordStatus.Pending));
|
||||||
|
else if (normalizedProgress == "Completed")
|
||||||
|
students = students.Where(student => !records.Any(record =>
|
||||||
|
record.StudentId == student.Id &&
|
||||||
|
record.GraduationClearanceItem!.IsRequired &&
|
||||||
|
record.Status == GraduationClearanceRecordStatus.Pending));
|
||||||
|
|
||||||
|
var total = await students.CountAsync(token);
|
||||||
|
var studentPage = await students
|
||||||
|
.OrderBy(student => student.StudentNumber)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(student => new
|
||||||
|
{
|
||||||
|
student.Id,
|
||||||
|
student.StudentNumber,
|
||||||
|
student.Name,
|
||||||
|
ClassName = student.AdministrativeClass!.Name,
|
||||||
|
MajorName = student.AdministrativeClass.Major!.Name
|
||||||
|
}).ToListAsync(token);
|
||||||
|
var studentIds = studentPage.Select(student => student.Id).ToArray();
|
||||||
|
var pageRecords = await records
|
||||||
|
.WhereIn(studentIds, x => x.StudentId)
|
||||||
.OrderBy(x => x.Student!.StudentNumber)
|
.OrderBy(x => x.Student!.StudentNumber)
|
||||||
.ThenBy(x => x.GraduationClearanceItem!.SortOrder)
|
.ThenBy(x => x.GraduationClearanceItem!.SortOrder)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
@@ -89,11 +152,32 @@ public sealed class GraduationClearanceController(
|
|||||||
x.GraduationClearanceItem.IsRequired,
|
x.GraduationClearanceItem.IsRequired,
|
||||||
x.Status, x.Notes, x.CompletedAt
|
x.Status, x.Notes, x.CompletedAt
|
||||||
}).ToListAsync(token);
|
}).ToListAsync(token);
|
||||||
|
var recordsByStudent = pageRecords
|
||||||
|
.GroupBy(record => record.StudentId)
|
||||||
|
.ToDictionary(group => group.Key, group => group.ToList());
|
||||||
|
var studentItems = studentPage.Select(student => new
|
||||||
|
{
|
||||||
|
StudentId = student.Id,
|
||||||
|
student.StudentNumber,
|
||||||
|
student.Name,
|
||||||
|
student.ClassName,
|
||||||
|
student.MajorName,
|
||||||
|
Records = recordsByStudent.GetValueOrDefault(student.Id) ?? []
|
||||||
|
}).ToList();
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
batch.Id, batch.Name, batch.GraduationYear, batch.Status,
|
batch.Id, batch.Name, batch.GraduationYear, batch.Status,
|
||||||
batch.Notes, batch.ClosedAt, batch.CreatedAt, batch.Items,
|
batch.Notes, batch.ClosedAt, batch.CreatedAt, batch.Items,
|
||||||
Records = records
|
StudentCount = summary?.StudentCount ?? 0,
|
||||||
|
RecordCount = summary?.RecordCount ?? 0,
|
||||||
|
CompletedCount = summary?.CompletedCount ?? 0,
|
||||||
|
Students = new
|
||||||
|
{
|
||||||
|
Items = studentItems,
|
||||||
|
Total = total,
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,16 +234,15 @@ public sealed class GraduationClearanceController(
|
|||||||
if (batch.Status != GraduationClearanceBatchStatus.Open)
|
if (batch.Status != GraduationClearanceBatchStatus.Open)
|
||||||
return ConflictProblem("已关闭批次不能重新生成办理记录。");
|
return ConflictProblem("已关闭批次不能重新生成办理记录。");
|
||||||
|
|
||||||
var eligibleAudits = await db.GraduationAuditResults.AsNoTracking()
|
var studentIds = await db.GraduationAuditResults.AsNoTracking()
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
x.GraduationAuditBatch!.GraduationYear == batch.GraduationYear &&
|
x.GraduationAuditBatch!.GraduationYear == batch.GraduationYear &&
|
||||||
x.GraduationAuditBatch.Status == GraduationAuditBatchStatus.Published &&
|
x.GraduationAuditBatch.Status == GraduationAuditBatchStatus.Published &&
|
||||||
x.Conclusion == GraduationAuditConclusion.Eligible &&
|
x.Conclusion == GraduationAuditConclusion.Eligible &&
|
||||||
x.Student!.Status == StudentStatus.Graduated)
|
x.Student!.Status == StudentStatus.Graduated)
|
||||||
.OrderByDescending(x => x.GraduationAuditBatch!.PublishedAt)
|
.Select(x => x.StudentId)
|
||||||
.Select(x => new { x.StudentId })
|
.Distinct()
|
||||||
.ToListAsync(token);
|
.ToListAsync(token);
|
||||||
var studentIds = eligibleAudits.Select(x => x.StudentId).Distinct().ToArray();
|
|
||||||
var existing = await db.GraduationClearanceRecords.AsNoTracking()
|
var existing = await db.GraduationClearanceRecords.AsNoTracking()
|
||||||
.Where(x => x.GraduationClearanceItem!.GraduationClearanceBatchId == id)
|
.Where(x => x.GraduationClearanceItem!.GraduationClearanceBatchId == id)
|
||||||
.Select(x => new { x.GraduationClearanceItemId, x.StudentId })
|
.Select(x => new { x.GraduationClearanceItemId, x.StudentId })
|
||||||
@@ -180,7 +263,7 @@ public sealed class GraduationClearanceController(
|
|||||||
created++;
|
created++;
|
||||||
}
|
}
|
||||||
await db.SaveChangesAsync(token);
|
await db.SaveChangesAsync(token);
|
||||||
return Ok(new { StudentCount = studentIds.Length, CreatedRecordCount = created });
|
return Ok(new { StudentCount = studentIds.Count, CreatedRecordCount = created });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("records/{id:guid}")]
|
[HttpPut("records/{id:guid}")]
|
||||||
@@ -221,15 +304,17 @@ public sealed class GraduationClearanceController(
|
|||||||
public async Task<ActionResult> Close(Guid id, CancellationToken token)
|
public async Task<ActionResult> Close(Guid id, CancellationToken token)
|
||||||
{
|
{
|
||||||
var batch = await db.GraduationClearanceBatches
|
var batch = await db.GraduationClearanceBatches
|
||||||
.Include(x => x.Items)
|
|
||||||
.ThenInclude(x => x.Records)
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == id, token);
|
.FirstOrDefaultAsync(x => x.Id == id, token);
|
||||||
if (batch is null) return NotFound();
|
if (batch is null) return NotFound();
|
||||||
if (batch.Status != GraduationClearanceBatchStatus.Open)
|
if (batch.Status != GraduationClearanceBatchStatus.Open)
|
||||||
return ConflictProblem("该批次已经关闭。");
|
return ConflictProblem("该批次已经关闭。");
|
||||||
var records = batch.Items.SelectMany(item => item.Records.Select(record =>
|
var hasIncompleteRequiredItem = await db.GraduationClearanceRecords
|
||||||
(item.IsRequired, record.Status)));
|
.AnyAsync(record =>
|
||||||
if (!GraduationClearanceRules.CanClose(records))
|
record.GraduationClearanceItem!.GraduationClearanceBatchId == id &&
|
||||||
|
record.GraduationClearanceItem.IsRequired &&
|
||||||
|
record.Status == GraduationClearanceRecordStatus.Pending,
|
||||||
|
token);
|
||||||
|
if (hasIncompleteRequiredItem)
|
||||||
return ConflictProblem("仍有必办离校事项尚未完成。");
|
return ConflictProblem("仍有必办离校事项尚未完成。");
|
||||||
batch.Status = GraduationClearanceBatchStatus.Closed;
|
batch.Status = GraduationClearanceBatchStatus.Closed;
|
||||||
batch.ClosedAt = DateTime.UtcNow;
|
batch.ClosedAt = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
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 GraduationAuditsControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetBatch_ReturnsPagedResultsAndWholeBatchSummary()
|
||||||
|
{
|
||||||
|
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 college = new College { Code = "CS", Name = "计算机学院" };
|
||||||
|
var major = new Major
|
||||||
|
{
|
||||||
|
Code = "SE",
|
||||||
|
Name = "软件工程",
|
||||||
|
CollegeId = college.Id,
|
||||||
|
DegreeType = "工学"
|
||||||
|
};
|
||||||
|
var administrativeClass = new AdministrativeClass
|
||||||
|
{
|
||||||
|
Code = "SE2601",
|
||||||
|
Name = "软件工程2601班",
|
||||||
|
MajorId = major.Id,
|
||||||
|
Grade = 2026
|
||||||
|
};
|
||||||
|
var batch = new GraduationAuditBatch
|
||||||
|
{
|
||||||
|
Name = "2030届毕业资格审核",
|
||||||
|
GraduationYear = 2030,
|
||||||
|
EnrollmentYear = 2026
|
||||||
|
};
|
||||||
|
db.AddRange(college, major, administrativeClass, batch);
|
||||||
|
|
||||||
|
for (var index = 1; index <= 25; index++)
|
||||||
|
{
|
||||||
|
var student = new Student
|
||||||
|
{
|
||||||
|
StudentNumber = $"2026{index:0000}",
|
||||||
|
Name = index == 17 ? "分页检索学生" : $"学生{index:00}",
|
||||||
|
AdministrativeClassId = administrativeClass.Id,
|
||||||
|
EnrollmentYear = 2026,
|
||||||
|
EnrollmentDate = new DateOnly(2026, 9, 1)
|
||||||
|
};
|
||||||
|
db.Students.Add(student);
|
||||||
|
db.GraduationAuditResults.Add(new GraduationAuditResult
|
||||||
|
{
|
||||||
|
GraduationAuditBatchId = batch.Id,
|
||||||
|
StudentId = student.Id,
|
||||||
|
StudentStatusSnapshot = StudentStatus.Active,
|
||||||
|
RequiredCredits = 160,
|
||||||
|
EarnedCredits = 150,
|
||||||
|
RequiredCourseCount = 40,
|
||||||
|
PassedRequiredCourseCount = 38,
|
||||||
|
FailedCourseCount = 1,
|
||||||
|
MissingCourseNames = "毕业设计",
|
||||||
|
CalculatedConclusion = index % 2 == 0
|
||||||
|
? GraduationAuditConclusion.Eligible
|
||||||
|
: GraduationAuditConclusion.Ineligible,
|
||||||
|
Conclusion = index % 2 == 0
|
||||||
|
? GraduationAuditConclusion.Eligible
|
||||||
|
: GraduationAuditConclusion.Ineligible,
|
||||||
|
IsOverridden = index == 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var controller = new GraduationAuditsController(db, new AllDataScope());
|
||||||
|
var pageResult = Assert.IsType<OkObjectResult>(await controller.GetBatch(
|
||||||
|
batch.Id,
|
||||||
|
page: 2,
|
||||||
|
pageSize: 10));
|
||||||
|
using var pageJson = ToJson(pageResult.Value);
|
||||||
|
|
||||||
|
Assert.Equal(25, pageJson.RootElement.GetProperty("resultCount").GetInt32());
|
||||||
|
Assert.Equal(12, pageJson.RootElement.GetProperty("eligibleCount").GetInt32());
|
||||||
|
Assert.Equal(13, pageJson.RootElement.GetProperty("ineligibleCount").GetInt32());
|
||||||
|
Assert.Equal(1, pageJson.RootElement.GetProperty("overrideCount").GetInt32());
|
||||||
|
var pagedResults = pageJson.RootElement.GetProperty("results");
|
||||||
|
Assert.Equal(25, pagedResults.GetProperty("total").GetInt32());
|
||||||
|
Assert.Equal(2, pagedResults.GetProperty("page").GetInt32());
|
||||||
|
Assert.Equal(10, pagedResults.GetProperty("items").GetArrayLength());
|
||||||
|
|
||||||
|
var filteredResult = Assert.IsType<OkObjectResult>(await controller.GetBatch(
|
||||||
|
batch.Id,
|
||||||
|
keyword: "分页检索",
|
||||||
|
conclusion: GraduationAuditConclusion.Ineligible));
|
||||||
|
using var filteredJson = ToJson(filteredResult.Value);
|
||||||
|
var filteredResults = filteredJson.RootElement.GetProperty("results");
|
||||||
|
Assert.Equal(1, filteredResults.GetProperty("total").GetInt32());
|
||||||
|
Assert.Equal(
|
||||||
|
"分页检索学生",
|
||||||
|
filteredResults.GetProperty("items")[0].GetProperty("name").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonDocument ToJson(object? value) => JsonDocument.Parse(
|
||||||
|
JsonSerializer.Serialize(value, new JsonSerializerOptions(JsonSerializerDefaults.Web)));
|
||||||
|
|
||||||
|
private sealed class AllDataScope : ICurrentUserDataScope
|
||||||
|
{
|
||||||
|
public CurrentUserScope Current { get; } = new(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
"校级教务",
|
||||||
|
null,
|
||||||
|
DataScope.All,
|
||||||
|
new HashSet<string>([SystemRoles.AcademicAdmin]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
import { Check, Close, Medal, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
|
import { Check, Close, Medal, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
|
||||||
import http, { apiErrorMessage } from '../api/http'
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
@@ -17,6 +17,10 @@ const reviewDialog = ref(false)
|
|||||||
const reviewTarget = ref<any | null>(null)
|
const reviewTarget = ref<any | null>(null)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const conclusionFilter = ref('')
|
const conclusionFilter = ref('')
|
||||||
|
const resultPage = ref(1)
|
||||||
|
const resultPageSize = 20
|
||||||
|
const resultRequestId = ref(0)
|
||||||
|
let filterTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
const batchForm = reactive({
|
const batchForm = reactive({
|
||||||
name: '2030届学士学位授予审核',
|
name: '2030届学士学位授予审核',
|
||||||
graduationYear: 2030,
|
graduationYear: 2030,
|
||||||
@@ -29,12 +33,8 @@ const conclusionLabels: Record<string, string> = {
|
|||||||
Granted: '建议授予',
|
Granted: '建议授予',
|
||||||
NotGranted: '暂不授予',
|
NotGranted: '暂不授予',
|
||||||
}
|
}
|
||||||
const filteredResults = computed(() => {
|
const resultItems = computed(() => selected.value?.results?.items ?? [])
|
||||||
const q = keyword.value.trim().toLowerCase()
|
const resultTotal = computed(() => selected.value?.results?.total ?? 0)
|
||||||
return (selected.value?.results ?? []).filter((x: any) =>
|
|
||||||
(!conclusionFilter.value || x.conclusion === conclusionFilter.value) &&
|
|
||||||
(!q || `${x.studentNumber}${x.name}${x.className}${x.majorName}`.toLowerCase().includes(q)))
|
|
||||||
})
|
|
||||||
function dateText(value?: string) {
|
function dateText(value?: string) {
|
||||||
if (!value) return '—'
|
if (!value) return '—'
|
||||||
return new Intl.DateTimeFormat('zh-CN', {
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
@@ -60,7 +60,28 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function selectBatch(id: string) {
|
async function selectBatch(id: string) {
|
||||||
selected.value = (await http.get(`/degree-awards/batches/${id}`)).data
|
resultPage.value = 1
|
||||||
|
await loadBatch(id)
|
||||||
|
}
|
||||||
|
async function loadBatch(id = selected.value?.id) {
|
||||||
|
if (!id) return
|
||||||
|
const requestId = ++resultRequestId.value
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await http.get(`/degree-awards/batches/${id}`, {
|
||||||
|
params: {
|
||||||
|
page: resultPage.value,
|
||||||
|
pageSize: resultPageSize,
|
||||||
|
keyword: keyword.value.trim() || undefined,
|
||||||
|
conclusion: conclusionFilter.value || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (requestId === resultRequestId.value) selected.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId === resultRequestId.value) ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
if (requestId === resultRequestId.value) loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function createBatch() {
|
async function createBatch() {
|
||||||
try {
|
try {
|
||||||
@@ -99,7 +120,7 @@ async function saveReview() {
|
|||||||
await http.put(`/degree-awards/results/${reviewTarget.value.id}`, reviewForm)
|
await http.put(`/degree-awards/results/${reviewTarget.value.id}`, reviewForm)
|
||||||
reviewDialog.value = false
|
reviewDialog.value = false
|
||||||
ElMessage.success('学位复核结论已保存。')
|
ElMessage.success('学位复核结论已保存。')
|
||||||
await selectBatch(selected.value.id)
|
await loadBatch()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(apiErrorMessage(error))
|
ElMessage.error(apiErrorMessage(error))
|
||||||
}
|
}
|
||||||
@@ -117,6 +138,17 @@ async function publish() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch([keyword, conclusionFilter], () => {
|
||||||
|
resultPage.value = 1
|
||||||
|
if (filterTimer) clearTimeout(filterTimer)
|
||||||
|
filterTimer = setTimeout(() => loadBatch(), 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (filterTimer) clearTimeout(filterTimer)
|
||||||
|
resultRequestId.value++
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -160,25 +192,33 @@ onMounted(load)
|
|||||||
<el-tag v-else type="success" effect="plain">授予结果已锁定</el-tag>
|
<el-tag v-else type="success" effect="plain">授予结果已锁定</el-tag>
|
||||||
</header>
|
</header>
|
||||||
<div class="degree-summary">
|
<div class="degree-summary">
|
||||||
<div><span>参审人数</span><b>{{ selected.results.length }}</b></div>
|
<div><span>参审人数</span><b>{{ selected.resultCount }}</b></div>
|
||||||
<div><span>建议授予</span><b>{{ selected.results.filter((x: any) => x.conclusion === 'Granted').length }}</b></div>
|
<div><span>建议授予</span><b>{{ selected.grantedCount }}</b></div>
|
||||||
<div><span>暂不授予</span><b>{{ selected.results.filter((x: any) => x.conclusion === 'NotGranted').length }}</b></div>
|
<div><span>暂不授予</span><b>{{ selected.notGrantedCount }}</b></div>
|
||||||
<div><span>人工调整</span><b>{{ selected.results.filter((x: any) => x.isOverridden).length }}</b></div>
|
<div><span>人工调整</span><b>{{ selected.overrideCount }}</b></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="graduation-filter">
|
<div class="graduation-filter">
|
||||||
<el-input v-model="keyword" clearable placeholder="搜索学号、姓名、专业或班级" />
|
<el-input v-model="keyword" clearable placeholder="搜索学号、姓名、专业或班级" />
|
||||||
<el-select v-model="conclusionFilter" clearable placeholder="全部结论"><el-option label="建议授予" value="Granted" /><el-option label="暂不授予" value="NotGranted" /></el-select>
|
<el-select v-model="conclusionFilter" clearable placeholder="全部结论"><el-option label="建议授予" value="Granted" /><el-option label="暂不授予" value="NotGranted" /></el-select>
|
||||||
<span>显示 {{ filteredResults.length }} 条</span>
|
<span>共 {{ resultTotal }} 条</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="degree-result-list">
|
<div class="degree-result-list">
|
||||||
<article v-for="row in filteredResults" :key="row.id">
|
<article v-for="row in resultItems" :key="row.id">
|
||||||
<div><span>{{ row.studentNumber }} · {{ row.className }}</span><h4>{{ row.name }}</h4><p>{{ row.majorName }}</p></div>
|
<div><span>{{ row.studentNumber }} · {{ row.className }}</span><h4>{{ row.name }}</h4><p>{{ row.majorName }}</p></div>
|
||||||
<div><span>加权平均绩点</span><b>{{ row.averageGradePoint }}</b><small>批次要求 ≥ {{ selected.minimumGradePoint }}</small></div>
|
<div><span>加权平均绩点</span><b>{{ row.averageGradePoint }}</b><small>批次要求 ≥ {{ selected.minimumGradePoint }}</small></div>
|
||||||
<div class="degree-rule-note"><span>规则说明</span><b>{{ row.exceptionReason || '毕业资格与绩点均符合' }}</b></div>
|
<div class="degree-rule-note"><span>规则说明</span><b>{{ row.exceptionReason || '毕业资格与绩点均符合' }}</b></div>
|
||||||
<div class="degree-result-chip" :class="{ granted: row.conclusion === 'Granted' }"><el-icon><Check v-if="row.conclusion === 'Granted'" /><Close v-else /></el-icon><b>{{ conclusionLabels[row.conclusion] }}</b><small>{{ row.isOverridden ? '人工复核' : '规则计算' }}</small></div>
|
<div class="degree-result-chip" :class="{ granted: row.conclusion === 'Granted' }"><el-icon><Check v-if="row.conclusion === 'Granted'" /><Close v-else /></el-icon><b>{{ conclusionLabels[row.conclusion] }}</b><small>{{ row.isOverridden ? '人工复核' : '规则计算' }}</small></div>
|
||||||
<el-button v-if="selected.status === 'Draft'" link type="primary" @click="openReview(row)">人工复核</el-button>
|
<el-button v-if="selected.status === 'Draft'" link type="primary" @click="openReview(row)">人工复核</el-button>
|
||||||
</article>
|
</article>
|
||||||
<el-empty v-if="!filteredResults.length" description="没有符合筛选条件的授予记录" />
|
<el-empty v-if="!resultItems.length" description="没有符合筛选条件的授予记录" />
|
||||||
|
<el-pagination
|
||||||
|
v-if="resultTotal > resultPageSize"
|
||||||
|
v-model:current-page="resultPage"
|
||||||
|
:page-size="resultPageSize"
|
||||||
|
:total="resultTotal"
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
@current-change="loadBatch()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<el-empty v-else v-loading="loading" description="尚未建立学位授予批次" />
|
<el-empty v-else v-loading="loading" description="尚未建立学位授予批次" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
import { Check, DocumentChecked, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
|
import { Check, DocumentChecked, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
|
||||||
import http, { apiErrorMessage } from '../api/http'
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
@@ -17,6 +17,10 @@ const decisionDialog = ref(false)
|
|||||||
const decisionTarget = ref<any | null>(null)
|
const decisionTarget = ref<any | null>(null)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const conclusionFilter = ref('')
|
const conclusionFilter = ref('')
|
||||||
|
const resultPage = ref(1)
|
||||||
|
const resultPageSize = 20
|
||||||
|
const resultRequestId = ref(0)
|
||||||
|
let filterTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
const batchForm = reactive({
|
const batchForm = reactive({
|
||||||
name: '2030届本科生毕业资格审核',
|
name: '2030届本科生毕业资格审核',
|
||||||
graduationYear: 2030,
|
graduationYear: 2030,
|
||||||
@@ -26,12 +30,8 @@ const batchForm = reactive({
|
|||||||
const decisionForm = reactive({ conclusion: 'Ineligible', comment: '' })
|
const decisionForm = reactive({ conclusion: 'Ineligible', comment: '' })
|
||||||
const statusLabels: Record<string, string> = { Draft: '审核中', Published: '已发布' }
|
const statusLabels: Record<string, string> = { Draft: '审核中', Published: '已发布' }
|
||||||
const conclusionLabels: Record<string, string> = { Eligible: '符合毕业条件', Ineligible: '暂不符合' }
|
const conclusionLabels: Record<string, string> = { Eligible: '符合毕业条件', Ineligible: '暂不符合' }
|
||||||
const filteredResults = computed(() => {
|
const resultItems = computed(() => selected.value?.results?.items ?? [])
|
||||||
const q = keyword.value.trim().toLowerCase()
|
const resultTotal = computed(() => selected.value?.results?.total ?? 0)
|
||||||
return (selected.value?.results ?? []).filter((x: any) =>
|
|
||||||
(!conclusionFilter.value || x.conclusion === conclusionFilter.value) &&
|
|
||||||
(!q || `${x.studentNumber}${x.name}${x.className}${x.majorName}`.toLowerCase().includes(q)))
|
|
||||||
})
|
|
||||||
|
|
||||||
function percent(earned: number, required: number) {
|
function percent(earned: number, required: number) {
|
||||||
if (!required) return 0
|
if (!required) return 0
|
||||||
@@ -62,7 +62,28 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function selectBatch(id: string) {
|
async function selectBatch(id: string) {
|
||||||
selected.value = (await http.get(`/graduation-audits/batches/${id}`)).data
|
resultPage.value = 1
|
||||||
|
await loadBatch(id)
|
||||||
|
}
|
||||||
|
async function loadBatch(id = selected.value?.id) {
|
||||||
|
if (!id) return
|
||||||
|
const requestId = ++resultRequestId.value
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await http.get(`/graduation-audits/batches/${id}`, {
|
||||||
|
params: {
|
||||||
|
page: resultPage.value,
|
||||||
|
pageSize: resultPageSize,
|
||||||
|
keyword: keyword.value.trim() || undefined,
|
||||||
|
conclusion: conclusionFilter.value || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (requestId === resultRequestId.value) selected.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId === resultRequestId.value) ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
if (requestId === resultRequestId.value) loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function createBatch() {
|
async function createBatch() {
|
||||||
if (!batchForm.name.trim()) return
|
if (!batchForm.name.trim()) return
|
||||||
@@ -103,7 +124,7 @@ async function saveDecision() {
|
|||||||
await http.put(`/graduation-audits/results/${decisionTarget.value.id}`, decisionForm)
|
await http.put(`/graduation-audits/results/${decisionTarget.value.id}`, decisionForm)
|
||||||
decisionDialog.value = false
|
decisionDialog.value = false
|
||||||
ElMessage.success('人工复核结论已保存。')
|
ElMessage.success('人工复核结论已保存。')
|
||||||
await selectBatch(selected.value.id)
|
await loadBatch()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(apiErrorMessage(error))
|
ElMessage.error(apiErrorMessage(error))
|
||||||
}
|
}
|
||||||
@@ -123,6 +144,17 @@ async function publish() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch([keyword, conclusionFilter], () => {
|
||||||
|
resultPage.value = 1
|
||||||
|
if (filterTimer) clearTimeout(filterTimer)
|
||||||
|
filterTimer = setTimeout(() => loadBatch(), 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (filterTimer) clearTimeout(filterTimer)
|
||||||
|
resultRequestId.value++
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -187,10 +219,10 @@ onMounted(load)
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="graduation-summary">
|
<div class="graduation-summary">
|
||||||
<div><span>参审学生</span><b>{{ selected.results.length }}</b><small>人</small></div>
|
<div><span>参审学生</span><b>{{ selected.resultCount }}</b><small>人</small></div>
|
||||||
<div><span>符合条件</span><b>{{ selected.results.filter((x: any) => x.conclusion === 'Eligible').length }}</b><small>人</small></div>
|
<div><span>符合条件</span><b>{{ selected.eligibleCount }}</b><small>人</small></div>
|
||||||
<div><span>暂不符合</span><b>{{ selected.results.filter((x: any) => x.conclusion === 'Ineligible').length }}</b><small>人</small></div>
|
<div><span>暂不符合</span><b>{{ selected.ineligibleCount }}</b><small>人</small></div>
|
||||||
<div><span>人工调整</span><b>{{ selected.results.filter((x: any) => x.isOverridden).length }}</b><small>人</small></div>
|
<div><span>人工调整</span><b>{{ selected.overrideCount }}</b><small>人</small></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="graduation-filter">
|
<div class="graduation-filter">
|
||||||
@@ -199,18 +231,26 @@ onMounted(load)
|
|||||||
<el-option label="符合毕业条件" value="Eligible" />
|
<el-option label="符合毕业条件" value="Eligible" />
|
||||||
<el-option label="暂不符合" value="Ineligible" />
|
<el-option label="暂不符合" value="Ineligible" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<span>显示 {{ filteredResults.length }} 条</span>
|
<span>共 {{ resultTotal }} 条</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="graduation-result-list">
|
<div class="graduation-result-list">
|
||||||
<article v-for="row in filteredResults" :key="row.id">
|
<article v-for="row in resultItems" :key="row.id">
|
||||||
<div class="graduation-student"><span>{{ row.studentNumber }} · {{ row.className }}</span><h4>{{ row.name }}</h4><p>{{ row.majorName }} · {{ row.planName || '未匹配培养方案' }}</p></div>
|
<div class="graduation-student"><span>{{ row.studentNumber }} · {{ row.className }}</span><h4>{{ row.name }}</h4><p>{{ row.majorName }} · {{ row.planName || '未匹配培养方案' }}</p></div>
|
||||||
<div class="credit-progress"><span>学分完成度</span><b>{{ row.earnedCredits }} / {{ row.requiredCredits }}</b><el-progress :percentage="percent(row.earnedCredits, row.requiredCredits)" :show-text="false" /></div>
|
<div class="credit-progress"><span>学分完成度</span><b>{{ row.earnedCredits }} / {{ row.requiredCredits }}</b><el-progress :percentage="percent(row.earnedCredits, row.requiredCredits)" :show-text="false" /></div>
|
||||||
<div class="course-clearance"><span>必修要求</span><b>{{ row.passedRequiredCourseCount }} / {{ row.requiredCourseCount }}</b><small v-if="row.missingCourseNames">缺:{{ row.missingCourseNames }}</small><small v-else>指定必修与课程组均已完成</small></div>
|
<div class="course-clearance"><span>必修要求</span><b>{{ row.passedRequiredCourseCount }} / {{ row.requiredCourseCount }}</b><small v-if="row.missingCourseNames">缺:{{ row.missingCourseNames }}</small><small v-else>指定必修与课程组均已完成</small></div>
|
||||||
<div class="graduation-conclusion" :class="{ eligible: row.conclusion === 'Eligible' }"><el-icon><Check /></el-icon><span>{{ conclusionLabels[row.conclusion] }}</span><small v-if="row.isOverridden">人工复核</small><small v-else>规则计算</small></div>
|
<div class="graduation-conclusion" :class="{ eligible: row.conclusion === 'Eligible' }"><el-icon><Check /></el-icon><span>{{ conclusionLabels[row.conclusion] }}</span><small v-if="row.isOverridden">人工复核</small><small v-else>规则计算</small></div>
|
||||||
<el-button v-if="selected.status === 'Draft'" link type="primary" @click="openDecision(row)">人工复核</el-button>
|
<el-button v-if="selected.status === 'Draft'" link type="primary" @click="openDecision(row)">人工复核</el-button>
|
||||||
</article>
|
</article>
|
||||||
<el-empty v-if="!filteredResults.length" description="没有符合筛选条件的审核结果" />
|
<el-empty v-if="!resultItems.length" description="没有符合筛选条件的审核结果" />
|
||||||
|
<el-pagination
|
||||||
|
v-if="resultTotal > resultPageSize"
|
||||||
|
v-model:current-page="resultPage"
|
||||||
|
:page-size="resultPageSize"
|
||||||
|
:total="resultTotal"
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
@current-change="loadBatch()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<el-empty v-else v-loading="loading" description="尚未建立毕业资格审核批次" />
|
<el-empty v-else v-loading="loading" description="尚未建立毕业资格审核批次" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
import { Check, CircleCheck, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
|
import { Check, CircleCheck, Plus, Refresh, Stamp } from '@element-plus/icons-vue'
|
||||||
import http, { apiErrorMessage } from '../api/http'
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
@@ -16,6 +16,12 @@ const createDialog = ref(false)
|
|||||||
const recordDialog = ref(false)
|
const recordDialog = ref(false)
|
||||||
const recordTarget = ref<any | null>(null)
|
const recordTarget = ref<any | null>(null)
|
||||||
const recordForm = reactive({ status: 'Completed', notes: '' })
|
const recordForm = reactive({ status: 'Completed', notes: '' })
|
||||||
|
const keyword = ref('')
|
||||||
|
const progressFilter = ref('')
|
||||||
|
const resultPage = ref(1)
|
||||||
|
const resultPageSize = 20
|
||||||
|
const resultRequestId = ref(0)
|
||||||
|
let filterTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
const batchForm = reactive({
|
const batchForm = reactive({
|
||||||
name: '2030届毕业生离校手续',
|
name: '2030届毕业生离校手续',
|
||||||
graduationYear: 2030,
|
graduationYear: 2030,
|
||||||
@@ -34,23 +40,8 @@ const statusLabels: Record<string, string> = {
|
|||||||
const roleLabels: Record<string, string> = {
|
const roleLabels: Record<string, string> = {
|
||||||
AcademicAdmin: '校级办理', CollegeAdmin: '学院办理', Counselor: '辅导员办理',
|
AcademicAdmin: '校级办理', CollegeAdmin: '学院办理', Counselor: '辅导员办理',
|
||||||
}
|
}
|
||||||
const studentLedgers = computed(() => {
|
const studentItems = computed(() => selected.value?.students?.items ?? [])
|
||||||
const map = new Map<string, any>()
|
const studentTotal = computed(() => selected.value?.students?.total ?? 0)
|
||||||
for (const record of selected.value?.records ?? []) {
|
|
||||||
if (!map.has(record.studentId)) {
|
|
||||||
map.set(record.studentId, {
|
|
||||||
studentId: record.studentId,
|
|
||||||
studentNumber: record.studentNumber,
|
|
||||||
name: record.name,
|
|
||||||
className: record.className,
|
|
||||||
majorName: record.majorName,
|
|
||||||
records: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
map.get(record.studentId).records.push(record)
|
|
||||||
}
|
|
||||||
return [...map.values()]
|
|
||||||
})
|
|
||||||
function dateText(value?: string) {
|
function dateText(value?: string) {
|
||||||
if (!value) return '—'
|
if (!value) return '—'
|
||||||
return new Intl.DateTimeFormat('zh-CN', {
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
@@ -88,7 +79,28 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function selectBatch(id: string) {
|
async function selectBatch(id: string) {
|
||||||
selected.value = (await http.get(`/graduation-clearance/batches/${id}`)).data
|
resultPage.value = 1
|
||||||
|
await loadBatch(id)
|
||||||
|
}
|
||||||
|
async function loadBatch(id = selected.value?.id) {
|
||||||
|
if (!id) return
|
||||||
|
const requestId = ++resultRequestId.value
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await http.get(`/graduation-clearance/batches/${id}`, {
|
||||||
|
params: {
|
||||||
|
page: resultPage.value,
|
||||||
|
pageSize: resultPageSize,
|
||||||
|
keyword: keyword.value.trim() || undefined,
|
||||||
|
progress: progressFilter.value || undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (requestId === resultRequestId.value) selected.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (requestId === resultRequestId.value) ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
if (requestId === resultRequestId.value) loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function addItem() {
|
function addItem() {
|
||||||
batchForm.items.push({
|
batchForm.items.push({
|
||||||
@@ -135,7 +147,7 @@ async function saveRecord() {
|
|||||||
await http.put(`/graduation-clearance/records/${recordTarget.value.id}`, recordForm)
|
await http.put(`/graduation-clearance/records/${recordTarget.value.id}`, recordForm)
|
||||||
recordDialog.value = false
|
recordDialog.value = false
|
||||||
ElMessage.success('离校事项办理状态已更新。')
|
ElMessage.success('离校事项办理状态已更新。')
|
||||||
await selectBatch(selected.value.id)
|
await loadBatch()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(apiErrorMessage(error))
|
ElMessage.error(apiErrorMessage(error))
|
||||||
}
|
}
|
||||||
@@ -153,6 +165,17 @@ async function closeBatch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch([keyword, progressFilter], () => {
|
||||||
|
resultPage.value = 1
|
||||||
|
if (filterTimer) clearTimeout(filterTimer)
|
||||||
|
filterTimer = setTimeout(() => loadBatch(), 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (filterTimer) clearTimeout(filterTimer)
|
||||||
|
resultRequestId.value++
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -195,15 +218,23 @@ onMounted(load)
|
|||||||
|
|
||||||
<section v-if="selected" class="clearance-workbench" v-loading="loading">
|
<section v-if="selected" class="clearance-workbench" v-loading="loading">
|
||||||
<header>
|
<header>
|
||||||
<div><span>CLEARANCE LEDGER</span><h3>{{ selected.name }}</h3><p>{{ selected.items.length }} 项离校手续 · {{ studentLedgers.length }} 名毕业生</p></div>
|
<div><span>CLEARANCE LEDGER</span><h3>{{ selected.name }}</h3><p>{{ selected.items.length }} 项离校手续 · {{ selected.studentCount }} 名毕业生</p></div>
|
||||||
<div v-if="selected.status === 'Open' && isManager"><el-button :icon="Refresh" @click="generate">补齐名单</el-button><el-button type="primary" :icon="Stamp" @click="closeBatch">关闭批次</el-button></div>
|
<div v-if="selected.status === 'Open' && isManager"><el-button :icon="Refresh" @click="generate">补齐名单</el-button><el-button type="primary" :icon="Stamp" @click="closeBatch">关闭批次</el-button></div>
|
||||||
<el-tag v-else type="success" effect="plain">批次已锁定</el-tag>
|
<el-tag v-else type="success" effect="plain">批次已锁定</el-tag>
|
||||||
</header>
|
</header>
|
||||||
<div class="clearance-item-legend">
|
<div class="clearance-item-legend">
|
||||||
<div v-for="item in selected.items" :key="item.id"><span>{{ item.code }}</span><b>{{ item.name }}</b><small>{{ item.responsibleUnit }} · {{ roleLabels[item.responsibleRole] }}</small></div>
|
<div v-for="item in selected.items" :key="item.id"><span>{{ item.code }}</span><b>{{ item.name }}</b><small>{{ item.responsibleUnit }} · {{ roleLabels[item.responsibleRole] }}</small></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="graduation-filter">
|
||||||
|
<el-input v-model="keyword" clearable placeholder="搜索学号、姓名、专业或班级" />
|
||||||
|
<el-select v-model="progressFilter" clearable placeholder="全部进度">
|
||||||
|
<el-option label="有必办待办" value="Pending" />
|
||||||
|
<el-option label="必办已完成" value="Completed" />
|
||||||
|
</el-select>
|
||||||
|
<span>共 {{ studentTotal }} 名学生</span>
|
||||||
|
</div>
|
||||||
<div class="clearance-ledgers">
|
<div class="clearance-ledgers">
|
||||||
<article v-for="student in studentLedgers" :key="student.studentId">
|
<article v-for="student in studentItems" :key="student.studentId">
|
||||||
<header><div><span>{{ student.studentNumber }} · {{ student.className }}</span><h4>{{ student.name }}</h4><p>{{ student.majorName }}</p></div><b>{{ completedCount(student.records) }}/{{ student.records.length }}</b></header>
|
<header><div><span>{{ student.studentNumber }} · {{ student.className }}</span><h4>{{ student.name }}</h4><p>{{ student.majorName }}</p></div><b>{{ completedCount(student.records) }}/{{ student.records.length }}</b></header>
|
||||||
<div class="clearance-record-grid">
|
<div class="clearance-record-grid">
|
||||||
<div v-for="record in student.records" :key="record.id" :class="{ done: record.status !== 'Pending' }">
|
<div v-for="record in student.records" :key="record.id" :class="{ done: record.status !== 'Pending' }">
|
||||||
@@ -218,7 +249,15 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<el-empty v-if="!studentLedgers.length" description="尚未生成毕业生离校办理记录" />
|
<el-empty v-if="!studentItems.length" description="没有符合筛选条件的离校办理记录" />
|
||||||
|
<el-pagination
|
||||||
|
v-if="studentTotal > resultPageSize"
|
||||||
|
v-model:current-page="resultPage"
|
||||||
|
:page-size="resultPageSize"
|
||||||
|
:total="studentTotal"
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
@current-change="loadBatch()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<el-empty v-else v-loading="loading" description="尚未建立毕业离校批次" />
|
<el-empty v-else v-loading="loading" description="尚未建立毕业离校批次" />
|
||||||
|
|||||||
Reference in New Issue
Block a user