Files
Academic-Affairs-System/src/Jiaowu.Api/Infrastructure/Exams/ExamSignInWorkbookExporter.cs
T
2026-07-27 19:31:19 +08:00

306 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string>(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<string> usedNames)
{
var location = string.IsNullOrWhiteSpace(session.ClassroomName)
? "待分配"
: session.ClassroomName;
var raw = $"{session.ExamDate:MMdd}-{location}-{session.CourseCode}";
var invalidCharacters = new HashSet<char>(":\\/?*[]");
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<ExamSignInSessionData> Sessions);
public sealed record ExamSignInSessionData(
Guid SessionId,
DateOnly ExamDate,
DateTime StartsAt,
DateTime EndsAt,
string CourseCode,
string CourseName,
string TaskNumber,
string? BuildingName,
string? ClassroomName,
IReadOnlyList<string> InvigilatorNames,
IReadOnlyList<ExamSignInStudentData> Students);
public sealed record ExamSignInStudentData(
Guid StudentId,
string StudentNumber,
string Name,
string ClassName,
int SeatNumber = 0);