206 lines
9.1 KiB
C#
206 lines
9.1 KiB
C#
using ClosedXML.Excel;
|
|
|
|
namespace Jiaowu.Api.Infrastructure.Timetables;
|
|
|
|
public static class TimetableExcelExporter
|
|
{
|
|
private static readonly string[] Weekdays =
|
|
["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"];
|
|
|
|
public static byte[] Create(TimetableData timetable)
|
|
{
|
|
using var workbook = new XLWorkbook();
|
|
var sheet = workbook.Worksheets.Add("课表");
|
|
sheet.Style.Font.FontName = "Microsoft YaHei";
|
|
|
|
sheet.Range("A1:H1").Merge();
|
|
sheet.Cell("A1").Value = $"{timetable.Subject.Name} · {timetable.Term.Name}课表";
|
|
sheet.Cell("A1").Style
|
|
.Font.SetBold()
|
|
.Font.SetFontSize(18)
|
|
.Font.SetFontColor(XLColor.White)
|
|
.Fill.SetBackgroundColor(XLColor.FromHtml("#173E72"))
|
|
.Alignment.SetHorizontal(XLAlignmentHorizontalValues.Left)
|
|
.Alignment.SetVertical(XLAlignmentVerticalValues.Center);
|
|
sheet.Row(1).Height = 34;
|
|
|
|
sheet.Range("A2:H2").Merge();
|
|
var activitySummary = string.Concat(
|
|
timetable.ExamEntries.Count > 0
|
|
? $" · 已融入 {timetable.ExamEntries.Count} 场考试"
|
|
: "",
|
|
timetable.ExperimentEntries.Count > 0
|
|
? $" · 已融入 {timetable.ExperimentEntries.Count} 场实验"
|
|
: "");
|
|
var hasOneOffActivities =
|
|
timetable.ExamEntries.Count > 0 ||
|
|
timetable.ExperimentEntries.Count > 0;
|
|
sheet.Cell("A2").Value = timetable.Plan is null
|
|
? hasOneOffActivities
|
|
? $"固定课表尚未发布{activitySummary} · 导出时间:{DateTime.Now:yyyy-MM-dd HH:mm}"
|
|
: "本学期暂无可用课表版本"
|
|
: $"版本:{timetable.Plan.Version} · 状态:{PlanStatus(timetable.Plan.Status)}{activitySummary} · 导出时间:{DateTime.Now:yyyy-MM-dd HH:mm}";
|
|
sheet.Cell("A2").Style
|
|
.Font.SetFontColor(XLColor.FromHtml("#52677A"))
|
|
.Fill.SetBackgroundColor(XLColor.FromHtml("#EFF3F6"));
|
|
sheet.Row(2).Height = 24;
|
|
|
|
var headerRow = 4;
|
|
var headers = new[] { "节次 / 时间" }.Concat(Weekdays.Skip(1)).ToArray();
|
|
for (var column = 1; column <= headers.Length; column++)
|
|
{
|
|
var cell = sheet.Cell(headerRow, column);
|
|
cell.Value = headers[column - 1];
|
|
cell.Style
|
|
.Font.SetBold()
|
|
.Font.SetFontColor(XLColor.White)
|
|
.Fill.SetBackgroundColor(XLColor.FromHtml("#24706A"))
|
|
.Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center)
|
|
.Alignment.SetVertical(XLAlignmentVerticalValues.Center);
|
|
}
|
|
sheet.Row(headerRow).Height = 26;
|
|
|
|
var timedEntries = timetable.Entries
|
|
.Concat(timetable.ExamEntries)
|
|
.Concat(timetable.ExperimentEntries)
|
|
.ToArray();
|
|
var periods = timetable.Slots.Select(x => x.PeriodNumber)
|
|
.Concat(timedEntries.SelectMany(x =>
|
|
Enumerable.Range(x.StartPeriod, x.PeriodCount)))
|
|
.Distinct()
|
|
.Order()
|
|
.ToArray();
|
|
if (periods.Length == 0) periods = Enumerable.Range(1, 8).ToArray();
|
|
for (var index = 0; index < periods.Length; index++)
|
|
{
|
|
var period = periods[index];
|
|
var row = headerRow + index + 1;
|
|
var slot = timetable.Slots.FirstOrDefault(x => x.PeriodNumber == period);
|
|
sheet.Cell(row, 1).Value = slot is null
|
|
? $"第 {period} 节"
|
|
: $"第 {period} 节\n{slot.StartsAt:HH\\:mm}-{slot.EndsAt:HH\\:mm}";
|
|
sheet.Cell(row, 1).Style
|
|
.Font.SetBold()
|
|
.Fill.SetBackgroundColor(XLColor.FromHtml("#F0F4F6"))
|
|
.Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center)
|
|
.Alignment.SetVertical(XLAlignmentVerticalValues.Center)
|
|
.Alignment.SetWrapText();
|
|
|
|
for (var day = 1; day <= 7; day++)
|
|
{
|
|
var entries = timedEntries
|
|
.Where(x =>
|
|
x.DayOfWeek == day &&
|
|
x.StartPeriod <= period &&
|
|
x.StartPeriod + x.PeriodCount - 1 >= period)
|
|
.ToList();
|
|
var cell = sheet.Cell(row, day + 1);
|
|
cell.Value = string.Join("\n\n", entries.Select(EntryText));
|
|
cell.Style
|
|
.Alignment.SetVertical(XLAlignmentVerticalValues.Top)
|
|
.Alignment.SetWrapText();
|
|
if (entries.Count > 0)
|
|
{
|
|
cell.Style
|
|
.Fill.SetBackgroundColor(XLColor.FromHtml(
|
|
entries.Any(x => x.IsExam)
|
|
? "#FFF1E8"
|
|
: entries.Any(x => x.IsExperiment)
|
|
? "#E7F5F1"
|
|
: "#E8F3F2"))
|
|
.Font.SetFontColor(XLColor.FromHtml("#173F4C"));
|
|
}
|
|
}
|
|
sheet.Row(row).Height = 64;
|
|
}
|
|
|
|
var grid = sheet.Range(
|
|
headerRow,
|
|
1,
|
|
headerRow + periods.Length,
|
|
headers.Length);
|
|
grid.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
|
grid.Style.Border.InsideBorderColor = XLColor.FromHtml("#D8E1E6");
|
|
grid.Style.Border.OutsideBorder = XLBorderStyleValues.Medium;
|
|
grid.Style.Border.OutsideBorderColor = XLColor.FromHtml("#AABBC5");
|
|
sheet.Column(1).Width = 16;
|
|
for (var column = 2; column <= 8; column++) sheet.Column(column).Width = 24;
|
|
sheet.SheetView.FreezeRows(headerRow);
|
|
|
|
if (timetable.FlexibleCourses.Count > 0)
|
|
{
|
|
var startRow = headerRow + periods.Length + 3;
|
|
sheet.Range(startRow, 1, startRow, 8).Merge();
|
|
sheet.Cell(startRow, 1).Value = "非排时课程(不占正常上课时间与场地)";
|
|
sheet.Cell(startRow, 1).Style
|
|
.Font.SetBold()
|
|
.Font.SetFontColor(XLColor.White)
|
|
.Fill.SetBackgroundColor(XLColor.FromHtml("#805C27"));
|
|
var row = startRow + 1;
|
|
foreach (var course in timetable.FlexibleCourses)
|
|
{
|
|
sheet.Range(row, 1, row, 8).Merge();
|
|
sheet.Cell(row, 1).Value =
|
|
$"{course.CourseCode} · {course.CourseName} · {string.Join('、', course.TeacherNames)} · 第 {course.StartWeek}-{course.EndWeek} 周 · 每周 {course.WeeklyHours} 学时";
|
|
sheet.Cell(row, 1).Style
|
|
.Fill.SetBackgroundColor(XLColor.FromHtml("#FFF8EA"))
|
|
.Alignment.SetWrapText();
|
|
row++;
|
|
}
|
|
}
|
|
|
|
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);
|
|
using var stream = new MemoryStream();
|
|
workbook.SaveAs(stream);
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static string EntryText(TimetableEntryDto entry) =>
|
|
entry.IsExperiment
|
|
? $"【实验】{entry.ExperimentProjectName}\n" +
|
|
$"{entry.CourseName} · {ArrangementMode(entry)}\n" +
|
|
$"{string.Join('、', entry.TeacherNames)}\n" +
|
|
$"{Location(entry)}\n" +
|
|
$"{entry.ExperimentDate:yyyy-MM-dd} · 第 {entry.StartPeriod}-" +
|
|
$"{entry.StartPeriod + entry.PeriodCount - 1} 节"
|
|
: entry.IsExam
|
|
? $"【考试】{entry.CourseName}\n" +
|
|
$"{entry.ExamPlanName}\n" +
|
|
$"{string.Join('、', entry.TeacherNames)}\n" +
|
|
$"{Location(entry)}\n" +
|
|
$"{entry.ExamDate:yyyy-MM-dd} · 第 {entry.StartPeriod}-" +
|
|
$"{entry.StartPeriod + entry.PeriodCount - 1} 节"
|
|
: $"{entry.CourseName}\n" +
|
|
$"{string.Join('、', entry.TeacherNames)}\n" +
|
|
$"{Location(entry)}\n" +
|
|
$"{entry.StartWeek}-{entry.EndWeek} 周";
|
|
|
|
private static string ArrangementMode(TimetableEntryDto entry) =>
|
|
entry.ExperimentArrangementMode switch
|
|
{
|
|
Domain.Academic.ExperimentArrangementMode.Centralized => "集中安排",
|
|
Domain.Academic.ExperimentArrangementMode.SelfScheduled => "自主预约",
|
|
_ => "实验安排"
|
|
};
|
|
|
|
private static string Location(TimetableEntryDto entry) =>
|
|
string.Join(" · ", new[]
|
|
{
|
|
entry.CampusName,
|
|
entry.BuildingName,
|
|
entry.ClassroomName
|
|
}.Where(x => !string.IsNullOrWhiteSpace(x)));
|
|
|
|
private static string PlanStatus(Domain.Academic.SchedulePlanStatus status) =>
|
|
status switch
|
|
{
|
|
Domain.Academic.SchedulePlanStatus.Draft => "草稿",
|
|
Domain.Academic.SchedulePlanStatus.Published => "已发布",
|
|
Domain.Academic.SchedulePlanStatus.Archived => "已归档",
|
|
_ => status.ToString()
|
|
};
|
|
}
|