课表优化
This commit is contained in:
1 parent
e8482b26f2
commit
cd4ffa1d85
7 files changed
+568
-33
No files matched your search
@@ -171,6 +171,77 @@ public sealed class TimetableManagementController(
|
|||||||
return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
|
return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("export/batch.xlsx")]
|
||||||
|
public async Task<ActionResult> ExportBatch(
|
||||||
|
TimetableBatchExportRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var resourceIds = request.ResourceIds.Distinct().ToArray();
|
||||||
|
if (resourceIds.Length is 0 or > 100)
|
||||||
|
return ValidationProblem("请选择 1 至 100 个课表对象进行导出。", statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
|
var timetables = new List<TimetableData>(resourceIds.Length);
|
||||||
|
foreach (var resourceId in resourceIds)
|
||||||
|
{
|
||||||
|
var result = await LoadAuthorizedAsync(
|
||||||
|
request.ResourceType,
|
||||||
|
resourceId,
|
||||||
|
request.AcademicTermId,
|
||||||
|
request.SchedulePlanId,
|
||||||
|
cancellationToken);
|
||||||
|
if (result.Result is null) return result.Error!;
|
||||||
|
timetables.Add(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = TimetableExcelExporter.Create(timetables);
|
||||||
|
var fileName = $"课表批量导出-{FileName(timetables[0].Term.Name)}.xlsx";
|
||||||
|
return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("export.pdf")]
|
||||||
|
public async Task<ActionResult> ExportPdf(
|
||||||
|
TimetableResourceType resourceType,
|
||||||
|
Guid resourceId,
|
||||||
|
Guid academicTermId,
|
||||||
|
Guid? schedulePlanId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await LoadAuthorizedAsync(
|
||||||
|
resourceType,
|
||||||
|
resourceId,
|
||||||
|
academicTermId,
|
||||||
|
schedulePlanId,
|
||||||
|
cancellationToken);
|
||||||
|
if (result.Result is null) return result.Error!;
|
||||||
|
return PdfFile(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("export/batch.pdf")]
|
||||||
|
public async Task<ActionResult> ExportBatchPdf(
|
||||||
|
TimetableBatchExportRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var resourceIds = request.ResourceIds.Distinct().ToArray();
|
||||||
|
if (resourceIds.Length is 0 or > 100)
|
||||||
|
return ValidationProblem("请选择 1 至 100 个课表对象进行导出。", statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
|
var timetables = new List<TimetableData>(resourceIds.Length);
|
||||||
|
foreach (var resourceId in resourceIds)
|
||||||
|
{
|
||||||
|
var result = await LoadAuthorizedAsync(
|
||||||
|
request.ResourceType,
|
||||||
|
resourceId,
|
||||||
|
request.AcademicTermId,
|
||||||
|
request.SchedulePlanId,
|
||||||
|
cancellationToken);
|
||||||
|
if (result.Result is null) return result.Error!;
|
||||||
|
timetables.Add(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = $"课表批量导出-{FileName(timetables[0].Term.Name)}.pdf";
|
||||||
|
return File(TimetablePdfExporter.Create(timetables), "application/pdf", fileName);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<(TimetableData? Result, ActionResult? Error)> LoadAuthorizedAsync(
|
private async Task<(TimetableData? Result, ActionResult? Error)> LoadAuthorizedAsync(
|
||||||
TimetableResourceType resourceType,
|
TimetableResourceType resourceType,
|
||||||
Guid resourceId,
|
Guid resourceId,
|
||||||
@@ -205,8 +276,20 @@ public sealed class TimetableManagementController(
|
|||||||
value = value.Replace(character, '-');
|
value = value.Replace(character, '-');
|
||||||
return value.Trim();
|
return value.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ActionResult PdfFile(TimetableData timetable) =>
|
||||||
|
File(
|
||||||
|
TimetablePdfExporter.Create(timetable),
|
||||||
|
"application/pdf",
|
||||||
|
$"{FileName(timetable.Subject.Name)}-{FileName(timetable.Term.Name)}-课表.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record TimetableBatchExportRequest(
|
||||||
|
TimetableResourceType ResourceType,
|
||||||
|
[property: Required, MinLength(1), MaxLength(100)] IReadOnlyCollection<Guid> ResourceIds,
|
||||||
|
Guid AcademicTermId,
|
||||||
|
Guid? SchedulePlanId);
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/timetables")]
|
[Route("api/timetables")]
|
||||||
public sealed class FreeClassroomsController(
|
public sealed class FreeClassroomsController(
|
||||||
|
|||||||
@@ -100,6 +100,21 @@ public sealed class TimetablesController(
|
|||||||
return ExcelFile(result);
|
return ExcelFile(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("teachers/{teacherId:guid}/export.pdf")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<ActionResult> ExportTeacherTimetablePdf(
|
||||||
|
Guid teacherId,
|
||||||
|
Guid? academicTermId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await GetPublishedTimetableAsync(
|
||||||
|
TimetableResourceType.Teacher,
|
||||||
|
teacherId,
|
||||||
|
academicTermId,
|
||||||
|
cancellationToken);
|
||||||
|
return result is null ? NotFound() : PdfFile(result);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("classes/{classId:guid}/export.xlsx")]
|
[HttpGet("classes/{classId:guid}/export.xlsx")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<ActionResult> ExportClassTimetable(
|
public async Task<ActionResult> ExportClassTimetable(
|
||||||
@@ -116,6 +131,21 @@ public sealed class TimetablesController(
|
|||||||
return ExcelFile(result);
|
return ExcelFile(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("classes/{classId:guid}/export.pdf")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<ActionResult> ExportClassTimetablePdf(
|
||||||
|
Guid classId,
|
||||||
|
Guid? academicTermId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await GetPublishedTimetableAsync(
|
||||||
|
TimetableResourceType.Class,
|
||||||
|
classId,
|
||||||
|
academicTermId,
|
||||||
|
cancellationToken);
|
||||||
|
return result is null ? NotFound() : PdfFile(result);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("mine")]
|
[HttpGet("mine")]
|
||||||
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
|
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
|
||||||
public async Task<ActionResult> GetMyTimetable(
|
public async Task<ActionResult> GetMyTimetable(
|
||||||
@@ -227,6 +257,39 @@ public sealed class TimetablesController(
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("mine/export.pdf")]
|
||||||
|
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
|
||||||
|
public async Task<ActionResult> ExportMyTimetablePdf(
|
||||||
|
Guid? academicTermId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var student = await db.Students.AsNoTracking()
|
||||||
|
.Where(x => x.UserId == userId)
|
||||||
|
.Select(x => new { x.Id, x.AdministrativeClassId, x.StudentNumber, x.Name })
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
if (student is not null)
|
||||||
|
{
|
||||||
|
var result = await timetableDataService.BuildAsync(
|
||||||
|
TimetableResourceType.Class, student.AdministrativeClassId, academicTermId, null,
|
||||||
|
false, student.Id, new TimetableStudentDto(student.StudentNumber, student.Name),
|
||||||
|
cancellationToken);
|
||||||
|
return result is null ? NotFound() : PdfFile(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
var teacher = await db.Teachers.AsNoTracking()
|
||||||
|
.Where(x => x.UserId == userId && x.Status == TeacherStatus.Active)
|
||||||
|
.Select(x => new { x.Id })
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
if (teacher is null) return NotFound();
|
||||||
|
var teacherResult = await timetableDataService.BuildAsync(
|
||||||
|
TimetableResourceType.Teacher, teacher.Id, academicTermId, null, false, null, null,
|
||||||
|
cancellationToken);
|
||||||
|
return teacherResult is null ? NotFound() : PdfFile(teacherResult);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("mine/calendar-subscription")]
|
[HttpGet("mine/calendar-subscription")]
|
||||||
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
|
[Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
|
||||||
public async Task<ActionResult> GetMyCalendarSubscription(
|
public async Task<ActionResult> GetMyCalendarSubscription(
|
||||||
@@ -450,6 +513,13 @@ public sealed class TimetablesController(
|
|||||||
return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
|
return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ActionResult PdfFile(TimetableData result)
|
||||||
|
{
|
||||||
|
var fileName =
|
||||||
|
$"{SafeFileName(result.Subject.Name)}-{SafeFileName(result.Term.Name)}-课表.pdf";
|
||||||
|
return File(TimetablePdfExporter.Create(result), "application/pdf", fileName);
|
||||||
|
}
|
||||||
|
|
||||||
private static string SafeFileName(string value)
|
private static string SafeFileName(string value)
|
||||||
{
|
{
|
||||||
foreach (var character in Path.GetInvalidFileNameChars())
|
foreach (var character in Path.GetInvalidFileNameChars())
|
||||||
|
|||||||
@@ -10,7 +10,33 @@ public static class TimetableExcelExporter
|
|||||||
public static byte[] Create(TimetableData timetable)
|
public static byte[] Create(TimetableData timetable)
|
||||||
{
|
{
|
||||||
using var workbook = new XLWorkbook();
|
using var workbook = new XLWorkbook();
|
||||||
var sheet = workbook.Worksheets.Add("课表");
|
CreateWorksheet(workbook, timetable, "课表");
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
workbook.SaveAs(stream);
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Create(IReadOnlyCollection<TimetableData> timetables)
|
||||||
|
{
|
||||||
|
if (timetables.Count == 0)
|
||||||
|
throw new ArgumentException("至少需要一张课表。", nameof(timetables));
|
||||||
|
|
||||||
|
using var workbook = new XLWorkbook();
|
||||||
|
var index = 1;
|
||||||
|
foreach (var timetable in timetables)
|
||||||
|
CreateWorksheet(workbook, timetable, WorksheetName(timetable.Subject.Name, index++));
|
||||||
|
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
workbook.SaveAs(stream);
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateWorksheet(
|
||||||
|
XLWorkbook workbook,
|
||||||
|
TimetableData timetable,
|
||||||
|
string worksheetName)
|
||||||
|
{
|
||||||
|
var sheet = workbook.Worksheets.Add(worksheetName);
|
||||||
sheet.Style.Font.FontName = "Microsoft YaHei";
|
sheet.Style.Font.FontName = "Microsoft YaHei";
|
||||||
|
|
||||||
sheet.Range("A1:H1").Merge();
|
sheet.Range("A1:H1").Merge();
|
||||||
@@ -153,9 +179,17 @@ public static class TimetableExcelExporter
|
|||||||
sheet.PageSetup.PaperSize = XLPaperSize.A4Paper;
|
sheet.PageSetup.PaperSize = XLPaperSize.A4Paper;
|
||||||
sheet.PageSetup.FitToPages(1, 0);
|
sheet.PageSetup.FitToPages(1, 0);
|
||||||
sheet.PageSetup.Margins.SetLeft(0.25).SetRight(0.25).SetTop(0.35).SetBottom(0.35);
|
sheet.PageSetup.Margins.SetLeft(0.25).SetRight(0.25).SetTop(0.35).SetBottom(0.35);
|
||||||
using var stream = new MemoryStream();
|
}
|
||||||
workbook.SaveAs(stream);
|
|
||||||
return stream.ToArray();
|
private static string WorksheetName(string subjectName, int index)
|
||||||
|
{
|
||||||
|
var suffix = $"-{index}";
|
||||||
|
var name = string.Concat(subjectName.Select(character =>
|
||||||
|
"[]:*?/\\".Contains(character) ? '-' : character)).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(name)) name = "课表";
|
||||||
|
return name.Length > 31 - suffix.Length
|
||||||
|
? name[..(31 - suffix.Length)] + suffix
|
||||||
|
: name + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string EntryText(TimetableEntryDto entry) =>
|
private static string EntryText(TimetableEntryDto entry) =>
|
||||||
|
|||||||
Loaded 3 of 7 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user