using ClosedXML.Excel; namespace Jiaowu.Api.Infrastructure.Exams; public static class ExamSignInWorkbookExporter { private static readonly XLColor Navy = XLColor.FromHtml("#173E72"); private static readonly XLColor Teal = XLColor.FromHtml("#24706A"); private static readonly XLColor Pale = XLColor.FromHtml("#EFF3F6"); private static readonly XLColor Rule = XLColor.FromHtml("#C8D3DA"); public static byte[] Create(ExamSignInWorkbookData data) { using var workbook = new XLWorkbook(); AddSummarySheet(workbook, data); var usedNames = new HashSet(StringComparer.OrdinalIgnoreCase) { "考场汇总" }; foreach (var session in data.Sessions .OrderBy(x => x.ExamDate) .ThenBy(x => x.StartsAt) .ThenBy(x => x.BuildingName) .ThenBy(x => x.ClassroomName) .ThenBy(x => x.CourseCode)) { AddSignInSheet( workbook, data, session, UniqueSheetName(session, usedNames)); } using var stream = new MemoryStream(); workbook.SaveAs(stream); return stream.ToArray(); } private static void AddSummarySheet( XLWorkbook workbook, ExamSignInWorkbookData data) { var sheet = workbook.Worksheets.Add("考场汇总"); sheet.Style.Font.FontName = "Microsoft YaHei"; sheet.ShowGridLines = false; sheet.Range("A1:G1").Merge(); sheet.Cell("A1").Value = $"{data.PlanName} · 考场签名单"; StyleTitle(sheet.Range("A1:G1")); sheet.Row(1).Height = 34; sheet.Range("A2:G2").Merge(); sheet.Cell("A2").Value = $"{data.TermName} · 共 {data.Sessions.Count} 个场次 · 导出时间:{DateTime.Now:yyyy-MM-dd HH:mm}"; StyleSubTitle(sheet.Range("A2:G2")); sheet.Row(2).Height = 24; var headers = new[] { "日期", "时间", "考场", "课程", "教学班", "考生人数", "监考教师" }; for (var column = 1; column <= headers.Length; column++) sheet.Cell(4, column).Value = headers[column - 1]; StyleHeader(sheet.Range(4, 1, 4, headers.Length)); var row = 5; foreach (var session in data.Sessions .OrderBy(x => x.ExamDate) .ThenBy(x => x.StartsAt) .ThenBy(x => x.BuildingName) .ThenBy(x => x.ClassroomName)) { sheet.Cell(row, 1).Value = session.ExamDate.ToDateTime(TimeOnly.MinValue); sheet.Cell(row, 1).Style.DateFormat.Format = "yyyy-mm-dd"; sheet.Cell(row, 2).Value = $"{session.StartsAt:HH:mm}-{session.EndsAt:HH:mm}"; sheet.Cell(row, 3).Value = Location(session); sheet.Cell(row, 4).Value = $"{session.CourseCode} {session.CourseName}"; sheet.Cell(row, 5).Value = session.TaskNumber; sheet.Cell(row, 6).Value = session.Students.Count; sheet.Cell(row, 7).Value = Invigilators(session); row++; } if (row > 5) { var table = sheet.Range(4, 1, row - 1, headers.Length); table.Style.Border.InsideBorder = XLBorderStyleValues.Thin; table.Style.Border.InsideBorderColor = Rule; table.Style.Border.OutsideBorder = XLBorderStyleValues.Medium; table.Style.Border.OutsideBorderColor = Rule; table.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; sheet.Rows(5, row - 1).Height = 25; } sheet.Column(1).Width = 13; sheet.Column(2).Width = 15; sheet.Column(3).Width = 24; sheet.Column(4).Width = 30; sheet.Column(5).Width = 22; sheet.Column(6).Width = 11; sheet.Column(7).Width = 24; sheet.Columns(3, 7).Style.Alignment.WrapText = true; sheet.SheetView.FreezeRows(4); sheet.PageSetup.PageOrientation = XLPageOrientation.Landscape; sheet.PageSetup.PaperSize = XLPaperSize.A4Paper; sheet.PageSetup.FitToPages(1, 0); sheet.PageSetup.Margins .SetLeft(0.25).SetRight(0.25).SetTop(0.35).SetBottom(0.35); } private static void AddSignInSheet( XLWorkbook workbook, ExamSignInWorkbookData data, ExamSignInSessionData session, string sheetName) { var sheet = workbook.Worksheets.Add(sheetName); sheet.Style.Font.FontName = "Microsoft YaHei"; sheet.ShowGridLines = false; sheet.Range("A1:G1").Merge(); sheet.Cell("A1").Value = "考试考场签名单"; StyleTitle(sheet.Range("A1:G1")); sheet.Cell("A1").Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; sheet.Row(1).Height = 36; sheet.Range("A2:G2").Merge(); sheet.Cell("A2").Value = $"{data.PlanName} · {data.TermName}"; StyleSubTitle(sheet.Range("A2:G2")); sheet.Cell("A2").Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; sheet.Row(2).Height = 23; sheet.Range("A3:D3").Merge(); sheet.Cell("A3").Value = $"课程:{session.CourseCode} {session.CourseName}({session.TaskNumber})"; sheet.Range("E3:G3").Merge(); sheet.Cell("E3").Value = $"考生人数:{session.Students.Count} 人"; sheet.Range("A4:D4").Merge(); sheet.Cell("A4").Value = $"考试时间:{session.ExamDate:yyyy-MM-dd} {session.StartsAt:HH:mm}-{session.EndsAt:HH:mm}"; sheet.Range("E4:G4").Merge(); sheet.Cell("E4").Value = $"考场:{Location(session)}"; sheet.Range("A5:D5").Merge(); sheet.Cell("A5").Value = $"监考教师:{Invigilators(session)}"; sheet.Range("E5:G5").Merge(); sheet.Cell("E5").Value = "应到:______ 实到:______ 缺考:______"; var metadata = sheet.Range("A3:G5"); metadata.Style.Fill.BackgroundColor = XLColor.White; metadata.Style.Font.FontColor = XLColor.FromHtml("#304B60"); metadata.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; metadata.Style.Alignment.WrapText = true; metadata.Style.Border.BottomBorder = XLBorderStyleValues.Thin; metadata.Style.Border.BottomBorderColor = Rule; sheet.Rows(3, 5).Height = 24; var headers = new[] { "序号", "座位号", "学号", "姓名", "行政班", "考生签名", "备注" }; for (var column = 1; column <= headers.Length; column++) sheet.Cell(7, column).Value = headers[column - 1]; StyleHeader(sheet.Range(7, 1, 7, headers.Length)); sheet.Row(7).Height = 27; var row = 8; foreach (var student in session.Students .OrderBy(x => x.SeatNumber) .ThenBy(x => x.StudentNumber)) { var index = row - 7; sheet.Cell(row, 1).Value = index; sheet.Cell(row, 2).Value = (student.SeatNumber > 0 ? student.SeatNumber : index) .ToString("D3"); sheet.Cell(row, 3).Value = student.StudentNumber; sheet.Cell(row, 4).Value = student.Name; sheet.Cell(row, 5).Value = student.ClassName; sheet.Row(row).Height = 28; row++; } if (session.Students.Count == 0) { sheet.Range("A8:G8").Merge(); sheet.Cell("A8").Value = "本场次暂无考生"; sheet.Cell("A8").Style .Font.SetFontColor(XLColor.FromHtml("#8A5A20")) .Fill.SetBackgroundColor(XLColor.FromHtml("#FFF8EA")) .Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center); sheet.Row(8).Height = 28; row = 9; } var roster = sheet.Range(7, 1, row - 1, headers.Length); roster.Style.Border.InsideBorder = XLBorderStyleValues.Thin; roster.Style.Border.InsideBorderColor = Rule; roster.Style.Border.OutsideBorder = XLBorderStyleValues.Medium; roster.Style.Border.OutsideBorderColor = XLColor.FromHtml("#9CADB7"); roster.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; sheet.Range(8, 1, row - 1, 4).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; sheet.Range(8, 5, row - 1, 7).Style.Alignment.WrapText = true; sheet.Column(1).Width = 7; sheet.Column(2).Width = 10; sheet.Column(3).Width = 16; sheet.Column(4).Width = 12; sheet.Column(5).Width = 20; sheet.Column(6).Width = 22; sheet.Column(7).Width = 17; sheet.SheetView.FreezeRows(7); sheet.PageSetup.PageOrientation = XLPageOrientation.Portrait; sheet.PageSetup.PaperSize = XLPaperSize.A4Paper; sheet.PageSetup.FitToPages(1, 0); sheet.PageSetup.Margins .SetLeft(0.25).SetRight(0.25).SetTop(0.3).SetBottom(0.3); } private static void StyleTitle(IXLRange range) => range.Style .Font.SetBold() .Font.SetFontSize(18) .Font.SetFontColor(XLColor.White) .Fill.SetBackgroundColor(Navy) .Alignment.SetVertical(XLAlignmentVerticalValues.Center); private static void StyleSubTitle(IXLRange range) => range.Style .Font.SetFontColor(XLColor.FromHtml("#52677A")) .Fill.SetBackgroundColor(Pale) .Alignment.SetVertical(XLAlignmentVerticalValues.Center); private static void StyleHeader(IXLRange range) => range.Style .Font.SetBold() .Font.SetFontColor(XLColor.White) .Fill.SetBackgroundColor(Teal) .Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center) .Alignment.SetVertical(XLAlignmentVerticalValues.Center); private static string Location(ExamSignInSessionData session) => string.Join(" · ", new[] { session.BuildingName, session.ClassroomName }.Where(x => !string.IsNullOrWhiteSpace(x))) is { Length: > 0 } value ? value : "待分配考场"; private static string Invigilators(ExamSignInSessionData session) => session.InvigilatorNames.Count > 0 ? string.Join('、', session.InvigilatorNames) : "待分配"; private static string UniqueSheetName( ExamSignInSessionData session, ISet usedNames) { var location = string.IsNullOrWhiteSpace(session.ClassroomName) ? "待分配" : session.ClassroomName; var raw = $"{session.ExamDate:MMdd}-{location}-{session.CourseCode}"; var invalidCharacters = new HashSet(":\\/?*[]"); var sanitized = new string(raw .Select(character => invalidCharacters.Contains(character) ? '-' : character) .ToArray()).Trim(' ', '\''); if (sanitized.Length == 0) sanitized = "考场"; if (sanitized.Length > 31) sanitized = sanitized[..31]; var candidate = sanitized; var suffix = 2; while (!usedNames.Add(candidate)) { var marker = $"-{suffix++}"; var prefixLength = Math.Min(sanitized.Length, 31 - marker.Length); candidate = sanitized[..prefixLength] + marker; } return candidate; } } public sealed record ExamSignInWorkbookData( string PlanName, string TermName, IReadOnlyList Sessions); public sealed record ExamSignInSessionData( Guid SessionId, DateOnly ExamDate, DateTime StartsAt, DateTime EndsAt, string CourseCode, string CourseName, string TaskNumber, string? BuildingName, string? ClassroomName, IReadOnlyList InvigilatorNames, IReadOnlyList Students); public sealed record ExamSignInStudentData( Guid StudentId, string StudentNumber, string Name, string ClassName, int SeatNumber = 0);