using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using SSPCTester.Logic.Models; namespace SSPCTester.Logic.Report; /// 专业版固态功率控制器动态参数检测报告。 public sealed class ReportGenerator { private const string Navy = "#17324D"; private const string Blue = "#245B82"; private const string PaleBlue = "#EAF1F6"; private const string Line = "#CBD5DF"; private const string Muted = "#667585"; private const string Text = "#1E2933"; private const string Success = "#287A4B"; private const string SuccessBg = "#E8F3EC"; private const string Danger = "#A53232"; private const string DangerBg = "#F8EAEA"; private const string Warning = "#98651A"; private const string WarningBg = "#FAF1DE"; private const string NeutralBg = "#F1F3F5"; static ReportGenerator() => QuestPDF.Settings.License = LicenseType.Community; public Dictionary CurveImages { get; set; } = new(); public void Generate(Project project, string outputPath) { Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!); string reportNo = BuildReportNumber(project); Document.Create(document => { if (project.Report.Cover) document.Page(page => ComposeCover(page, project, reportNo)); document.Page(page => ComposeBody(page, project, reportNo)); }).GeneratePdf(outputPath); } private static void ComposeCover(PageDescriptor page, Project p, string reportNo) { page.Size(PageSizes.A4); page.MarginHorizontal(2.1f, Unit.Centimetre); page.MarginVertical(1.8f, Unit.Centimetre); page.DefaultTextStyle(x => x.FontFamily("Microsoft YaHei").FontSize(10).FontColor(Text)); page.Header().AlignRight().Text("SPC-DynaTest 检测系统") .FontSize(9).FontColor(Muted).LetterSpacing(0.05f); page.Content().Column(column => { //column.Item().Height(34).Background(Navy); column.Item().PaddingTop(54).AlignCenter().Text("固态功率控制器") .FontSize(22).Bold().FontColor(Navy); column.Item().PaddingTop(8).AlignCenter().Text("动态参数检测报告") .FontSize(30).Bold().FontColor(Navy).LetterSpacing(0.08f); // column.Item().PaddingTop(16).AlignCenter().Width(110) // .LineHorizontal(2).LineColor(Blue); column.Item().PaddingTop(45).Element(container => CoverIdentity(container, p, reportNo)); column.Item().PaddingTop(34).Element(container => OverallPanel(container, p)); column.Item().ExtendVertical(); column.Item().PaddingBottom(18).BorderTop(1).BorderColor(Line).PaddingTop(14).Table(table => { table.ColumnsDefinition(columns => { columns.RelativeColumn(); columns.RelativeColumn(); columns.RelativeColumn(); }); CoverSignCell(table, "编制", p.Site.Tester); CoverSignCell(table, "审核", ""); CoverSignCell(table, "批准", ""); }); }); page.Footer().Row(row => { row.RelativeItem().Text("本报告仅对所检样品及记录数据负责").FontSize(8).FontColor(Muted); row.ConstantItem(180).AlignRight().Text($"生成时间:{DateTime.Now:yyyy-MM-dd HH:mm}") .FontSize(8).FontColor(Muted); }); } private static void CoverIdentity(IContainer container, Project p, string reportNo) { container.Border(1).BorderColor(Line).Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(105); columns.RelativeColumn(); }); IdentityRow(table, "报告编号", reportNo, true); IdentityRow(table, "项目名称", ValueOrDash(p.Name)); IdentityRow(table, "设备编号", ValueOrDash(p.Dut.SerialNumber)); IdentityRow(table, "设备名称", ValueOrDash(p.Dut.ProductName)); IdentityRow(table, "设备型号", ValueOrDash(p.Dut.Model)); IdentityRow(table, "委托/生产单位", ValueOrDash(p.Dut.Manufacturer)); IdentityRow(table, "检测日期", p.Site.TestDate.ToString("yyyy年MM月dd日")); }); } private static void IdentityRow(TableDescriptor table, string label, string value, bool mono = false) { table.Cell().BorderBottom(1).BorderColor(Line).Background(PaleBlue) .PaddingVertical(8).PaddingHorizontal(10).Text(label).SemiBold().FontColor(Navy); var text = table.Cell().BorderBottom(1).BorderColor(Line) .PaddingVertical(8).PaddingHorizontal(12).Text(value); if (mono) text.FontFamily("Consolas"); } private static void CoverSignCell(TableDescriptor table, string label, string value) { table.Cell().PaddingRight(14).Column(column => { column.Item().Text(label).FontSize(9).FontColor(Muted); column.Item().PaddingTop(16).BorderBottom(1).BorderColor(Line) .PaddingBottom(4).Text(ValueOrDash(value)).FontSize(10); }); } private void ComposeBody(PageDescriptor page, Project p, string reportNo) { page.Size(PageSizes.A4); page.MarginHorizontal(1.5f, Unit.Centimetre); page.MarginVertical(1.3f, Unit.Centimetre); page.DefaultTextStyle(x => x.FontFamily("Microsoft YaHei").FontSize(8.7f).FontColor(Text)); page.Header().Element(container => BodyHeader(container, p, reportNo)); page.Content().PaddingVertical(10).Element(container => BodyContent(container, p)); page.Footer().Element(container => Footer(container, reportNo)); } private static void BodyHeader(IContainer container, Project p, string reportNo) { container.BorderBottom(1).BorderColor(Navy).PaddingBottom(7).Row(row => { row.RelativeItem().Column(column => { column.Item().Text("固态功率控制器动态参数检测报告") .FontSize(12).Bold().FontColor(Navy); column.Item().Text(ValueOrDash(p.Name)).FontSize(8).FontColor(Muted); }); row.ConstantItem(175).AlignRight().Column(column => { column.Item().AlignRight().Text($"报告编号:{reportNo}") .FontFamily("Consolas").FontSize(8).FontColor(Muted); column.Item().AlignRight().Text($"样品编号:{ValueOrDash(p.Dut.SerialNumber)}") .FontSize(8).FontColor(Muted); }); }); } private static void Footer(IContainer container, string reportNo) { container.BorderTop(0.7f).BorderColor(Line).PaddingTop(6).Row(row => { row.RelativeItem().Text($"SPC-DynaTest · {reportNo}").FontSize(7.5f).FontColor(Muted); row.RelativeItem().AlignCenter().Text("未经授权不得部分复制本报告") .FontSize(7.5f).FontColor(Muted); row.RelativeItem().AlignRight() .DefaultTextStyle(style => style.FontSize(7.5f).FontColor(Muted)) .Text(text => { text.Span("第 "); text.CurrentPageNumber(); text.Span(" 页 / 共 "); text.TotalPages(); text.Span(" 页"); }); }); } private void BodyContent(IContainer container, Project p) { container.Column(column => { column.Spacing(12); int section = 1; if (p.Report.Cover) { Section(column, section++, "检测概况", "TEST OVERVIEW"); column.Item().Element(x => Overview(x, p)); Section(column, section++, "结果摘要", "RESULT SUMMARY"); column.Item().Element(x => ResultSummary(x, p)); } if (p.Report.Basic && (p.BasicResults.Count > 0 || HasNote(p.TestFunction?.Basic))) { Section(column, section++, "基础测试", "BASIC FUNCTION TEST"); if (p.BasicResults.Count > 0) { column.Item().Element(x => ModuleSummary(x, "通道开合、电压、电流与关后残压", p.BasicResults.Select(r => r.Status))); column.Item().Element(x => BasicTable(x, p)); } ManualNote(column, p.TestFunction?.Basic); } if (p.Report.Curve && (p.CurveResults.Count > 0 || HasNote(p.TestFunction?.Curve))) { Section(column, section++, "开启与关断曲线", "SWITCHING CHARACTERISTICS"); if (p.CurveResults.Count > 0) { string criteria = $"判据:开启≤{p.CurveThresholds.MaxOnTimeMs:F1}ms,上升≤{p.CurveThresholds.MaxRiseTimeMs:F1}ms,关断≤{p.CurveThresholds.MaxOffTimeMs:F1}ms,下降≤{p.CurveThresholds.MaxFallTimeMs:F1}ms"; column.Item().Element(x => ModuleSummary(x, criteria, p.CurveResults.Select(r => r.Status))); column.Item().Element(x => CurveTable(x, p)); } ManualNote(column, p.TestFunction?.Curve); foreach (var (channel, images) in CurveImages.OrderBy(x => x.Key)) { AddCurveImage(column, channel, "ON", images.Rising); AddCurveImage(column, channel, "OFF", images.Falling); } } if (p.Report.Power && (p.PowerResults.Count > 0 || HasNote(p.TestFunction?.Power))) { Section(column, section++, "功率损耗", "POWER LOSS"); if (p.PowerResults.Count > 0) { column.Item().Element(x => ModuleSummary(x, "采集输入/输出两端电压与电流,计算功率损耗与效率", p.PowerResults.Select(r => r.Status))); column.Item().Element(x => PowerTable(x, p)); } ManualNote(column, p.TestFunction?.Power); } if (p.Report.Protect && (p.ProtectResults.Count > 0 || HasNote(p.TestFunction?.Protect))) { Section(column, section++, "保护功能", "PROTECTION FUNCTION"); if (p.ProtectResults.Count > 0) { column.Item().Element(x => ModuleSummary(x, "跳闸、短路、过温、过压、欠压及限流保护", p.ProtectResults.Select(r => r.Status))); column.Item().Element(x => ProtectTable(x, p)); } ManualNote(column, p.TestFunction?.Protect); } column.Item().PaddingTop(12).Element(x => Conclusion(x, p)); }); } private static void Section(ColumnDescriptor column, int number, string title, string english) { column.Item().PaddingTop(4).BorderBottom(1.3f).BorderColor(Navy).PaddingBottom(5).Row(row => { row.ConstantItem(34).Height(25).Background(Navy).AlignCenter().AlignMiddle() .Text(number.ToString("00")).Bold().FontColor(Colors.White).FontFamily("Consolas"); row.RelativeItem().PaddingLeft(10).AlignMiddle().Text(title) .FontSize(13).Bold().FontColor(Navy); row.ConstantItem(180).AlignRight().AlignMiddle().Text(english) .FontSize(7.5f).FontColor(Muted).LetterSpacing(0.05f); }); } private static void AddCurveImage(ColumnDescriptor column, int channel, string phaseLabel, byte[]? png) { if (png == null || png.Length == 0) return; column.Item().ShowEntire().Border(1).BorderColor(Line).Padding(8).Column(imageColumn => { imageColumn.Item().Text($"CH{channel:00} {phaseLabel} Curve").SemiBold().FontColor(Navy); imageColumn.Item().PaddingTop(6).Image(png).FitWidth(); }); } private static void Overview(IContainer container, Project p) { container.Table(table => { table.ColumnsDefinition(columns => { columns.ConstantColumn(82); columns.RelativeColumn(); columns.ConstantColumn(82); columns.RelativeColumn(); }); OverviewCell(table, "设备编号", p.Dut.SerialNumber); OverviewCell(table, "设备名称", p.Dut.ProductName); OverviewCell(table, "设备型号", p.Dut.Model); OverviewCell(table, "生产厂家", p.Dut.Manufacturer); OverviewCell(table, "测试人员", p.Site.Tester); OverviewCell(table, "测试日期", p.Site.TestDate.ToString("yyyy-MM-dd")); OverviewCell(table, "环境温度", $"{p.Site.TemperatureC:F1} ℃"); OverviewCell(table, "相对湿度", $"{p.Site.HumidityPct:F1} %RH"); OverviewCell(table, "环境气压", $"{p.Site.PressureKpa:F1} kPa"); OverviewCell(table, "项目创建", p.CreatedAt.ToString("yyyy-MM-dd HH:mm")); table.Cell().Border(0.6f).BorderColor(Line).Background(PaleBlue).Padding(6) .Text("测试备注").SemiBold().FontColor(Navy); table.Cell().ColumnSpan(3).Border(0.6f).BorderColor(Line).Padding(6) .Text(ValueOrDash(p.Site.Remark)); }); } private static void OverviewCell(TableDescriptor table, string label, string? value) { table.Cell().Border(0.6f).BorderColor(Line).Background(PaleBlue) .Padding(6).Text(label).SemiBold().FontColor(Navy); table.Cell().Border(0.6f).BorderColor(Line).Padding(6).Text(ValueOrDash(value)); } private static void ResultSummary(IContainer container, Project p) { var statuses = p.BasicResults.Select(x => x.Status) .Concat(p.CurveResults.Select(x => x.Status)) .Concat(p.PowerResults.Select(x => x.Status)) .Concat(p.ProtectResults.Select(x => x.Status)).ToArray(); container.Row(row => { row.RelativeItem().Element(x => SummaryCard(x, "检测项目", statuses.Length.ToString(), Navy, PaleBlue)); row.RelativeItem().PaddingLeft(7).Element(x => SummaryCard(x, "合格", statuses.Count(s => s == TestStatus.Pass).ToString(), Success, SuccessBg)); row.RelativeItem().PaddingLeft(7).Element(x => SummaryCard(x, "不合格", statuses.Count(s => s == TestStatus.Fail).ToString(), Danger, DangerBg)); row.RelativeItem().PaddingLeft(7).Element(x => SummaryCard(x, "已测", statuses.Count(s => s == TestStatus.Measured).ToString(), Blue, PaleBlue)); row.RelativeItem().PaddingLeft(7).Element(x => SummaryCard(x, "未测试", statuses.Count(s => s == TestStatus.Untested).ToString(), Muted, NeutralBg)); row.RelativeItem().PaddingLeft(7).Element(x => SummaryCard(x, "综合判定", StatusText(p.OverallStatus), StatusColor(p.OverallStatus), StatusBackground(p.OverallStatus))); }); } private static void SummaryCard(IContainer container, string title, string value, string color, string background) { container.MinHeight(58).Border(0.8f).BorderColor(color).Background(background) .Padding(8).Column(column => { column.Item().Text(title).FontSize(7.5f).FontColor(Muted); column.Item().PaddingTop(4).Text(value).FontSize(13).Bold().FontColor(color); }); } private static void ModuleSummary(IContainer container, string description, IEnumerable source) { var statuses = source.ToArray(); container.Background(PaleBlue).BorderLeft(3).BorderColor(Blue).Padding(7).Row(row => { row.RelativeItem().Text(description).FontSize(8).FontColor(Muted); row.ConstantItem(190).AlignRight() .DefaultTextStyle(style => style.FontSize(8)) .Text(text => { text.Span($"合格 {statuses.Count(x => x == TestStatus.Pass)}").FontColor(Success).SemiBold(); text.Span(" "); text.Span($"不合格 {statuses.Count(x => x == TestStatus.Fail)}").FontColor(Danger).SemiBold(); text.Span(" "); text.Span($"已测 {statuses.Count(x => x == TestStatus.Measured)}").FontColor(Blue).SemiBold(); text.Span(" "); text.Span($"未测 {statuses.Count(x => x == TestStatus.Untested)}").FontColor(Muted); }); }); } private static void BasicTable(IContainer container, Project p) => DataTable(container, new[] { 0.65f, 1f, 1f, 1.15f, 1.15f, 0.8f }, new[] { "通道", "导通电压\nV", "导通电流\nA", "关后电压\nV", "关后电流\nA", "判定" }, p.BasicResults.Select(r => new ReportRow(r.Status, new[] { $"CH{r.Channel:00}", Format(r.VoltageOn, "F2"), Format(r.CurrentOn, "F3"), Format(r.VoltageOff, "F2"), Format(r.CurrentOff, "F3"), StatusText(r.Status) }))); private static void CurveTable(IContainer container, Project p) => DataTable(container, new[] { 0.62f, 1f, 1f, 1f, 1f, 0.76f }, new[] { "通道", "开启时间\nms", "上升时间\nms", "关断时间\nms", "下降时间\nms", "判定" }, p.CurveResults.Select(r => new ReportRow(r.Status, new[] { $"CH{r.Channel:00}", Format(r.OnTimeMs, "F3"), Format(r.RiseTimeMs, "F3"), Format(r.OffTimeMs, "F3"), Format(r.FallTimeMs, "F3"), StatusText(r.Status) }))); private static void PowerTable(IContainer container, Project p) => DataTable(container, new[] { 0.62f, 0.9f, 0.9f, 0.9f, 0.9f, 0.9f, 0.9f, 0.76f }, new[] { "通道", "输入电压\nV", "输入电流\nA", "输出电压\nV", "输出电流\nA", "功率损耗\nW", "效率\n%", "状态" }, p.PowerResults.Select(r => new ReportRow(r.Status, new[] { $"CH{r.Channel:00}", Format(r.Vin, "F3"), Format(r.Iin, "F3"), Format(r.Vout, "F3"), Format(r.Iout, "F3"), Format(r.PowerLossW, "F3"), Format(r.EfficiencyPct, "F3"), StatusText(r.Status) }))); private static void ProtectTable(IContainer container, Project p) => DataTable(container, new[] { 0.55f, 1.1f, 1.45f, 1f, 0.95f, 0.75f }, new[] { "序号", "保护类型", "触发条件", "实测值", "判别模式", "判别结果" }, p.ProtectResults.Select(r => new ReportRow(r.Status, new[] { r.Index > 0 ? r.Index.ToString() : "—", ValueOrDash(r.ProtectType), ValueOrDash(r.Trigger), ValueOrDash(r.MeasuredValue), ValueOrDash(r.JudgeMode), ProtectStatusText(r.Status) }))); private static void DataTable(IContainer container, IReadOnlyList widths, IReadOnlyList headers, IEnumerable source) { var rows = source.ToArray(); container.Table(table => { table.ColumnsDefinition(columns => { foreach (float width in widths) columns.RelativeColumn(width); }); table.Header(header => { foreach (string text in headers) header.Cell().Background(Navy).BorderRight(0.5f).BorderColor(Colors.White) .PaddingVertical(6).PaddingHorizontal(4).AlignCenter().AlignMiddle() .Text(text).FontSize(7.6f).SemiBold().FontColor(Colors.White); }); for (int rowIndex = 0; rowIndex < rows.Length; rowIndex++) { var row = rows[rowIndex]; string background = row.Status == TestStatus.Fail ? DangerBg : rowIndex % 2 == 0 ? Colors.White : "#F7F9FB"; for (int columnIndex = 0; columnIndex < row.Values.Length; columnIndex++) { var text = table.Cell().Background(background).BorderBottom(0.55f).BorderColor(Line) .PaddingVertical(4.5f).PaddingHorizontal(4).AlignCenter().AlignMiddle() .Text(row.Values[columnIndex]).FontSize(7.8f); if (columnIndex == row.Values.Length - 1) text.SemiBold().FontColor(StatusColor(row.Status)); else if (columnIndex == 0) text.FontFamily("Consolas").SemiBold().FontColor(Navy); } } }); } private static void ManualNote(ColumnDescriptor column, string? note) { if (!HasNote(note)) return; column.Item().ShowEntire().Border(0.8f).BorderColor(Line).Background("#F8FAFC") .Padding(8).Column(noteColumn => { noteColumn.Item().Text("测试说明 / 现场记录").FontSize(8).SemiBold().FontColor(Navy); noteColumn.Item().PaddingTop(5).Text(note!.Trim()).FontSize(8.2f).LineHeight(1.35f); }); } private static void Conclusion(IContainer container, Project p) { container.ShowEntire().Border(1.2f).BorderColor(StatusColor(p.OverallStatus)) .Background(StatusBackground(p.OverallStatus)).Padding(12).Column(column => { column.Item().Row(row => { row.RelativeItem().Text("检测结论").FontSize(11).Bold().FontColor(Navy); row.ConstantItem(120).AlignRight().Text(StatusText(p.OverallStatus)) .FontSize(13).Bold().FontColor(StatusColor(p.OverallStatus)); }); column.Item().PaddingTop(8).Text(ConclusionText(p.OverallStatus)) .FontSize(8.5f).FontColor(Text).LineHeight(1.4f); column.Item().PaddingTop(18).Row(row => { row.RelativeItem().Text($"检测:{ValueOrDash(p.Site.Tester)}").FontSize(8).FontColor(Muted); row.RelativeItem().Text("复核:________________").FontSize(8).FontColor(Muted); row.RelativeItem().AlignRight().Text("日期:____年__月__日").FontSize(8).FontColor(Muted); }); }); } private static void OverallPanel(IContainer container, Project p) { container.Border(1.2f).BorderColor(StatusColor(p.OverallStatus)) .Background(StatusBackground(p.OverallStatus)).Padding(14).Row(row => { row.RelativeItem().Column(column => { column.Item().Text("综合检测结论").FontSize(9).FontColor(Muted); column.Item().PaddingTop(4).Text(ConclusionText(p.OverallStatus)).FontSize(9); }); row.ConstantItem(110).AlignRight().AlignMiddle().Text(StatusText(p.OverallStatus)) .FontSize(18).Bold().FontColor(StatusColor(p.OverallStatus)); }); } private static string BuildReportNumber(Project p) { string serial = new(p.Dut.SerialNumber.Where(char.IsLetterOrDigit).ToArray()); if (string.IsNullOrWhiteSpace(serial)) serial = "UNKNOWN"; if (serial.Length > 14) serial = serial[^14..]; return $"SPC-{p.Site.TestDate:yyyyMMdd}-{serial.ToUpperInvariant()}"; } private static string ConclusionText(TestStatus status) => status switch { TestStatus.Pass => "本次所选检测项目均满足当前判定要求,综合结论为合格。", TestStatus.Fail => "本次检测存在不合格项目,综合结论为不合格;请结合异常通道记录进行复核与处置。", TestStatus.Running => "检测尚未结束,当前结果仅供过程参考,不作为最终判定依据。", TestStatus.Measured => "功率损耗已完成测量,报告基于输入/输出两端数据计算损耗与效率。", _ => "检测数据尚不完整,暂不具备出具最终合格判定的条件。" }; private static string Format(double value, string format) => double.IsNaN(value) || double.IsInfinity(value) ? "—" : value.ToString(format); private static bool HasNote(string? value) => !string.IsNullOrWhiteSpace(value); private static string ValueOrDash(string? value) => string.IsNullOrWhiteSpace(value) ? "—" : value.Trim(); private static string StatusText(TestStatus status) => status switch { TestStatus.Pass => "合格", TestStatus.Fail => "不合格", TestStatus.Running => "测试中", TestStatus.Measured => "已测", _ => "未测试" }; private static string ProtectStatusText(TestStatus status) => status switch { TestStatus.Pass => "合格", TestStatus.Fail => "不合格", TestStatus.Running => "测试中", _ => "未判定" }; private static string StatusColor(TestStatus status) => status switch { TestStatus.Pass => Success, TestStatus.Fail => Danger, TestStatus.Running => Warning, TestStatus.Measured => Blue, _ => Muted }; private static string StatusBackground(TestStatus status) => status switch { TestStatus.Pass => SuccessBg, TestStatus.Fail => DangerBg, TestStatus.Running => WarningBg, TestStatus.Measured => PaleBlue, _ => NeutralBg }; private sealed record ReportRow(TestStatus Status, string[] Values); }