实验成绩管理改为紧凑的课程分组视图
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -198,10 +198,11 @@ public sealed class ExperimentGradesControllerTests
|
||||
1,
|
||||
10,
|
||||
CancellationToken.None));
|
||||
Assert.Equal(12, Property<int>(page.Value, "Total"));
|
||||
Assert.Equal(10, Property<System.Collections.IEnumerable>(
|
||||
page.Value,
|
||||
"Items").Cast<object>().Count());
|
||||
Assert.Equal(1, Property<int>(page.Value, "Total"));
|
||||
var course = Assert.Single(Property<System.Collections.IEnumerable>(
|
||||
page.Value, "Items").Cast<object>());
|
||||
Assert.Equal(12, Property<System.Collections.IEnumerable>(
|
||||
course, "Projects").Cast<object>().Count());
|
||||
|
||||
var filtered = Assert.IsType<OkObjectResult>(await controller.GetManagement(
|
||||
null,
|
||||
|
||||
@@ -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<any[]>([])
|
||||
const courseTotal = ref(0)
|
||||
const courses = ref<any[]>([])
|
||||
const studentResults = ref<any[]>([])
|
||||
const detailDrawer = ref(false)
|
||||
const detail = ref<any | null>(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 () => {
|
||||
<el-button type="primary" @click="projectPage = 1; load()">查询</el-button>
|
||||
</section>
|
||||
|
||||
<section v-loading="loading" class="grade-project-list">
|
||||
<article v-for="project in projects" :key="project.id" class="grade-project-card">
|
||||
<div class="project-mark">
|
||||
<span>{{ project.code }}</span>
|
||||
<b>{{ modeLabels[project.arrangementMode] }}</b>
|
||||
<section v-loading="loading" class="grade-course-list">
|
||||
<article v-for="course in courses" :key="course.teachingTaskId" class="grade-course-card">
|
||||
<header class="grade-course-head">
|
||||
<div>
|
||||
<span>{{ course.termName }} · {{ course.taskNumber }}</span>
|
||||
<h3>{{ course.courseCode }} · {{ course.courseName }}</h3>
|
||||
<small>{{ course.teacherNames.join('、') || '教师待定' }} · {{ course.collegeName }}</small>
|
||||
</div>
|
||||
<div class="project-summary">
|
||||
<h3>{{ project.name }}</h3>
|
||||
<p>{{ project.courseCode }} · {{ project.courseName }}</p>
|
||||
<small>{{ project.taskNumber }} · {{ project.termName }} · {{ project.teacherNames.join('、') || '教师待定' }}</small>
|
||||
<div class="course-rollup">
|
||||
<b>{{ course.projectCount }}</b><span>个实验项目</span>
|
||||
<small v-if="course.sheetCount">{{ course.sheetCount }} 份成绩单 · {{ course.scoredCount }}/{{ course.studentCount }} 已计分</small>
|
||||
<small v-else>尚未建立成绩单</small>
|
||||
</div>
|
||||
<template v-if="project.sheet">
|
||||
<div class="sheet-progress">
|
||||
<el-tag :type="statusTypes[project.sheet.status] as any" effect="plain">
|
||||
{{ statusLabels[project.sheet.status] }}
|
||||
</el-tag>
|
||||
<span><b>{{ project.sheet.scoredCount }}</b> / {{ project.sheet.studentCount }} 已计分</span>
|
||||
<span>评分项 {{ project.sheet.itemCount }} 个 · 合格线 {{ project.sheet.passScore }}</span>
|
||||
</header>
|
||||
<el-table :data="course.projects" size="small" class="course-project-table">
|
||||
<el-table-column label="实验项目" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="course-project-name">
|
||||
<b>{{ row.code }} · {{ row.name }}</b>
|
||||
<small>{{ modeLabels[row.arrangementMode] }}</small>
|
||||
</div>
|
||||
<el-button type="primary" plain :icon="EditPen" @click="openDetail(project)">
|
||||
打开成绩单
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="sheet-empty">
|
||||
<b>尚未建立实验成绩单</b>
|
||||
<span>建立时会按集中名单或有效预约生成评分对象。</span>
|
||||
</el-table-column>
|
||||
<el-table-column label="成绩进度" min-width="190">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.sheet" class="course-project-progress">
|
||||
<el-tag :type="statusTypes[row.sheet.status] as any" size="small" effect="plain">{{ statusLabels[row.sheet.status] }}</el-tag>
|
||||
<span>{{ row.sheet.scoredCount }}/{{ row.sheet.studentCount }} 已计分 · {{ row.sheet.itemCount }} 项</span>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate(project)">
|
||||
建立成绩单
|
||||
</el-button>
|
||||
<span v-else class="table-muted">尚未建立成绩单</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.sheet" type="primary" text :icon="EditPen" @click="openDetail(row)">打开成绩单</el-button>
|
||||
<el-button v-else type="primary" text :icon="Plus" @click="openCreate(row)">建立成绩单</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</article>
|
||||
<el-empty v-if="!loading && !projects.length" description="当前筛选条件下没有可评分实验项目" />
|
||||
<el-empty v-if="!loading && !courses.length" description="当前筛选条件下没有可评分实验课程" />
|
||||
</section>
|
||||
<el-pagination
|
||||
v-model:current-page="projectPage"
|
||||
v-model:page-size="projectPageSize"
|
||||
:total="projectTotal"
|
||||
:total="courseTotal"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@current-change="load"
|
||||
@@ -850,18 +857,24 @@ onMounted(async () => {
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user