2026-06-30 12:10:29 +08:00
|
|
|
|
using QuestPDF.Fluent;
|
|
|
|
|
|
using QuestPDF.Helpers;
|
|
|
|
|
|
using QuestPDF.Infrastructure;
|
|
|
|
|
|
using SSPCTester.Logic.Models;
|
|
|
|
|
|
|
|
|
|
|
|
namespace SSPCTester.Logic.Report;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 用 QuestPDF 生成测试报告 PDF。按 Project.Report 勾选拼装章节。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
internal sealed class LegacyReportGenerator
|
|
|
|
|
|
{
|
|
|
|
|
|
static LegacyReportGenerator()
|
|
|
|
|
|
{
|
|
|
|
|
|
QuestPDF.Settings.License = LicenseType.Community;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>外部传入曲线图 PNG,键 = 通道号。</summary>
|
2026-07-01 20:04:27 +08:00
|
|
|
|
public Dictionary<int, CurveReportImages> CurveImages { get; set; } = new();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
|
|
|
|
|
|
public void Generate(Project project, string outputPath)
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
|
|
|
|
|
|
var sel = project.Report;
|
|
|
|
|
|
Document.Create(container =>
|
|
|
|
|
|
{
|
|
|
|
|
|
container.Page(page =>
|
|
|
|
|
|
{
|
|
|
|
|
|
page.Size(PageSizes.A4);
|
|
|
|
|
|
page.Margin(2, Unit.Centimetre);
|
|
|
|
|
|
page.DefaultTextStyle(t => t.FontFamily("Microsoft YaHei").FontSize(10));
|
|
|
|
|
|
|
|
|
|
|
|
page.Header().Element(h => Header(h, project));
|
|
|
|
|
|
page.Content().Element(c => Content(c, project, sel));
|
|
|
|
|
|
page.Footer().AlignCenter().Text(t =>
|
|
|
|
|
|
{
|
|
|
|
|
|
t.Span("第 ");
|
|
|
|
|
|
t.CurrentPageNumber();
|
|
|
|
|
|
t.Span(" / ");
|
|
|
|
|
|
t.TotalPages();
|
|
|
|
|
|
t.Span(" 页");
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}).GeneratePdf(outputPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void Header(IContainer h, Project p)
|
|
|
|
|
|
{
|
|
|
|
|
|
h.Column(col =>
|
|
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
col.Item().Text($"固态功率控制器动态参数检测报告").FontSize(16).Bold();
|
2026-06-30 12:10:29 +08:00
|
|
|
|
col.Item().Text($"项目:{p.Name} 生成时间:{DateTime.Now:yyyy-MM-dd HH:mm}").FontSize(9).FontColor(Colors.Grey.Darken2);
|
|
|
|
|
|
col.Item().PaddingTop(4).LineHorizontal(0.5f).LineColor(Colors.Grey.Lighten2);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void Content(IContainer c, Project p, ReportSelection sel)
|
|
|
|
|
|
{
|
|
|
|
|
|
c.Column(col =>
|
|
|
|
|
|
{
|
|
|
|
|
|
col.Spacing(14);
|
|
|
|
|
|
|
|
|
|
|
|
if (sel.Cover)
|
|
|
|
|
|
{
|
|
|
|
|
|
Section(col, "1. 被测件信息");
|
|
|
|
|
|
col.Item().Element(e => DutTable(e, p));
|
|
|
|
|
|
Section(col, "2. 测试信息");
|
|
|
|
|
|
col.Item().Element(e => SiteTable(e, p));
|
|
|
|
|
|
Section(col, "3. 综合判定");
|
|
|
|
|
|
col.Item().Element(e => OverallBadge(e, p.OverallStatus));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (sel.Basic && (p.BasicResults.Count > 0 || HasManualNote(p.TestFunction?.Basic)))
|
|
|
|
|
|
{
|
|
|
|
|
|
Section(col, "4. 基础测试结果(24 路)");
|
|
|
|
|
|
if (p.BasicResults.Count > 0) col.Item().Element(e => BasicTable(e, p));
|
|
|
|
|
|
ManualNote(col, p.TestFunction?.Basic);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (sel.Curve && (p.CurveResults.Count > 0 || HasManualNote(p.TestFunction?.Curve)))
|
|
|
|
|
|
{
|
|
|
|
|
|
Section(col, "5. 开启与关断曲线(1.1–1.6)");
|
|
|
|
|
|
if (p.CurveResults.Count > 0) col.Item().Element(e => CurveTable(e, p));
|
|
|
|
|
|
ManualNote(col, p.TestFunction?.Curve);
|
2026-07-01 20:04:27 +08:00
|
|
|
|
foreach (var (ch, images) in CurveImages.OrderBy(x => x.Key))
|
2026-06-30 12:10:29 +08:00
|
|
|
|
{
|
2026-07-01 20:04:27 +08:00
|
|
|
|
AddCurveImage(col, ch, "ON", images.Rising);
|
|
|
|
|
|
AddCurveImage(col, ch, "OFF", images.Falling);
|
2026-06-30 12:10:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (sel.Power && (p.PowerResults.Count > 0 || HasManualNote(p.TestFunction?.Power)))
|
|
|
|
|
|
{
|
|
|
|
|
|
Section(col, "6. 功率损耗");
|
|
|
|
|
|
if (p.PowerResults.Count > 0) col.Item().Element(e => PowerTable(e, p));
|
|
|
|
|
|
ManualNote(col, p.TestFunction?.Power);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (sel.Protect && (p.ProtectResults.Count > 0 || HasManualNote(p.TestFunction?.Protect)))
|
|
|
|
|
|
{
|
|
|
|
|
|
Section(col, "7. 保护功能(框架占位)");
|
|
|
|
|
|
if (p.ProtectResults.Count > 0) col.Item().Element(e => ProtectTable(e, p));
|
|
|
|
|
|
ManualNote(col, p.TestFunction?.Protect);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void Section(ColumnDescriptor col, string title)
|
|
|
|
|
|
{
|
|
|
|
|
|
col.Item().PaddingTop(6).Text(title).FontSize(13).Bold().FontColor(Colors.Blue.Darken2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 20:04:27 +08:00
|
|
|
|
private static void AddCurveImage(ColumnDescriptor column, int channel, string phaseLabel, byte[]? png)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (png == null || png.Length == 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
column.Item().PaddingTop(6).Text($"CH{channel:00} {phaseLabel} Curve").Bold();
|
|
|
|
|
|
column.Item().Image(png).FitWidth();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 12:10:29 +08:00
|
|
|
|
private static bool HasManualNote(string? text) => !string.IsNullOrWhiteSpace(text);
|
|
|
|
|
|
|
|
|
|
|
|
private static void ManualNote(ColumnDescriptor col, string? note)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!HasManualNote(note)) return;
|
|
|
|
|
|
|
|
|
|
|
|
col.Item().PaddingTop(8).Text("测试说明").FontSize(11).Bold();
|
|
|
|
|
|
col.Item()
|
|
|
|
|
|
.Border(1)
|
|
|
|
|
|
.BorderColor(Colors.Grey.Lighten2)
|
|
|
|
|
|
.Background(Colors.Grey.Lighten4)
|
|
|
|
|
|
.Padding(6)
|
|
|
|
|
|
.Column(lines =>
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var line in NormalizeLines(note!))
|
|
|
|
|
|
lines.Item().Text(string.IsNullOrEmpty(line) ? " " : line).FontSize(9);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static IEnumerable<string> NormalizeLines(string text)
|
|
|
|
|
|
=> text.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
|
|
|
|
|
|
|
|
|
|
|
|
private static void DutTable(IContainer c, Project p)
|
|
|
|
|
|
{
|
|
|
|
|
|
c.Table(t =>
|
|
|
|
|
|
{
|
|
|
|
|
|
t.ColumnsDefinition(cd => { cd.ConstantColumn(120); cd.RelativeColumn(); });
|
|
|
|
|
|
Row(t, "设备名称", p.Dut.ProductName);
|
|
|
|
|
|
Row(t, "设备型号", p.Dut.Model);
|
|
|
|
|
|
Row(t, "设备编号", p.Dut.SerialNumber);
|
|
|
|
|
|
Row(t, "生产厂家", p.Dut.Manufacturer);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void SiteTable(IContainer c, Project p)
|
|
|
|
|
|
{
|
|
|
|
|
|
c.Table(t =>
|
|
|
|
|
|
{
|
|
|
|
|
|
t.ColumnsDefinition(cd => { cd.ConstantColumn(120); cd.RelativeColumn(); });
|
|
|
|
|
|
Row(t, "测试人员", p.Site.Tester);
|
|
|
|
|
|
Row(t, "测试日期", p.Site.TestDate.ToString("yyyy-MM-dd"));
|
|
|
|
|
|
Row(t, "环境", $"温度 {p.Site.TemperatureC}℃ 湿度 {p.Site.HumidityPct}% 气压 {p.Site.PressureKpa}kPa");
|
|
|
|
|
|
Row(t, "备注", p.Site.Remark);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void Row(TableDescriptor t, string k, string? v)
|
|
|
|
|
|
{
|
|
|
|
|
|
t.Cell().Background(Colors.Grey.Lighten4).Padding(4).Text(k).SemiBold();
|
|
|
|
|
|
t.Cell().Padding(4).Text(v ?? "");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void OverallBadge(IContainer c, TestStatus s)
|
|
|
|
|
|
{
|
|
|
|
|
|
(string text, string color) = s switch
|
|
|
|
|
|
{
|
|
|
|
|
|
TestStatus.Pass => ("综合判定:合格", Colors.Green.Darken1),
|
|
|
|
|
|
TestStatus.Fail => ("综合判定:不合格", Colors.Red.Darken1),
|
|
|
|
|
|
TestStatus.Running => ("综合判定:测试中", Colors.Blue.Darken1),
|
|
|
|
|
|
TestStatus.Measured => ("综合判定:已测(无性能判据)", Colors.Blue.Darken1),
|
|
|
|
|
|
_ => ("综合判定:未测试", Colors.Grey.Darken1),
|
|
|
|
|
|
};
|
|
|
|
|
|
c.Padding(6).Background(color).Padding(10).Text(text).FontColor(Colors.White).FontSize(14).Bold();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void BasicTable(IContainer c, Project p)
|
|
|
|
|
|
{
|
|
|
|
|
|
c.Table(t =>
|
|
|
|
|
|
{
|
|
|
|
|
|
t.ColumnsDefinition(cd =>
|
|
|
|
|
|
{
|
|
|
|
|
|
cd.ConstantColumn(60); cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.ConstantColumn(70);
|
|
|
|
|
|
});
|
|
|
|
|
|
HeaderRow(t, "通道", "电压(V)", "电流(A)", "关后电压(V)", "结果");
|
|
|
|
|
|
foreach (var r in p.BasicResults)
|
|
|
|
|
|
{
|
|
|
|
|
|
t.Cell().Padding(3).Text($"CH{r.Channel}");
|
|
|
|
|
|
t.Cell().Padding(3).Text(r.VoltageOn.ToString("F2"));
|
|
|
|
|
|
t.Cell().Padding(3).Text(r.CurrentOn.ToString("F3"));
|
|
|
|
|
|
t.Cell().Padding(3).Text(r.VoltageOff.ToString("F2"));
|
|
|
|
|
|
t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void CurveTable(IContainer c, Project p)
|
|
|
|
|
|
{
|
|
|
|
|
|
c.Table(t =>
|
|
|
|
|
|
{
|
|
|
|
|
|
t.ColumnsDefinition(cd =>
|
|
|
|
|
|
{
|
|
|
|
|
|
cd.ConstantColumn(50); cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.ConstantColumn(60);
|
|
|
|
|
|
});
|
|
|
|
|
|
HeaderRow(t, "通道", "开启(ms)", "上升(ms)", "关断(ms)", "下降(ms)", "结果");
|
|
|
|
|
|
foreach (var r in p.CurveResults)
|
|
|
|
|
|
{
|
|
|
|
|
|
t.Cell().Padding(3).Text($"CH{r.Channel}");
|
|
|
|
|
|
t.Cell().Padding(3).Text(FormatMs(r.OnTimeMs));
|
|
|
|
|
|
t.Cell().Padding(3).Text(FormatMs(r.RiseTimeMs));
|
|
|
|
|
|
t.Cell().Padding(3).Text(FormatMs(r.OffTimeMs));
|
|
|
|
|
|
t.Cell().Padding(3).Text(FormatMs(r.FallTimeMs));
|
|
|
|
|
|
t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void PowerTable(IContainer c, Project p)
|
|
|
|
|
|
{
|
|
|
|
|
|
c.Table(t =>
|
|
|
|
|
|
{
|
|
|
|
|
|
t.ColumnsDefinition(cd =>
|
|
|
|
|
|
{
|
|
|
|
|
|
cd.ConstantColumn(50);
|
|
|
|
|
|
cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn();
|
|
|
|
|
|
cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn();
|
|
|
|
|
|
cd.ConstantColumn(60);
|
|
|
|
|
|
});
|
|
|
|
|
|
HeaderRow(t, "通道", "Vin(V)", "Iin(A)", "Vout(V)", "Iout(A)", "损耗(W)", "效率(%)", "状态");
|
|
|
|
|
|
foreach (var r in p.PowerResults)
|
|
|
|
|
|
{
|
|
|
|
|
|
t.Cell().Padding(3).Text($"CH{r.Channel}");
|
2026-07-02 01:50:02 +08:00
|
|
|
|
t.Cell().Padding(3).Text(r.Vin.ToString("F3"));
|
2026-06-30 12:10:29 +08:00
|
|
|
|
t.Cell().Padding(3).Text(r.Iin.ToString("F3"));
|
2026-07-02 01:50:02 +08:00
|
|
|
|
t.Cell().Padding(3).Text(r.Vout.ToString("F3"));
|
2026-06-30 12:10:29 +08:00
|
|
|
|
t.Cell().Padding(3).Text(r.Iout.ToString("F3"));
|
2026-07-02 01:50:02 +08:00
|
|
|
|
t.Cell().Padding(3).Text(r.PowerLossW.ToString("F3"));
|
|
|
|
|
|
t.Cell().Padding(3).Text(r.EfficiencyPct.ToString("F3"));
|
2026-06-30 12:10:29 +08:00
|
|
|
|
t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void ProtectTable(IContainer c, Project p)
|
|
|
|
|
|
{
|
|
|
|
|
|
c.Table(t =>
|
|
|
|
|
|
{
|
|
|
|
|
|
t.ColumnsDefinition(cd =>
|
|
|
|
|
|
{
|
|
|
|
|
|
cd.RelativeColumn(); cd.RelativeColumn(); cd.RelativeColumn(); cd.ConstantColumn(60);
|
|
|
|
|
|
});
|
|
|
|
|
|
HeaderRow(t, "保护类型", "触发条件", "关断时间(ms)", "结果");
|
|
|
|
|
|
foreach (var r in p.ProtectResults)
|
|
|
|
|
|
{
|
|
|
|
|
|
t.Cell().Padding(3).Text(r.ProtectType);
|
|
|
|
|
|
t.Cell().Padding(3).Text(r.Trigger);
|
|
|
|
|
|
t.Cell().Padding(3).Text(r.TripTimeMs.ToString("F2"));
|
|
|
|
|
|
t.Cell().Padding(3).Text(StatusText(r.Status)).FontColor(StatusColor(r.Status));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void HeaderRow(TableDescriptor t, params string[] cols)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var col in cols)
|
|
|
|
|
|
t.Cell().Background(Colors.Grey.Lighten3).Padding(4).Text(col).SemiBold();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string FormatMs(double v) => double.IsNaN(v) ? "—" : v.ToString("F3");
|
|
|
|
|
|
|
|
|
|
|
|
private static string StatusText(TestStatus s) => s switch
|
|
|
|
|
|
{
|
|
|
|
|
|
TestStatus.Pass => "合格",
|
|
|
|
|
|
TestStatus.Fail => "不合格",
|
|
|
|
|
|
TestStatus.Running => "测试中",
|
|
|
|
|
|
TestStatus.Measured => "已测",
|
|
|
|
|
|
_ => "未测试"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
private static string StatusColor(TestStatus s) => s switch
|
|
|
|
|
|
{
|
|
|
|
|
|
TestStatus.Pass => Colors.Green.Darken1,
|
|
|
|
|
|
TestStatus.Fail => Colors.Red.Darken1,
|
|
|
|
|
|
TestStatus.Running => Colors.Blue.Darken1,
|
|
|
|
|
|
TestStatus.Measured => Colors.Blue.Darken1,
|
|
|
|
|
|
_ => Colors.Grey.Darken1
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|