455 lines
20 KiB
C#
455 lines
20 KiB
C#
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)),
|
|
("设备名称", 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", "关后电流 A", "判定" },
|
|
p.BasicResults.Select(r => new[]
|
|
{
|
|
$"CH{r.Channel:00}", Format(r.VoltageOn, "F2"), Format(r.CurrentOn, "F3"),
|
|
Format(r.VoltageOff, "F2"), Format(r.CurrentOff, "F3"), 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(
|
|
new[] { "序号", "保护类型", "触发条件", "实测值", "判别模式", "判别结果" },
|
|
p.ProtectResults.Select(r => new[]
|
|
{
|
|
r.Index > 0 ? r.Index.ToString() : "—",
|
|
ValueOrDash(r.ProtectType),
|
|
ValueOrDash(r.Trigger),
|
|
ValueOrDash(r.MeasuredValue),
|
|
ValueOrDash(r.JudgeMode),
|
|
ProtectStatusText(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[]
|
|
{
|
|
("设备编号", 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 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 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 => "相关项目已完成测量,报告基于记录数据给出测量结果。",
|
|
_ => "检测数据尚不完整,暂不具备出具最终合格判定的条件。"
|
|
};
|
|
}
|