SSPC-Tester/SSPCTester.Logic/Report/WordReportGenerator.cs

448 lines
20 KiB
C#
Raw Normal View History

using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using SSPCTester.Logic.Models;
using A = DocumentFormat.OpenXml.Drawing;
using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing;
using PIC = DocumentFormat.OpenXml.Drawing.Pictures;
namespace SSPCTester.Logic.Report;
public sealed class WordReportGenerator
{
private const string FontName = "SimSun";
private const string Navy = "17324D";
private const string Blue = "245B82";
private const string PaleBlue = "EAF1F6";
private const string Line = "CBD5DF";
private const string TextColor = "1E2933";
private const string Muted = "667585";
private const string Success = "287A4B";
private const string Danger = "A53232";
private const string Warning = "98651A";
public Dictionary<int, CurveReportImages> CurveImages { get; set; } = new();
public void Generate(Project project, string outputPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
using var document = WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document);
var main = document.AddMainDocumentPart();
main.Document = new Document(new Body());
AddStyles(main);
Body body = main.Document.Body!;
string reportNo = BuildReportNumber(project);
if (project.Report.Cover)
ComposeCover(body, project, reportNo);
ComposeBody(body, main, project, reportNo);
body.Append(PageSectionProperties());
main.Document.Save();
}
private static void AddStyles(MainDocumentPart main)
{
var styles = main.AddNewPart<StyleDefinitionsPart>();
styles.Styles = new Styles(
new DocDefaults(
new RunPropertiesDefault(new RunProperties(
new RunFonts { Ascii = FontName, HighAnsi = FontName, EastAsia = FontName },
new FontSize { Val = "20" },
new Color { Val = TextColor })),
new ParagraphPropertiesDefault(new ParagraphProperties(
new SpacingBetweenLines { After = "120", Line = "276", LineRule = LineSpacingRuleValues.Auto }))),
Style("Title", "Title", 36, Navy, bold: true),
Style("Heading1", "Heading 1", 26, Navy, bold: true),
Style("Heading2", "Heading 2", 22, Navy, bold: true),
Style("Normal", "Normal", 20, TextColor));
styles.Styles.Save();
}
private static Style Style(string id, string name, int halfPoints, string color, bool bold = false)
{
var run = new RunProperties(
new RunFonts { Ascii = FontName, HighAnsi = FontName, EastAsia = FontName },
new FontSize { Val = halfPoints.ToString() },
new Color { Val = color });
if (bold) run.Append(new Bold());
return new Style(
new StyleName { Val = name },
new BasedOn { Val = "Normal" },
new NextParagraphStyle { Val = "Normal" },
run)
{ Type = StyleValues.Paragraph, StyleId = id };
}
private static void ComposeCover(Body body, Project p, string reportNo)
{
body.Append(
Paragraph("SPC-DynaTest 检测系统", 18, Muted, JustificationValues.Right),
Spacer(32),
Paragraph("固态功率控制器", 32, Navy, JustificationValues.Center, bold: true),
Paragraph("动态参数检测报告", 44, Navy, JustificationValues.Center, bold: true),
Spacer(22),
KeyValueTable(new[]
{
("报告编号", reportNo),
("项目名称", ValueOrDash(p.Name)),
("设备编号", ValueOrDash(p.Dut.SerialNumber)),
2026-07-02 17:43:32 +08:00
("设备名称", ValueOrDash(p.Dut.ProductName)),
("设备型号", ValueOrDash(p.Dut.Model)),
("委托/生产单位", ValueOrDash(p.Dut.Manufacturer)),
("检测日期", p.Site.TestDate.ToString("yyyy-MM-dd"))
}),
Spacer(18),
ConclusionBox(p),
Spacer(52),
SignatureTable(),
PageBreak());
}
private void ComposeBody(Body body, MainDocumentPart main, Project p, string reportNo)
{
body.Append(HeaderParagraph("固态功率控制器动态参数检测报告", reportNo));
int section = 1;
if (p.Report.Cover)
{
Section(body, section++, "检测概况", "TEST OVERVIEW");
body.Append(OverviewTable(p));
Section(body, section++, "结果摘要", "RESULT SUMMARY");
body.Append(SummaryTable(p));
}
if (p.Report.Basic && (p.BasicResults.Count > 0 || HasNote(p.TestFunction?.Basic)))
{
Section(body, section++, "基础测试", "BASIC FUNCTION TEST");
if (p.BasicResults.Count > 0)
body.Append(ResultTable(
new[] { "通道", "导通电压 V", "导通电流 A", "关后电压 V", "判定" },
p.BasicResults.Select(r => new[]
{
$"CH{r.Channel:00}", Format(r.VoltageOn, "F2"), Format(r.CurrentOn, "F3"),
Format(r.VoltageOff, "F2"), StatusText(r.Status)
})));
ManualNote(body, p.TestFunction?.Basic);
}
if (p.Report.Curve && (p.CurveResults.Count > 0 || HasNote(p.TestFunction?.Curve)))
{
Section(body, section++, "开启与关断曲线", "SWITCHING CHARACTERISTICS");
if (p.CurveResults.Count > 0)
body.Append(ResultTable(
new[] { "通道", "开启时间 ms", "上升时间 ms", "关断时间 ms", "下降时间 ms", "判定" },
p.CurveResults.Select(r => new[]
{
$"CH{r.Channel:00}", Format(r.OnTimeMs, "F3"), Format(r.RiseTimeMs, "F3"),
Format(r.OffTimeMs, "F3"), Format(r.FallTimeMs, "F3"), StatusText(r.Status)
})));
ManualNote(body, p.TestFunction?.Curve);
foreach (var (channel, images) in CurveImages.OrderBy(x => x.Key))
{
AddCurveImage(body, main, channel, "ON", images.Rising);
AddCurveImage(body, main, channel, "OFF", images.Falling);
}
}
if (p.Report.Power && (p.PowerResults.Count > 0 || HasNote(p.TestFunction?.Power)))
{
Section(body, section++, "功率损耗", "POWER LOSS");
if (p.PowerResults.Count > 0)
body.Append(ResultTable(
new[] { "通道", "输入电压 V", "输入电流 A", "输出电压 V", "输出电流 A", "功率损耗 W", "效率 %", "状态" },
p.PowerResults.Select(r => 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)
})));
ManualNote(body, p.TestFunction?.Power);
}
if (p.Report.Protect && (p.ProtectResults.Count > 0 || HasNote(p.TestFunction?.Protect)))
{
Section(body, section++, "保护功能", "PROTECTION FUNCTION");
if (p.ProtectResults.Count > 0)
body.Append(ResultTable(
2026-07-02 17:43:32 +08:00
new[] { "序号", "保护类型", "触发条件", "实测值", "判别模式", "判别结果" },
p.ProtectResults.Select(r => new[]
{
2026-07-02 17:43:32 +08:00
r.Index > 0 ? r.Index.ToString() : "—",
ValueOrDash(r.ProtectType),
ValueOrDash(r.Trigger),
ValueOrDash(r.MeasuredValue),
ValueOrDash(r.JudgeMode),
StatusText(r.Status)
})));
ManualNote(body, p.TestFunction?.Protect);
}
body.Append(Spacer(10), ConclusionBox(p), FooterParagraph());
}
private static Paragraph HeaderParagraph(string title, string reportNo) =>
Paragraph($"{title} 报告编号:{reportNo}", 20, Navy, bold: true);
private static void Section(Body body, int number, string title, string english)
{
body.Append(Spacer(8));
body.Append(Paragraph($"{number:00} {title} {english}", 24, Navy, bold: true));
}
private static Table OverviewTable(Project p) => KeyValueGrid(new[]
{
2026-07-02 17:43:32 +08:00
("设备编号", p.Dut.SerialNumber), ("设备名称", p.Dut.ProductName), ("设备型号", p.Dut.Model),
("测试人员", p.Site.Tester), ("测试日期", p.Site.TestDate.ToString("yyyy-MM-dd")),
("环境温度", $"{p.Site.TemperatureC:F1} ℃"), ("相对湿度", $"{p.Site.HumidityPct:F1} %RH"),
("环境气压", $"{p.Site.PressureKpa:F1} kPa"), ("项目创建", p.CreatedAt.ToString("yyyy-MM-dd HH:mm")),
("测试备注", p.Site.Remark), ("综合判定", StatusText(p.OverallStatus))
});
private static Table SummaryTable(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();
return KeyValueGrid(new[]
{
("检测项目", statuses.Length.ToString()),
("合格", statuses.Count(x => x == TestStatus.Pass).ToString()),
("不合格", statuses.Count(x => x == TestStatus.Fail).ToString()),
("已测", statuses.Count(x => x == TestStatus.Measured).ToString()),
("未测", statuses.Count(x => x == TestStatus.Untested).ToString()),
("综合判定", StatusText(p.OverallStatus))
});
}
private static Table SignatureTable()
{
var table = TableBase(new[] { 1.0, 1.0, 1.0 });
AddRow(table, new[] { "编制:", "审核:", "批准:" }, header: false);
AddRow(table, new[] { "日期:", "日期:", "日期:" }, header: false);
return table;
}
private static Table KeyValueTable(IEnumerable<(string Label, string Value)> rows)
{
var table = TableBase(new[] { 1.1, 3.0 });
foreach (var (label, value) in rows)
AddRow(table, new[] { label, value }, labelRow: true);
return table;
}
private static Table KeyValueGrid(IEnumerable<(string Label, string Value)> source)
{
var rows = source.ToArray();
var table = TableBase(new[] { 1.1, 2.1, 1.1, 2.1 });
for (int i = 0; i < rows.Length; i += 2)
{
var left = rows[i];
var right = i + 1 < rows.Length ? rows[i + 1] : ("", "");
AddRow(table, new[]
{
left.Label, ValueOrDash(left.Value),
right.Item1, ValueOrDash(right.Item2)
}, labelColumns: new[] { 0, 2 });
}
return table;
}
private static Table ResultTable(IReadOnlyList<string> headers, IEnumerable<string[]> rows)
{
var table = TableBase(Enumerable.Repeat(1.0, headers.Count).ToArray());
AddRow(table, headers, header: true);
foreach (var row in rows)
AddRow(table, row, header: false);
return table;
}
private static Table TableBase(IReadOnlyList<double> widths)
{
var grid = new TableGrid(widths.Select(w => new GridColumn { Width = ((int)(w * 1200)).ToString() }));
var table = new Table();
table.Append(new TableProperties(
new TableWidth { Width = "5000", Type = TableWidthUnitValues.Pct },
new TableBorders(
new TopBorder { Val = BorderValues.Single, Color = Line, Size = 4 },
new BottomBorder { Val = BorderValues.Single, Color = Line, Size = 4 },
new LeftBorder { Val = BorderValues.Single, Color = Line, Size = 4 },
new RightBorder { Val = BorderValues.Single, Color = Line, Size = 4 },
new InsideHorizontalBorder { Val = BorderValues.Single, Color = Line, Size = 4 },
new InsideVerticalBorder { Val = BorderValues.Single, Color = Line, Size = 4 })));
table.Append(grid);
return table;
}
private static void AddRow(
Table table,
IReadOnlyList<string> values,
bool header = false,
bool labelRow = false,
IReadOnlyCollection<int>? labelColumns = null)
{
var row = new TableRow();
for (int i = 0; i < values.Count; i++)
{
bool label = labelRow && i == 0 || labelColumns?.Contains(i) == true;
row.Append(new TableCell(
new TableCellProperties(
new Shading { Fill = header ? Navy : label ? PaleBlue : "FFFFFF" },
new TableCellMargin(
new TopMargin { Width = "70", Type = TableWidthUnitValues.Dxa },
new BottomMargin { Width = "70", Type = TableWidthUnitValues.Dxa },
new LeftMargin { Width = "90", Type = TableWidthUnitValues.Dxa },
new RightMargin { Width = "90", Type = TableWidthUnitValues.Dxa })),
Paragraph(values[i], 17, header ? "FFFFFF" : label ? Navy : TextColor, bold: header || label)));
}
table.Append(row);
}
private static Paragraph ConclusionBox(Project p)
{
string text = $"检测结论:{StatusText(p.OverallStatus)}。{ConclusionText(p.OverallStatus)}";
var paragraph = Paragraph(text, 22, StatusColor(p.OverallStatus), bold: true);
paragraph.ParagraphProperties ??= new ParagraphProperties();
paragraph.ParagraphProperties.Append(new Shading { Fill = StatusFill(p.OverallStatus) });
return paragraph;
}
private static void ManualNote(Body body, string? note)
{
if (!HasNote(note)) return;
body.Append(Paragraph("测试说明 / 现场记录", 18, Navy, bold: true));
body.Append(Paragraph(note!.Trim(), 18, TextColor));
}
private static void AddImage(Body body, MainDocumentPart main, byte[] png, string name, double widthCm, double heightCm)
{
var imagePart = main.AddImagePart(ImagePartType.Png);
using (var stream = new MemoryStream(png))
imagePart.FeedData(stream);
string id = main.GetIdOfPart(imagePart);
long cx = CmToEmu(widthCm);
long cy = CmToEmu(heightCm);
var drawing = new Drawing(
new DW.Inline(
new DW.Extent { Cx = cx, Cy = cy },
new DW.EffectExtent { LeftEdge = 0, TopEdge = 0, RightEdge = 0, BottomEdge = 0 },
new DW.DocProperties { Id = (UInt32Value)1U, Name = name },
new DW.NonVisualGraphicFrameDrawingProperties(new A.GraphicFrameLocks { NoChangeAspect = true }),
new A.Graphic(
new A.GraphicData(
new PIC.Picture(
new PIC.NonVisualPictureProperties(
new PIC.NonVisualDrawingProperties { Id = (UInt32Value)0U, Name = name },
new PIC.NonVisualPictureDrawingProperties()),
new PIC.BlipFill(
new A.Blip { Embed = id },
new A.Stretch(new A.FillRectangle())),
new PIC.ShapeProperties(
new A.Transform2D(
new A.Offset { X = 0, Y = 0 },
new A.Extents { Cx = cx, Cy = cy }),
new A.PresetGeometry(new A.AdjustValueList()) { Preset = A.ShapeTypeValues.Rectangle })))
{ Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" }))
{ DistanceFromTop = 0U, DistanceFromBottom = 0U, DistanceFromLeft = 0U, DistanceFromRight = 0U });
body.Append(new Paragraph(new Run(drawing)));
}
private static void AddCurveImage(Body body, MainDocumentPart main, int channel, string phaseLabel, byte[]? png)
{
if (png == null || png.Length == 0) return;
body.Append(Paragraph($"CH{channel:00} {phaseLabel} Curve", 20, Navy, bold: true));
AddImage(body, main, png, $"CH{channel:00} {phaseLabel} Curve", 15.6, 6.2);
}
private static Paragraph Paragraph(
string text,
int halfPoints,
string color,
JustificationValues? align = null,
bool bold = false)
{
var runProperties = new RunProperties(
new RunFonts { Ascii = FontName, HighAnsi = FontName, EastAsia = FontName },
new FontSize { Val = halfPoints.ToString() },
new Color { Val = color });
if (bold) runProperties.Append(new Bold());
return new Paragraph(
new ParagraphProperties(
new Justification { Val = align ?? JustificationValues.Left },
new SpacingBetweenLines { After = "120" }),
new Run(runProperties, new Text(text) { Space = SpaceProcessingModeValues.Preserve }));
}
private static Paragraph Spacer(int points) =>
new(new ParagraphProperties(new SpacingBetweenLines { After = (points * 20).ToString() }), new Run(new Text("")));
private static Paragraph PageBreak() => new(new Run(new Break { Type = BreakValues.Page }));
private static Paragraph FooterParagraph() =>
Paragraph($"生成时间:{DateTime.Now:yyyy-MM-dd HH:mm} 本报告仅对所检样品及记录数据负责。", 16, Muted, JustificationValues.Center);
private static SectionProperties PageSectionProperties() =>
new(
new PageSize { Width = 11906U, Height = 16838U },
new PageMargin { Top = 900, Right = 850U, Bottom = 900, Left = 850U, Header = 450U, Footer = 450U, Gutter = 0U });
private static long CmToEmu(double cm) => (long)Math.Round(cm * 360000.0);
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 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 StatusColor(TestStatus status) => status switch
{
TestStatus.Pass => Success,
TestStatus.Fail => Danger,
TestStatus.Running => Warning,
TestStatus.Measured => Blue,
_ => Muted
};
private static string StatusFill(TestStatus status) => status switch
{
TestStatus.Pass => "E8F3EC",
TestStatus.Fail => "F8EAEA",
TestStatus.Running => "FAF1DE",
TestStatus.Measured => PaleBlue,
_ => "F1F3F5"
};
private static string ConclusionText(TestStatus status) => status switch
{
TestStatus.Pass => "本次所选检测项目均满足当前判定要求,综合结论为合格。",
TestStatus.Fail => "本次检测存在不合格项目,请结合异常通道记录进行复核与处置。",
TestStatus.Running => "检测尚未结束,当前结果仅供过程参考。",
TestStatus.Measured => "相关项目已完成测量,报告基于记录数据给出测量结果。",
_ => "检测数据尚不完整,暂不具备出具最终合格判定的条件。"
};
}