From fc326ac16ed2a192bae28dee8a85eeef6b189fd3 Mon Sep 17 00:00:00 2001 From: biss Date: Mon, 10 Aug 2026 16:57:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E9=AA=8C=E6=88=90=E7=BB=A9=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=94=B9=E4=B8=BA=E7=B4=A7=E5=87=91=E7=9A=84=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E5=88=86=E7=BB=84=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/ExperimentGradesController.cs | 48 ++++++- .../ExperimentGradesControllerTests.cs | 9 +- web/src/views/ExperimentGradesView.vue | 117 ++++++++++-------- 3 files changed, 112 insertions(+), 62 deletions(-) diff --git a/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs b/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs index e9f5379..8c4c29e 100644 --- a/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs +++ b/src/Jiaowu.Api/Controllers/ExperimentGradesController.cs @@ -76,13 +76,25 @@ public sealed class ExperimentGradesController( item.Teacher!.TeacherNumber.Contains(keyword) || item.Teacher.Name.Contains(keyword))); - var total = await source.CountAsync(cancellationToken); - var items = await source - .OrderByDescending(x => x.TeachingTask!.AcademicTerm!.StartDate) - .ThenBy(x => x.TeachingTask!.TaskNumber) - .ThenBy(x => x.Code) + var total = await source.Select(x => x.TeachingTaskId) + .Distinct() + .CountAsync(cancellationToken); + var taskIds = await source + .GroupBy(x => new + { + x.TeachingTaskId, + StartDate = x.TeachingTask!.AcademicTerm!.StartDate, + x.TeachingTask.TaskNumber + }) + .OrderByDescending(x => x.Key.StartDate) + .ThenBy(x => x.Key.TaskNumber) .Skip((page - 1) * pageSize) .Take(pageSize) + .Select(x => x.Key.TeachingTaskId) + .ToListAsync(cancellationToken); + var projects = await source + .Where(x => taskIds.Contains(x.TeachingTaskId)) + .OrderBy(x => x.Code) .Select(x => new { x.Id, @@ -124,6 +136,32 @@ public sealed class ExperimentGradesController( } }) .ToListAsync(cancellationToken); + var order = taskIds.Select((id, index) => new { id, index }) + .ToDictionary(x => x.id, x => x.index); + var items = projects.GroupBy(x => x.TeachingTaskId) + .OrderBy(group => order[group.Key]) + .Select(group => + { + var first = group.First(); + return new + { + TeachingTaskId = group.Key, + first.TaskNumber, + first.TaskName, + first.AcademicTermId, + first.TermName, + first.CourseCode, + first.CourseName, + first.CollegeName, + first.TeacherNames, + ProjectCount = group.Count(), + SheetCount = group.Count(project => project.Sheet != null), + ScoredCount = group.Sum(project => project.Sheet?.ScoredCount ?? 0), + StudentCount = group.Sum(project => project.Sheet?.StudentCount ?? 0), + Projects = group.ToList() + }; + }) + .ToList(); return Ok(new { Items = items, diff --git a/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs b/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs index f979d43..a5f420d 100644 --- a/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs +++ b/tests/Jiaowu.Api.Tests/ExperimentGradesControllerTests.cs @@ -198,10 +198,11 @@ public sealed class ExperimentGradesControllerTests 1, 10, CancellationToken.None)); - Assert.Equal(12, Property(page.Value, "Total")); - Assert.Equal(10, Property( - page.Value, - "Items").Cast().Count()); + Assert.Equal(1, Property(page.Value, "Total")); + var course = Assert.Single(Property( + page.Value, "Items").Cast()); + Assert.Equal(12, Property( + course, "Projects").Cast().Count()); var filtered = Assert.IsType(await controller.GetManagement( null, diff --git a/web/src/views/ExperimentGradesView.vue b/web/src/views/ExperimentGradesView.vue index 7b50193..7b2cfd2 100644 --- a/web/src/views/ExperimentGradesView.vue +++ b/web/src/views/ExperimentGradesView.vue @@ -36,8 +36,8 @@ const collegeId = ref('') const projectKeyword = ref('') const projectPage = ref(1) const projectPageSize = ref(20) -const projectTotal = ref(0) -const projects = ref([]) +const courseTotal = ref(0) +const courses = ref([]) const studentResults = ref([]) const detailDrawer = ref(false) const detail = ref(null) @@ -200,8 +200,8 @@ async function load() { pageSize: projectPageSize.value, }, })).data - projects.value = data.items - projectTotal.value = data.total + courses.value = data.items + courseTotal.value = data.total projectPage.value = data.page projectPageSize.value = data.pageSize } @@ -564,45 +564,52 @@ onMounted(async () => { 查询 -
-
-
- {{ project.code }} - {{ modeLabels[project.arrangementMode] }} -
-
-

{{ project.name }}

-

{{ project.courseCode }} · {{ project.courseName }}

- {{ project.taskNumber }} · {{ project.termName }} · {{ project.teacherNames.join('、') || '教师待定' }} -
- - + + + + + + + + + + + +
- +
{ .grade-toolbar .el-select { width: 220px; } .grade-toolbar .el-input { width: min(320px, 100%); } .grade-toolbar > span { margin-left: auto; color: var(--muted); font-size: 12px; } -.grade-project-list { display: grid; gap: 12px; min-height: 180px; } -.grade-project-card { min-width: 0; padding: 16px 18px; display: grid; grid-template-columns: 130px minmax(220px, 1.4fr) minmax(220px, 1fr) auto; align-items: center; gap: 18px; border: 1px solid #d9e4e8; border-left: 5px solid var(--grade-blue); background: #fff; box-shadow: 0 6px 18px rgb(30 68 86 / 5%); } -.project-mark { display: grid; gap: 7px; } -.project-mark span { overflow: hidden; color: #607b88; font: 700 10px/1.3 Consolas, monospace; text-overflow: ellipsis; } -.project-mark b { color: var(--grade-blue); font-size: 12px; } -.project-summary { min-width: 0; } -.project-summary h3 { margin: 0 0 5px; color: var(--grade-ink); font-size: 17px; } -.project-summary p { margin: 0 0 4px; color: #365f73; font-size: 13px; } -.project-summary small { display: block; overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; } -.sheet-progress, .sheet-empty { display: grid; gap: 5px; color: #607681; font-size: 12px; } -.sheet-progress b { color: var(--grade-teal); } -.sheet-empty b { color: #5d7480; } +.grade-course-list { display: grid; gap: 10px; min-height: 180px; } +.grade-course-card { overflow: hidden; border: 1px solid #d9e4e8; border-left: 4px solid var(--grade-blue); background: #fff; box-shadow: 0 4px 13px rgb(30 68 86 / 4%); } +.grade-course-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 14px; background: #f3f7f8; } +.grade-course-head > div:first-child { min-width: 0; } +.grade-course-head span { color: #607b88; font: 700 10px/1.3 Consolas, monospace; } +.grade-course-head h3 { margin: 3px 0; color: var(--grade-ink); font-size: 15px; } +.grade-course-head small { display: block; overflow: hidden; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.course-rollup { display: grid; grid-template-columns: auto auto; align-items: baseline; column-gap: 5px; flex: none; text-align: right; } +.course-rollup b { color: var(--grade-teal); font: 750 22px/1 Consolas, monospace; } +.course-rollup > span { color: #55707b; font: 600 11px/1.2 inherit; } +.course-rollup small { grid-column: 1 / -1; margin-top: 3px; color: #70858d; font-size: 10px; } +.course-project-table { width: 100%; --el-table-border-color: #e0e8ea; } +.course-project-table :deep(td.el-table__cell), .course-project-table :deep(th.el-table__cell) { padding: 6px 0; } +.course-project-table :deep(th.el-table__cell) { color: #71858d; font-size: 10px; } +.course-project-name { display: grid; gap: 2px; } +.course-project-name b { color: #294d5c; font-size: 12px; } +.course-project-name small, .course-project-progress { color: #6d838b; font-size: 10px; } +.course-project-progress { display: flex; align-items: center; gap: 7px; } .student-course-results { display: grid; gap: 12px; min-height: 180px; } .student-course-result { overflow: hidden; border: 1px solid #cfdee3; background: #fff; } .student-course-head { min-height: 76px; padding: 11px 14px 11px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-left: 5px solid var(--grade-blue); background: #edf4f6; } @@ -942,9 +955,7 @@ onMounted(async () => { .workbench-actions b { color: #a24f36; font-size: 12px; } .workbench-actions span { color: var(--muted); font-size: 12px; } @media (max-width: 980px) { - .grade-project-card { grid-template-columns: 110px minmax(0, 1fr) auto; } - .sheet-progress, .sheet-empty { grid-column: 2; } - .grade-project-card > .el-button { grid-column: 3; grid-row: 1 / span 2; } + .grade-course-head { align-items: flex-start; } } @media (max-width: 700px) { .assessment-flow { grid-template-columns: 1fr; } @@ -952,8 +963,8 @@ onMounted(async () => { .grade-toolbar { align-items: stretch; flex-direction: column; } .grade-toolbar .el-select, .grade-toolbar .el-input { width: 100%; } .grade-toolbar > span { margin-left: 0; } - .grade-project-card { grid-template-columns: 1fr; } - .sheet-progress, .sheet-empty, .grade-project-card > .el-button { grid-column: 1; grid-row: auto; width: 100%; } + .grade-course-head { align-items: stretch; flex-direction: column; } + .course-rollup { align-self: flex-start; text-align: left; } .student-result-grid { grid-template-columns: 1fr; } .student-result-card > header { align-items: stretch; flex-direction: column; } .score-seal { width: auto; padding-top: 12px; grid-template-columns: auto auto; gap: 8px; border-top: 1px solid #dce6e8; border-left: 0; }