草稿计划支持选择场次后“批量移除已选场次”,一次最多 100 个。
批量操作会完整校验:包含不存在、其他计划或已发布计划的场次时整批拒绝,不会部分删除。[ExamsController.cs (line 384)](E:/jiaowu/src/Jiaowu.Api/Controllers/ExamsController.cs:384) 草稿计划新增“删除草稿”按钮,删除前显示计划名称及场次数量确认。[ExamsView.vue (line 292)](E:/jiaowu/web/src/views/ExamsView.vue:292) 删除草稿计划会级联清理场次和监考关联;已发布计划不能删除。[ExamsController.cs (line 81)](E:/jiaowu/src/Jiaowu.Api/Controllers/ExamsController.cs:81) 删除后会自动切换到其他计划;没有剩余计划时正确显示空状态。 新增草稿、已发布、跨计划混选及级联删除测试。[ExamDeletionControllerTests.cs (line 17)](E:/jiaowu/tests/Jiaowu.Api.Tests/ExamDeletionControllerTests.cs:17)
This commit is contained in:
@@ -76,6 +76,33 @@ public sealed class ExamsController(
|
|||||||
return await SaveAsync(plan.Id, true, cancellationToken);
|
return await SaveAsync(plan.Id, true, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("plans/{id:guid}")]
|
||||||
|
[Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> DeletePlan(
|
||||||
|
Guid id,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var plan = await db.ExamPlans
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||||
|
if (plan is null) return NotFound();
|
||||||
|
if (plan.Status != ExamPlanStatus.Draft)
|
||||||
|
return ConflictProblem("只有草稿考试计划可以删除。");
|
||||||
|
|
||||||
|
db.ExamPlans.Remove(plan);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
await cache.RemoveByTagAsync(
|
||||||
|
AppCacheTags.Timetables,
|
||||||
|
cancellationToken);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException)
|
||||||
|
{
|
||||||
|
return ConflictProblem("删除考试计划失败,关联数据可能已发生变化。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("plans/{id:guid}")]
|
[HttpGet("plans/{id:guid}")]
|
||||||
public async Task<ActionResult> GetPlan(Guid id, CancellationToken cancellationToken)
|
public async Task<ActionResult> GetPlan(Guid id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -354,6 +381,48 @@ public sealed class ExamsController(
|
|||||||
return await SaveAsync(id, false, cancellationToken);
|
return await SaveAsync(id, false, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("plans/{planId:guid}/sessions/batch-remove")]
|
||||||
|
[Authorize(Roles = Managers)]
|
||||||
|
public async Task<ActionResult> RemoveSessionsBatch(
|
||||||
|
Guid planId,
|
||||||
|
RemoveExamSessionsBatchRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sessionIds = request.SessionIds.Distinct().ToArray();
|
||||||
|
if (sessionIds.Length == 0)
|
||||||
|
return ValidationProblem("请至少选择一个考试场次。");
|
||||||
|
if (sessionIds.Length > 100)
|
||||||
|
return ValidationProblem("一次最多移除100个考试场次。");
|
||||||
|
|
||||||
|
var plan = await db.ExamPlans.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == planId, cancellationToken);
|
||||||
|
if (plan is null) return NotFound();
|
||||||
|
if (plan.Status != ExamPlanStatus.Draft)
|
||||||
|
return ConflictProblem("已发布的考试计划不能调整场次。");
|
||||||
|
|
||||||
|
var sessions = await db.ExamSessions
|
||||||
|
.Where(x => x.ExamPlanId == planId)
|
||||||
|
.WhereIn(sessionIds, x => x.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
if (sessions.Count != sessionIds.Length)
|
||||||
|
return ConflictProblem(
|
||||||
|
"所选考试场次包含不存在或不属于当前计划的记录,请刷新后重新选择。");
|
||||||
|
|
||||||
|
db.ExamSessions.RemoveRange(sessions);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
await cache.RemoveByTagAsync(
|
||||||
|
AppCacheTags.Timetables,
|
||||||
|
cancellationToken);
|
||||||
|
return Ok(new { removedCount = sessions.Count });
|
||||||
|
}
|
||||||
|
catch (DbUpdateException)
|
||||||
|
{
|
||||||
|
return ConflictProblem("批量移除考试场次失败,关联数据可能已发生变化。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// Auto-arrange
|
// Auto-arrange
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
@@ -958,6 +1027,9 @@ public sealed record CreateExamSessionsBatchRequest(
|
|||||||
[Range(1, 10)] int RequiredInvigilatorCount,
|
[Range(1, 10)] int RequiredInvigilatorCount,
|
||||||
[MaxLength(500)] string? Notes);
|
[MaxLength(500)] string? Notes);
|
||||||
|
|
||||||
|
public sealed record RemoveExamSessionsBatchRequest(
|
||||||
|
[Required] IReadOnlyCollection<Guid> SessionIds);
|
||||||
|
|
||||||
public sealed record ExamAutoArrangeRequest(
|
public sealed record ExamAutoArrangeRequest(
|
||||||
IReadOnlyCollection<Guid>? SessionIds = null,
|
IReadOnlyCollection<Guid>? SessionIds = null,
|
||||||
bool AssignClassrooms = true,
|
bool AssignClassrooms = true,
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
using Jiaowu.Api.Controllers;
|
||||||
|
using Jiaowu.Api.Domain.Academic;
|
||||||
|
using Jiaowu.Api.Domain.Identity;
|
||||||
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
|
using Jiaowu.Api.Infrastructure.Caching;
|
||||||
|
using Jiaowu.Api.Infrastructure.Exams;
|
||||||
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Tests;
|
||||||
|
|
||||||
|
public sealed class ExamDeletionControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveSessionsBatch_ValidatesCompleteSelectionBeforeRemoving()
|
||||||
|
{
|
||||||
|
await using var fixture = await Fixture.CreateAsync();
|
||||||
|
var controller = fixture.CreateController();
|
||||||
|
var selectedIds = fixture.DraftPlan.Sessions
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToArray();
|
||||||
|
var otherPlanSessionId = fixture.PublishedPlan.Sessions.Single().Id;
|
||||||
|
|
||||||
|
var invalidResult = await controller.RemoveSessionsBatch(
|
||||||
|
fixture.DraftPlan.Id,
|
||||||
|
new RemoveExamSessionsBatchRequest(
|
||||||
|
[selectedIds[0], otherPlanSessionId]),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.IsType<ConflictObjectResult>(invalidResult);
|
||||||
|
Assert.Equal(2, await fixture.Db.ExamSessions.CountAsync(
|
||||||
|
x => x.ExamPlanId == fixture.DraftPlan.Id));
|
||||||
|
|
||||||
|
var result = Assert.IsType<OkObjectResult>(
|
||||||
|
await controller.RemoveSessionsBatch(
|
||||||
|
fixture.DraftPlan.Id,
|
||||||
|
new RemoveExamSessionsBatchRequest(selectedIds),
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
Assert.Equal(2, ReadIntProperty(result.Value!, "removedCount"));
|
||||||
|
Assert.False(await fixture.Db.ExamSessions.AnyAsync(
|
||||||
|
x => x.ExamPlanId == fixture.DraftPlan.Id));
|
||||||
|
Assert.False(await fixture.Db.ExamSessionInvigilators.AnyAsync(
|
||||||
|
x => selectedIds.Contains(x.ExamSessionId)));
|
||||||
|
Assert.True(await fixture.Db.ExamPlans.AnyAsync(
|
||||||
|
x => x.Id == fixture.DraftPlan.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveSessionsBatch_RejectsPublishedPlan()
|
||||||
|
{
|
||||||
|
await using var fixture = await Fixture.CreateAsync();
|
||||||
|
var controller = fixture.CreateController();
|
||||||
|
var sessionId = fixture.PublishedPlan.Sessions.Single().Id;
|
||||||
|
|
||||||
|
var result = await controller.RemoveSessionsBatch(
|
||||||
|
fixture.PublishedPlan.Id,
|
||||||
|
new RemoveExamSessionsBatchRequest([sessionId]),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.IsType<ConflictObjectResult>(result);
|
||||||
|
Assert.True(await fixture.Db.ExamSessions.AnyAsync(
|
||||||
|
x => x.Id == sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeletePlan_DeletesDraftWithSessions_AndRejectsPublishedPlan()
|
||||||
|
{
|
||||||
|
await using var fixture = await Fixture.CreateAsync();
|
||||||
|
var controller = fixture.CreateController();
|
||||||
|
var draftPlanId = fixture.DraftPlan.Id;
|
||||||
|
var publishedPlanId = fixture.PublishedPlan.Id;
|
||||||
|
var draftSessionIds = fixture.DraftPlan.Sessions
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var publishedResult = await controller.DeletePlan(
|
||||||
|
publishedPlanId,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.IsType<ConflictObjectResult>(publishedResult);
|
||||||
|
Assert.True(await fixture.Db.ExamPlans.AnyAsync(
|
||||||
|
x => x.Id == publishedPlanId));
|
||||||
|
|
||||||
|
var draftResult = await controller.DeletePlan(
|
||||||
|
draftPlanId,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.IsType<NoContentResult>(draftResult);
|
||||||
|
Assert.False(await fixture.Db.ExamPlans.AnyAsync(
|
||||||
|
x => x.Id == draftPlanId));
|
||||||
|
Assert.False(await fixture.Db.ExamSessions.AnyAsync(
|
||||||
|
x => x.ExamPlanId == draftPlanId));
|
||||||
|
Assert.False(await fixture.Db.ExamSessionInvigilators.AnyAsync(
|
||||||
|
x => draftSessionIds.Contains(x.ExamSessionId)));
|
||||||
|
Assert.True(await fixture.Db.ExamPlans.AnyAsync(
|
||||||
|
x => x.Id == publishedPlanId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadIntProperty(object value, string name) =>
|
||||||
|
(int)value.GetType().GetProperty(name)!.GetValue(value)!;
|
||||||
|
|
||||||
|
private sealed class Fixture : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly SqliteConnection connection;
|
||||||
|
|
||||||
|
private Fixture(
|
||||||
|
SqliteConnection connection,
|
||||||
|
AppDbContext db,
|
||||||
|
ExamPlan draftPlan,
|
||||||
|
ExamPlan publishedPlan)
|
||||||
|
{
|
||||||
|
this.connection = connection;
|
||||||
|
Db = db;
|
||||||
|
DraftPlan = draftPlan;
|
||||||
|
PublishedPlan = publishedPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppDbContext Db { get; }
|
||||||
|
public ExamPlan DraftPlan { get; }
|
||||||
|
public ExamPlan PublishedPlan { get; }
|
||||||
|
|
||||||
|
public static async Task<Fixture> CreateAsync()
|
||||||
|
{
|
||||||
|
var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(connection)
|
||||||
|
.Options;
|
||||||
|
var db = new AppDbContext(options);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var college = new College
|
||||||
|
{
|
||||||
|
Code = "CS",
|
||||||
|
Name = "计算机学院"
|
||||||
|
};
|
||||||
|
var term = new AcademicTerm
|
||||||
|
{
|
||||||
|
Code = "2026-1",
|
||||||
|
Name = "2026—2027 学年第一学期",
|
||||||
|
AcademicYear = "2026-2027",
|
||||||
|
Season = TermSeason.Autumn,
|
||||||
|
StartDate = new DateOnly(2026, 9, 1),
|
||||||
|
EndDate = new DateOnly(2027, 1, 20)
|
||||||
|
};
|
||||||
|
var course = new Course
|
||||||
|
{
|
||||||
|
Code = "CS101",
|
||||||
|
Name = "程序设计基础",
|
||||||
|
CollegeId = college.Id,
|
||||||
|
Credits = 4,
|
||||||
|
TotalHours = 64,
|
||||||
|
LectureHours = 48,
|
||||||
|
PracticeHours = 16,
|
||||||
|
Nature = CourseNature.MajorRequired,
|
||||||
|
AssessmentMethod = AssessmentMethod.Examination
|
||||||
|
};
|
||||||
|
var firstTask = CreateTask(
|
||||||
|
term.Id,
|
||||||
|
course.Id,
|
||||||
|
"2026-1-CS101-01");
|
||||||
|
var secondTask = CreateTask(
|
||||||
|
term.Id,
|
||||||
|
course.Id,
|
||||||
|
"2026-1-CS101-02");
|
||||||
|
var invigilator = new Teacher
|
||||||
|
{
|
||||||
|
TeacherNumber = "T001",
|
||||||
|
Name = "张老师",
|
||||||
|
CollegeId = college.Id
|
||||||
|
};
|
||||||
|
var firstDraftSession = CreateSession(
|
||||||
|
firstTask.Id,
|
||||||
|
new DateOnly(2027, 1, 8));
|
||||||
|
firstDraftSession.Invigilators =
|
||||||
|
[
|
||||||
|
new ExamSessionInvigilator
|
||||||
|
{
|
||||||
|
TeacherId = invigilator.Id
|
||||||
|
}
|
||||||
|
];
|
||||||
|
var draftPlan = new ExamPlan
|
||||||
|
{
|
||||||
|
AcademicTermId = term.Id,
|
||||||
|
Name = "期末考试草稿",
|
||||||
|
Sessions =
|
||||||
|
[
|
||||||
|
firstDraftSession,
|
||||||
|
CreateSession(secondTask.Id, new DateOnly(2027, 1, 9))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
var publishedPlan = new ExamPlan
|
||||||
|
{
|
||||||
|
AcademicTermId = term.Id,
|
||||||
|
Name = "已发布期末考试",
|
||||||
|
Status = ExamPlanStatus.Published,
|
||||||
|
PublishedAt = DateTime.UtcNow,
|
||||||
|
Sessions =
|
||||||
|
[
|
||||||
|
CreateSession(firstTask.Id, new DateOnly(2027, 1, 10))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
db.AddRange(
|
||||||
|
college,
|
||||||
|
term,
|
||||||
|
course,
|
||||||
|
firstTask,
|
||||||
|
secondTask,
|
||||||
|
invigilator,
|
||||||
|
draftPlan,
|
||||||
|
publishedPlan);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new Fixture(
|
||||||
|
connection,
|
||||||
|
db,
|
||||||
|
draftPlan,
|
||||||
|
publishedPlan);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExamsController CreateController() => new(
|
||||||
|
Db,
|
||||||
|
new ManagerDataScope(),
|
||||||
|
new ExamArrangementService(Db),
|
||||||
|
NoOpAppCache.Instance);
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await Db.DisposeAsync();
|
||||||
|
await connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TeachingTask CreateTask(
|
||||||
|
Guid termId,
|
||||||
|
Guid courseId,
|
||||||
|
string number) => new()
|
||||||
|
{
|
||||||
|
AcademicTermId = termId,
|
||||||
|
CourseId = courseId,
|
||||||
|
TaskNumber = number,
|
||||||
|
Name = $"教学班 {number}",
|
||||||
|
Capacity = 60,
|
||||||
|
Status = TeachingTaskStatus.Published
|
||||||
|
};
|
||||||
|
|
||||||
|
private static ExamSession CreateSession(
|
||||||
|
Guid taskId,
|
||||||
|
DateOnly examDate) => new()
|
||||||
|
{
|
||||||
|
TeachingTaskId = taskId,
|
||||||
|
ExamDate = examDate,
|
||||||
|
StartPeriod = 1,
|
||||||
|
PeriodCount = 2,
|
||||||
|
StartsAt = examDate.ToDateTime(new TimeOnly(8, 0)),
|
||||||
|
EndsAt = examDate.ToDateTime(new TimeOnly(9, 50)),
|
||||||
|
RequiredInvigilatorCount = 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ManagerDataScope : ICurrentUserDataScope
|
||||||
|
{
|
||||||
|
public CurrentUserScope Current { get; } = new(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
"考试管理员",
|
||||||
|
null,
|
||||||
|
DataScope.All,
|
||||||
|
new HashSet<string>([SystemRoles.AcademicAdmin]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { Download, Plus, Promotion, Refresh, UserFilled, Setting } from '@element-plus/icons-vue'
|
import { Delete, Download, Plus, Promotion, Refresh, UserFilled, Setting } from '@element-plus/icons-vue'
|
||||||
import http, { apiErrorMessage } from '../api/http'
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
import { downloadApiFile } from '../api/excel'
|
import { downloadApiFile } from '../api/excel'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
@@ -22,6 +22,8 @@ const timeSlots = ref<any[]>([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const arrangeLoading = ref(false)
|
const arrangeLoading = ref(false)
|
||||||
const exportLoading = ref(false)
|
const exportLoading = ref(false)
|
||||||
|
const removeLoading = ref(false)
|
||||||
|
const deletePlanLoading = ref(false)
|
||||||
const planDialog = ref(false)
|
const planDialog = ref(false)
|
||||||
const sessionDialog = ref(false)
|
const sessionDialog = ref(false)
|
||||||
const editingSession = ref<any | null>(null)
|
const editingSession = ref<any | null>(null)
|
||||||
@@ -104,6 +106,12 @@ async function load() {
|
|||||||
?? plans.value.find((x) => x.termIsCurrent)
|
?? plans.value.find((x) => x.termIsCurrent)
|
||||||
?? plans.value[0]
|
?? plans.value[0]
|
||||||
if (plan) await selectPlan(plan.id)
|
if (plan) await selectPlan(plan.id)
|
||||||
|
else {
|
||||||
|
selected.value = null
|
||||||
|
selectedSessionIds.value = []
|
||||||
|
tasks.value = []
|
||||||
|
timeSlots.value = []
|
||||||
|
}
|
||||||
} catch (error) { ElMessage.error(apiErrorMessage(error)) }
|
} catch (error) { ElMessage.error(apiErrorMessage(error)) }
|
||||||
finally { loading.value = false }
|
finally { loading.value = false }
|
||||||
}
|
}
|
||||||
@@ -189,6 +197,35 @@ async function removeSession(row: any) {
|
|||||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function removeSelectedSessions() {
|
||||||
|
if (!selected.value || selectedSessionIds.value.length === 0) return
|
||||||
|
const count = selectedSessionIds.value.length
|
||||||
|
if (count > 100) {
|
||||||
|
ElMessage.warning('一次最多移除100个考试场次,请分批选择')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定从当前考试计划移除已选的 ${count} 个考试场次吗?`,
|
||||||
|
'批量移除考试场次',
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '确认移除',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
removeLoading.value = true
|
||||||
|
const res = await http.post(
|
||||||
|
`/exams/plans/${selected.value.id}/sessions/batch-remove`,
|
||||||
|
{ sessionIds: selectedSessionIds.value },
|
||||||
|
)
|
||||||
|
ElMessage.success(`已移除 ${res.data.removedCount} 个考试场次`)
|
||||||
|
await selectPlan(selected.value.id)
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
removeLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
function selectFilteredTasks() {
|
function selectFilteredTasks() {
|
||||||
selectedTaskIds.value = Array.from(new Set([
|
selectedTaskIds.value = Array.from(new Set([
|
||||||
...selectedTaskIds.value,
|
...selectedTaskIds.value,
|
||||||
@@ -252,6 +289,29 @@ async function publishPlan() {
|
|||||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function deleteDraftPlan() {
|
||||||
|
if (!selected.value || selected.value.status !== 'Draft') return
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定删除草稿考试计划“${selected.value.name}”吗?计划内 ${selected.value.sessions.length} 个考试场次也会一并删除,此操作无法撤销。`,
|
||||||
|
'删除考试计划',
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除草稿',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
deletePlanLoading.value = true
|
||||||
|
await http.delete(`/exams/plans/${selected.value.id}`)
|
||||||
|
ElMessage.success('草稿考试计划已删除')
|
||||||
|
selected.value = null
|
||||||
|
selectedSessionIds.value = []
|
||||||
|
await load()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
deletePlanLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
async function exportSignInSheets() {
|
async function exportSignInSheets() {
|
||||||
if (!selected.value) return
|
if (!selected.value) return
|
||||||
exportLoading.value = true
|
exportLoading.value = true
|
||||||
@@ -345,6 +405,14 @@ onMounted(async () => {
|
|||||||
<el-button v-if="selected.status === 'Draft'" :icon="Setting" @click="autoArrange('all')" :loading="arrangeLoading">一键完成</el-button>
|
<el-button v-if="selected.status === 'Draft'" :icon="Setting" @click="autoArrange('all')" :loading="arrangeLoading">一键完成</el-button>
|
||||||
<el-button v-if="selected.status === 'Draft'" :icon="Plus" @click="openSession()">批量安排场次</el-button>
|
<el-button v-if="selected.status === 'Draft'" :icon="Plus" @click="openSession()">批量安排场次</el-button>
|
||||||
<el-button v-if="selected.status === 'Draft'" type="primary" :icon="Promotion" @click="publishPlan">发布计划</el-button>
|
<el-button v-if="selected.status === 'Draft'" type="primary" :icon="Promotion" @click="publishPlan">发布计划</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="selected.status === 'Draft'"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:icon="Delete"
|
||||||
|
:loading="deletePlanLoading"
|
||||||
|
@click="deleteDraftPlan"
|
||||||
|
>删除草稿</el-button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="exam-filter-bar">
|
<div class="exam-filter-bar">
|
||||||
@@ -359,6 +427,14 @@ onMounted(async () => {
|
|||||||
<el-button link type="primary" @click="selectFilteredSessions">选择筛选结果</el-button>
|
<el-button link type="primary" @click="selectFilteredSessions">选择筛选结果</el-button>
|
||||||
<el-button link @click="clearSessionSelection">清空选择</el-button>
|
<el-button link @click="clearSessionSelection">清空选择</el-button>
|
||||||
<el-tag v-if="selectedSessionIds.length" type="info">已选 {{ selectedSessionIds.length }} 个</el-tag>
|
<el-tag v-if="selectedSessionIds.length" type="info">已选 {{ selectedSessionIds.length }} 个</el-tag>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:icon="Delete"
|
||||||
|
:loading="removeLoading"
|
||||||
|
:disabled="selectedSessionIds.length === 0"
|
||||||
|
@click="removeSelectedSessions"
|
||||||
|
>批量移除已选场次</el-button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="exam-timeline">
|
<div class="exam-timeline">
|
||||||
|
|||||||
Reference in New Issue
Block a user