修复实验成绩管理:
管理列表改为服务端分页,支持 10/20/50 条切换。 增加学期、成绩状态、开课学院以及项目/课程/教学班/教师关键词筛选。 数据范围分级:校级管理员可查看全校,院级管理员仅限本学院,教师仅限本人任课项目。 修复任课教师提交审核时报“无权限”:工作流查询现在会加载任课教师关系。 增加权限、学院隔离、分页和筛选回归测试。
This commit is contained in:
@@ -38,8 +38,15 @@ public sealed class ExperimentGradesController(
|
|||||||
public async Task<ActionResult> GetManagement(
|
public async Task<ActionResult> GetManagement(
|
||||||
Guid? academicTermId,
|
Guid? academicTermId,
|
||||||
ExperimentGradeSheetStatus? status,
|
ExperimentGradeSheetStatus? status,
|
||||||
CancellationToken cancellationToken)
|
Guid? collegeId = null,
|
||||||
|
string? keyword = null,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
page = Math.Max(1, page);
|
||||||
|
pageSize = Math.Clamp(pageSize, 10, 50);
|
||||||
|
keyword = Normalize(keyword);
|
||||||
var source = ScopedProjects().AsNoTracking()
|
var source = ScopedProjects().AsNoTracking()
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
x.Status == ExperimentProjectStatus.Published ||
|
x.Status == ExperimentProjectStatus.Published ||
|
||||||
@@ -51,11 +58,28 @@ public sealed class ExperimentGradesController(
|
|||||||
source = source.Where(x =>
|
source = source.Where(x =>
|
||||||
x.GradeSheet != null &&
|
x.GradeSheet != null &&
|
||||||
x.GradeSheet.Status == status.Value);
|
x.GradeSheet.Status == status.Value);
|
||||||
|
if (collegeId.HasValue)
|
||||||
|
source = source.Where(x =>
|
||||||
|
x.TeachingTask!.Course!.CollegeId == collegeId.Value);
|
||||||
|
if (keyword is not null)
|
||||||
|
source = source.Where(x =>
|
||||||
|
x.Code.Contains(keyword) ||
|
||||||
|
x.Name.Contains(keyword) ||
|
||||||
|
x.TeachingTask!.TaskNumber.Contains(keyword) ||
|
||||||
|
x.TeachingTask.Name.Contains(keyword) ||
|
||||||
|
x.TeachingTask.Course!.Code.Contains(keyword) ||
|
||||||
|
x.TeachingTask.Course.Name.Contains(keyword) ||
|
||||||
|
x.TeachingTask.Teachers.Any(item =>
|
||||||
|
item.Teacher!.TeacherNumber.Contains(keyword) ||
|
||||||
|
item.Teacher.Name.Contains(keyword)));
|
||||||
|
|
||||||
return Ok(await source
|
var total = await source.CountAsync(cancellationToken);
|
||||||
|
var items = await source
|
||||||
.OrderByDescending(x => x.TeachingTask!.AcademicTerm!.StartDate)
|
.OrderByDescending(x => x.TeachingTask!.AcademicTerm!.StartDate)
|
||||||
.ThenBy(x => x.TeachingTask!.TaskNumber)
|
.ThenBy(x => x.TeachingTask!.TaskNumber)
|
||||||
.ThenBy(x => x.Code)
|
.ThenBy(x => x.Code)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
.Select(x => new
|
.Select(x => new
|
||||||
{
|
{
|
||||||
x.Id,
|
x.Id,
|
||||||
@@ -96,7 +120,14 @@ public sealed class ExperimentGradesController(
|
|||||||
x.GradeSheet.PublishedAt
|
x.GradeSheet.PublishedAt
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ToListAsync(cancellationToken));
|
.ToListAsync(cancellationToken);
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
Total = total,
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("mine")]
|
[HttpGet("mine")]
|
||||||
@@ -747,6 +778,10 @@ public sealed class ExperimentGradesController(
|
|||||||
.Include(x => x.ExperimentProject)
|
.Include(x => x.ExperimentProject)
|
||||||
.ThenInclude(x => x!.TeachingTask)
|
.ThenInclude(x => x!.TeachingTask)
|
||||||
.ThenInclude(x => x!.Course)
|
.ThenInclude(x => x!.Course)
|
||||||
|
.Include(x => x.ExperimentProject)
|
||||||
|
.ThenInclude(x => x!.TeachingTask)
|
||||||
|
.ThenInclude(x => x!.Teachers)
|
||||||
|
.ThenInclude(x => x.Teacher)
|
||||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
@@ -1953,6 +1953,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.Property<Guid>("PlanId")
|
b.Property<Guid>("PlanId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("ProjectIdsJson")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<Guid?>("RequestedByUserId")
|
b.Property<Guid?>("RequestedByUserId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
|||||||
+6873
File diff suppressed because it is too large
Load Diff
+28
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ExamArrangementJobPayload : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ProjectIdsJson",
|
||||||
|
table: "ExamArrangementJobs",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ProjectIdsJson",
|
||||||
|
table: "ExamArrangementJobs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -1848,6 +1848,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.Property<int>("ProcessedSessions")
|
b.Property<int>("ProcessedSessions")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ProjectIdsJson")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<Guid?>("RequestedByUserId")
|
b.Property<Guid?>("RequestedByUserId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@@ -1950,6 +1953,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
|
|||||||
b.Property<Guid>("PlanId")
|
b.Property<Guid>("PlanId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("ProjectIdsJson")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<Guid?>("RequestedByUserId")
|
b.Property<Guid?>("RequestedByUserId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,160 @@ namespace Jiaowu.Api.Tests;
|
|||||||
|
|
||||||
public sealed class ExperimentGradesControllerTests
|
public sealed class ExperimentGradesControllerTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task AssignedTeacher_CanSubmitExperimentGradeSheet()
|
||||||
|
{
|
||||||
|
await using var fixture = await ExperimentGradeFixture.CreateAsync();
|
||||||
|
var project = new ExperimentProject
|
||||||
|
{
|
||||||
|
TeachingTaskId = fixture.Task.Id,
|
||||||
|
Code = "LAB-TEACHER",
|
||||||
|
Name = "教师提交实验",
|
||||||
|
ArrangementMode = ExperimentArrangementMode.Centralized,
|
||||||
|
StartDate = fixture.Term.StartDate,
|
||||||
|
EndDate = fixture.Term.EndDate,
|
||||||
|
Status = ExperimentProjectStatus.Published,
|
||||||
|
PublishedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
fixture.Db.ExperimentProjects.Add(project);
|
||||||
|
await fixture.Db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var admin = fixture.ExperimentGrades(fixture.AdminScope);
|
||||||
|
await admin.CreateSheet(
|
||||||
|
new ExperimentGradeSheetRequest(
|
||||||
|
project.Id,
|
||||||
|
1,
|
||||||
|
60,
|
||||||
|
[new ExperimentGradeItemRequest(
|
||||||
|
"操作",
|
||||||
|
ExperimentGradeItemKind.Operation,
|
||||||
|
100)]),
|
||||||
|
CancellationToken.None);
|
||||||
|
var sheet = await fixture.Db.ExperimentGradeSheets
|
||||||
|
.Include(x => x.Items)
|
||||||
|
.Include(x => x.Records)
|
||||||
|
.ThenInclude(x => x.ItemScores)
|
||||||
|
.SingleAsync();
|
||||||
|
var record = Assert.Single(sheet.Records);
|
||||||
|
var item = Assert.Single(sheet.Items);
|
||||||
|
|
||||||
|
var teacher = fixture.ExperimentGrades(fixture.TeacherScope);
|
||||||
|
Assert.IsType<NoContentResult>(await teacher.UpdateRecords(
|
||||||
|
sheet.Id,
|
||||||
|
new ExperimentGradeRecordsRequest(
|
||||||
|
[
|
||||||
|
new ExperimentGradeRecordRequest(
|
||||||
|
record.Id,
|
||||||
|
ExperimentParticipationStatus.Completed,
|
||||||
|
false,
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
[new ExperimentGradeItemScoreRequest(item.Id, 85, null)])
|
||||||
|
]),
|
||||||
|
CancellationToken.None));
|
||||||
|
fixture.Db.ChangeTracker.Clear();
|
||||||
|
Assert.IsType<NoContentResult>(await teacher.Submit(
|
||||||
|
sheet.Id,
|
||||||
|
CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ManagementList_IsPagedFilteredAndCollegeScoped()
|
||||||
|
{
|
||||||
|
await using var fixture = await ExperimentGradeFixture.CreateAsync();
|
||||||
|
var collegeId = await fixture.Db.Courses
|
||||||
|
.Where(x => x.Id == fixture.Task.CourseId)
|
||||||
|
.Select(x => x.CollegeId)
|
||||||
|
.SingleAsync();
|
||||||
|
for (var index = 1; index <= 12; index++)
|
||||||
|
{
|
||||||
|
fixture.Db.ExperimentProjects.Add(new ExperimentProject
|
||||||
|
{
|
||||||
|
TeachingTaskId = fixture.Task.Id,
|
||||||
|
Code = $"LAB-{index:00}",
|
||||||
|
Name = $"分页实验 {index:00}",
|
||||||
|
ArrangementMode = ExperimentArrangementMode.Centralized,
|
||||||
|
StartDate = fixture.Term.StartDate,
|
||||||
|
EndDate = fixture.Term.EndDate,
|
||||||
|
Status = ExperimentProjectStatus.Published,
|
||||||
|
PublishedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherCollege = new College { Code = "OTHER", Name = "其他学院" };
|
||||||
|
var otherCourse = new Course
|
||||||
|
{
|
||||||
|
CollegeId = otherCollege.Id,
|
||||||
|
Code = "OTHER-LAB",
|
||||||
|
Name = "其他学院实验",
|
||||||
|
Credits = 1,
|
||||||
|
TotalHours = 16,
|
||||||
|
PracticeHours = 16,
|
||||||
|
Nature = CourseNature.Practice,
|
||||||
|
AssessmentMethod = AssessmentMethod.Assessment
|
||||||
|
};
|
||||||
|
var otherTask = new TeachingTask
|
||||||
|
{
|
||||||
|
AcademicTermId = fixture.Term.Id,
|
||||||
|
CourseId = otherCourse.Id,
|
||||||
|
TaskNumber = "OTHER-LAB-01",
|
||||||
|
Name = "其他学院实验班",
|
||||||
|
Capacity = 20,
|
||||||
|
Status = TeachingTaskStatus.Published
|
||||||
|
};
|
||||||
|
var otherProject = new ExperimentProject
|
||||||
|
{
|
||||||
|
TeachingTaskId = otherTask.Id,
|
||||||
|
Code = "FOREIGN-LAB",
|
||||||
|
Name = "不可见实验",
|
||||||
|
ArrangementMode = ExperimentArrangementMode.Centralized,
|
||||||
|
StartDate = fixture.Term.StartDate,
|
||||||
|
EndDate = fixture.Term.EndDate,
|
||||||
|
Status = ExperimentProjectStatus.Published,
|
||||||
|
PublishedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
fixture.Db.AddRange(otherCollege, otherCourse, otherTask, otherProject);
|
||||||
|
await fixture.Db.SaveChangesAsync();
|
||||||
|
fixture.Db.ChangeTracker.Clear();
|
||||||
|
|
||||||
|
var collegeScope = new FixedScope(new CurrentUserScope(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
"学院管理员",
|
||||||
|
collegeId,
|
||||||
|
DataScope.College,
|
||||||
|
new HashSet<string>([SystemRoles.CollegeAdmin])));
|
||||||
|
var controller = fixture.ExperimentGrades(collegeScope);
|
||||||
|
var page = Assert.IsType<OkObjectResult>(await controller.GetManagement(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
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());
|
||||||
|
|
||||||
|
var filtered = Assert.IsType<OkObjectResult>(await controller.GetManagement(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"LAB-12",
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
CancellationToken.None));
|
||||||
|
Assert.Equal(1, Property<int>(filtered.Value, "Total"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T Property<T>(object? value, string name) =>
|
||||||
|
Assert.IsAssignableFrom<T>(
|
||||||
|
value!.GetType().GetProperty(name)!.GetValue(value));
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task IndependentExperimentGrade_CanPublishAndImportAsCourseSnapshot()
|
public async Task IndependentExperimentGrade_CanPublishAndImportAsCourseSnapshot()
|
||||||
{
|
{
|
||||||
@@ -260,6 +414,7 @@ public sealed class ExperimentGradesControllerTests
|
|||||||
Student student,
|
Student student,
|
||||||
Classroom classroom,
|
Classroom classroom,
|
||||||
ICurrentUserDataScope adminScope,
|
ICurrentUserDataScope adminScope,
|
||||||
|
ICurrentUserDataScope teacherScope,
|
||||||
ICurrentUserDataScope studentScope)
|
ICurrentUserDataScope studentScope)
|
||||||
{
|
{
|
||||||
Connection = connection;
|
Connection = connection;
|
||||||
@@ -269,6 +424,7 @@ public sealed class ExperimentGradesControllerTests
|
|||||||
Student = student;
|
Student = student;
|
||||||
Classroom = classroom;
|
Classroom = classroom;
|
||||||
AdminScope = adminScope;
|
AdminScope = adminScope;
|
||||||
|
TeacherScope = teacherScope;
|
||||||
StudentScope = studentScope;
|
StudentScope = studentScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,6 +435,7 @@ public sealed class ExperimentGradesControllerTests
|
|||||||
public Student Student { get; }
|
public Student Student { get; }
|
||||||
public Classroom Classroom { get; }
|
public Classroom Classroom { get; }
|
||||||
public ICurrentUserDataScope AdminScope { get; }
|
public ICurrentUserDataScope AdminScope { get; }
|
||||||
|
public ICurrentUserDataScope TeacherScope { get; }
|
||||||
public ICurrentUserDataScope StudentScope { get; }
|
public ICurrentUserDataScope StudentScope { get; }
|
||||||
|
|
||||||
public static async Task<ExperimentGradeFixture> CreateAsync()
|
public static async Task<ExperimentGradeFixture> CreateAsync()
|
||||||
@@ -410,6 +567,7 @@ public sealed class ExperimentGradesControllerTests
|
|||||||
student,
|
student,
|
||||||
classroom,
|
classroom,
|
||||||
Scope(admin, SystemRoles.SuperAdmin, DataScope.All),
|
Scope(admin, SystemRoles.SuperAdmin, DataScope.All),
|
||||||
|
Scope(teacherUser, SystemRoles.Teacher, DataScope.Self),
|
||||||
Scope(studentUser, SystemRoles.Student, DataScope.Self));
|
Scope(studentUser, SystemRoles.Student, DataScope.Self));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -344,7 +344,7 @@ button { cursor: pointer; }
|
|||||||
.form-grid.three { grid-template-columns: repeat(3, 1fr); }
|
.form-grid.three { grid-template-columns: repeat(3, 1fr); }
|
||||||
.visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
.visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||||
|
|
||||||
/* MessageBox 的定位与底色不能依赖其伪元素:部分浏览器在遮罩层中会将其压到左上角。 */
|
/* 由遮罩层 flex 居中;MessageBox 默认的全屏 fixed 包装层会脱离该布局。 */
|
||||||
.el-overlay.is-message-box {
|
.el-overlay.is-message-box {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -352,6 +352,7 @@ button { cursor: pointer; }
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.el-overlay.is-message-box .el-overlay-message-box {
|
.el-overlay.is-message-box .el-overlay-message-box {
|
||||||
|
position: static !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: min(420px, calc(100vw - 32px));
|
width: min(420px, calc(100vw - 32px));
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
|
|||||||
@@ -19,12 +19,21 @@ import {
|
|||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const isStudent = computed(() => auth.user?.roles.includes('Student') ?? false)
|
const isStudent = computed(() => auth.user?.roles.includes('Student') ?? false)
|
||||||
|
const canViewAllColleges = computed(() =>
|
||||||
|
auth.user?.roles.some((role) => ['SuperAdmin', 'AcademicAdmin'].includes(role)) ?? false,
|
||||||
|
)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const terms = ref<any[]>([])
|
const terms = ref<any[]>([])
|
||||||
|
const colleges = ref<any[]>([])
|
||||||
const termId = ref('')
|
const termId = ref('')
|
||||||
const statusFilter = ref('')
|
const statusFilter = ref('')
|
||||||
|
const collegeId = ref('')
|
||||||
|
const projectKeyword = ref('')
|
||||||
|
const projectPage = ref(1)
|
||||||
|
const projectPageSize = ref(20)
|
||||||
|
const projectTotal = ref(0)
|
||||||
const projects = ref<any[]>([])
|
const projects = ref<any[]>([])
|
||||||
const studentResults = ref<any[]>([])
|
const studentResults = ref<any[]>([])
|
||||||
const detailDrawer = ref(false)
|
const detailDrawer = ref(false)
|
||||||
@@ -177,12 +186,20 @@ async function load() {
|
|||||||
if (isStudent.value) {
|
if (isStudent.value) {
|
||||||
studentResults.value = (await http.get('/experiment-grades/mine')).data
|
studentResults.value = (await http.get('/experiment-grades/mine')).data
|
||||||
} else {
|
} else {
|
||||||
projects.value = (await http.get('/experiment-grades/management', {
|
const data = (await http.get('/experiment-grades/management', {
|
||||||
params: {
|
params: {
|
||||||
academicTermId: termId.value || undefined,
|
academicTermId: termId.value || undefined,
|
||||||
status: statusFilter.value || undefined,
|
status: statusFilter.value || undefined,
|
||||||
|
collegeId: collegeId.value || undefined,
|
||||||
|
keyword: projectKeyword.value.trim() || undefined,
|
||||||
|
page: projectPage.value,
|
||||||
|
pageSize: projectPageSize.value,
|
||||||
},
|
},
|
||||||
})).data
|
})).data
|
||||||
|
projects.value = data.items
|
||||||
|
projectTotal.value = data.total
|
||||||
|
projectPage.value = data.page
|
||||||
|
projectPageSize.value = data.pageSize
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(apiErrorMessage(error))
|
ElMessage.error(apiErrorMessage(error))
|
||||||
@@ -367,7 +384,11 @@ function scoreTone(result: any) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!isStudent.value) {
|
if (!isStudent.value) {
|
||||||
try {
|
try {
|
||||||
terms.value = (await http.get('/base-data/terms')).data
|
const requests: Promise<any>[] = [http.get('/base-data/terms')]
|
||||||
|
if (canViewAllColleges.value) requests.push(http.get('/base-data/colleges'))
|
||||||
|
const [termResponse, collegeResponse] = await Promise.all(requests)
|
||||||
|
terms.value = termResponse.data
|
||||||
|
colleges.value = collegeResponse?.data ?? []
|
||||||
termId.value = defaultAcademicTermId(terms.value) ?? ''
|
termId.value = defaultAcademicTermId(terms.value) ?? ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(apiErrorMessage(error))
|
ElMessage.error(apiErrorMessage(error))
|
||||||
@@ -438,7 +459,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<section class="grade-toolbar">
|
<section class="grade-toolbar">
|
||||||
<el-select v-model="termId" clearable placeholder="全部学期" @change="load">
|
<el-select v-model="termId" clearable placeholder="全部学期" @change="projectPage = 1; load()">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="term in terms"
|
v-for="term in terms"
|
||||||
:key="term.id"
|
:key="term.id"
|
||||||
@@ -447,10 +468,27 @@ onMounted(async () => {
|
|||||||
:class="academicTermOptionClass(term)"
|
:class="academicTermOptionClass(term)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select v-model="statusFilter" clearable placeholder="全部成绩状态" @change="load">
|
<el-select v-model="statusFilter" clearable placeholder="全部成绩状态" @change="projectPage = 1; load()">
|
||||||
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value" />
|
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<span>每个实验项目独立建单;课程只读取已发布的汇总快照。</span>
|
<el-select
|
||||||
|
v-if="canViewAllColleges"
|
||||||
|
v-model="collegeId"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="全部开课学院"
|
||||||
|
@change="projectPage = 1; load()"
|
||||||
|
>
|
||||||
|
<el-option v-for="college in colleges" :key="college.id" :label="college.name" :value="college.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-model="projectKeyword"
|
||||||
|
clearable
|
||||||
|
placeholder="项目、课程、教学班或教师"
|
||||||
|
@keyup.enter="projectPage = 1; load()"
|
||||||
|
@clear="projectPage = 1; load()"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="projectPage = 1; load()">查询</el-button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-loading="loading" class="grade-project-list">
|
<section v-loading="loading" class="grade-project-list">
|
||||||
@@ -488,6 +526,15 @@ onMounted(async () => {
|
|||||||
</article>
|
</article>
|
||||||
<el-empty v-if="!loading && !projects.length" description="当前筛选条件下没有可评分实验项目" />
|
<el-empty v-if="!loading && !projects.length" description="当前筛选条件下没有可评分实验项目" />
|
||||||
</section>
|
</section>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="projectPage"
|
||||||
|
v-model:page-size="projectPageSize"
|
||||||
|
:total="projectTotal"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="projectPage = 1; load()"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -724,6 +771,7 @@ onMounted(async () => {
|
|||||||
.assessment-flow > i { display: grid; place-items: center; color: #8aa0aa; font-style: normal; background: #f4f7f8; }
|
.assessment-flow > i { display: grid; place-items: center; color: #8aa0aa; font-style: normal; background: #f4f7f8; }
|
||||||
.grade-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
|
.grade-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
|
||||||
.grade-toolbar .el-select { width: 220px; }
|
.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-toolbar > span { margin-left: auto; color: var(--muted); font-size: 12px; }
|
||||||
.grade-project-list { display: grid; gap: 12px; min-height: 180px; }
|
.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%); }
|
.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%); }
|
||||||
@@ -812,7 +860,7 @@ onMounted(async () => {
|
|||||||
.assessment-flow { grid-template-columns: 1fr; }
|
.assessment-flow { grid-template-columns: 1fr; }
|
||||||
.assessment-flow > i { display: none; }
|
.assessment-flow > i { display: none; }
|
||||||
.grade-toolbar { align-items: stretch; flex-direction: column; }
|
.grade-toolbar { align-items: stretch; flex-direction: column; }
|
||||||
.grade-toolbar .el-select { width: 100%; }
|
.grade-toolbar .el-select, .grade-toolbar .el-input { width: 100%; }
|
||||||
.grade-toolbar > span { margin-left: 0; }
|
.grade-toolbar > span { margin-left: 0; }
|
||||||
.grade-project-card { grid-template-columns: 1fr; }
|
.grade-project-card { grid-template-columns: 1fr; }
|
||||||
.sheet-progress, .sheet-empty, .grade-project-card > .el-button { grid-column: 1; grid-row: auto; width: 100%; }
|
.sheet-progress, .sheet-empty, .grade-project-card > .el-button { grid-column: 1; grid-row: auto; width: 100%; }
|
||||||
|
|||||||
Reference in New Issue
Block a user