From 29bc8576af3d35b19d00929e81462a25f9169be6 Mon Sep 17 00:00:00 2001 From: QI Mingxuan Date: Thu, 14 May 2026 19:29:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A5=E5=91=8AXP.ReportEngine=EF=BC=9A?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20IReportService=E6=8E=A5=E5=8F=A3=E5=B0=81?= =?UTF-8?q?=E8=A3=85=E5=AE=8C=E6=95=B4=E6=8A=A5=E5=91=8A=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E7=B1=BB=E5=BA=93=E7=9B=B4=E6=8E=A5=E8=B0=83=E7=94=A8=EF=BC=9B?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20ReportRequest=E3=80=81ReportServiceResult?= =?UTF-8?q?=20=E8=AF=B7=E6=B1=82/=E5=93=8D=E5=BA=94=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=EF=BC=9B=E6=96=B0=E5=A2=9E=E5=BC=95=E6=93=8E=E9=A2=84=E7=83=AD?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=EF=BC=9BPDF=20=E7=94=9F=E6=88=90=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20Task.Run=20=E5=90=8E=E5=8F=B0=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=EF=BC=8C=E8=A7=A3=E5=86=B3=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=92=8C=E4=B8=BB=E7=AA=97=E5=8F=A3=E5=8D=A1?= =?UTF-8?q?=E6=AD=BB=E9=97=AE=E9=A2=98=EF=BC=9B=E5=AE=8C=E5=96=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Documents/FontFilesGuidance.md | 53 ++- XP.ReportEngine/Documents/Guidance.md | 269 ++++++++++++ XP.ReportEngine/Documents/README.md | 237 +++++----- .../Documents/TemplateDevelopment.md | 403 ++++++++++++++++++ XP.ReportEngine/Interfaces/IReportService.cs | 45 ++ XP.ReportEngine/Models/ReportRequest.cs | 50 +++ XP.ReportEngine/Models/ReportServiceResult.cs | 49 +++ XP.ReportEngine/ReportEngineModule.cs | 8 + XP.ReportEngine/Services/ITextPdfRenderer.cs | 20 +- .../Services/ProcessorDataAdapter.cs | 3 +- XP.ReportEngine/Services/ReportService.cs | 390 +++++++++++++++++ .../ViewModels/ReportDemoViewModel.cs | 167 ++------ XP.ReportEngine/Views/ReportDemoWindow.xaml | 109 ++--- 13 files changed, 1485 insertions(+), 318 deletions(-) create mode 100644 XP.ReportEngine/Documents/Guidance.md create mode 100644 XP.ReportEngine/Documents/TemplateDevelopment.md create mode 100644 XP.ReportEngine/Interfaces/IReportService.cs create mode 100644 XP.ReportEngine/Models/ReportRequest.cs create mode 100644 XP.ReportEngine/Models/ReportServiceResult.cs create mode 100644 XP.ReportEngine/Services/ReportService.cs diff --git a/XP.ReportEngine/Documents/FontFilesGuidance.md b/XP.ReportEngine/Documents/FontFilesGuidance.md index ce0850d..63d4ff5 100644 --- a/XP.ReportEngine/Documents/FontFilesGuidance.md +++ b/XP.ReportEngine/Documents/FontFilesGuidance.md @@ -17,18 +17,57 @@ XP.ReportEngine 使用 **Windows 系统自带字体** 生成 PDF,无需额外 ## 后备机制 | Fallback Mechanism -ITextPdfRenderer 按以下顺序加载字体: +`ITextPdfRenderer` 按以下顺序加载字体: -1. 微软雅黑(`msyh.ttc`)→ 加载失败时尝试宋体(`simsun.ttc`) -2. Arial(`arial.ttf`)→ 加载失败时使用 iText 内置 Helvetica -3. 最终后备:Helvetica(不支持中文字符) +``` +微软雅黑 (msyh.ttc) + ↓ 加载失败 +宋体 (simsun.ttc) + ↓ 加载失败 +_cjkFont = null + +Arial (arial.ttf) + ↓ 加载失败 +_westernFont = null + +最终后备: iText 内置 Helvetica(不支持中文字符) +``` + +语言选择逻辑: +- zh-CN / zh-TW → 优先使用微软雅黑 +- en-US → 优先使用 Arial,后备微软雅黑(微软雅黑也支持西文) + +## 字体子集化 | Font Subsetting + +iText7 在 `document.Close()` 时执行字体子集化: +- 分析文档中实际使用的字符 +- 从完整字体文件(微软雅黑约 15MB)中提取用到的字形子集 +- 仅嵌入子集到 PDF 中 + +这使得最终 PDF 文件大小合理(通常 200KB-2MB),但 `Close()` 操作需要约 1-1.5 秒。 + +## 性能说明 | Performance Notes + +| 阶段 | 首次耗时 | 后续耗时 | 说明 | +|------|---------|---------|------| +| 字体加载 | ~5-6s | 0ms | 首次需从磁盘读取 TTC 文件并解析 | +| 字体子集化 | ~1.2s | ~1.2s | 每次生成都需要,与字符数量相关 | + +模块通过 `WarmUpAsync()` 预热机制在应用启动时后台完成首次字体加载,用户首次生成报告时不会感受到延迟。 ## 系统要求 | System Requirements - Windows 10 或更高版本(微软雅黑为系统预装字体) - 如果在精简版 Windows 上运行,需确保系统已安装微软雅黑字体 -## Fonts 目录 | Fonts Directory +## 自定义字体扩展 | Custom Font Extension -`XP.ReportEngine/Fonts/` 目录当前为空,保留用于未来可能的自定义字体扩展。 -如需添加自定义字体,可在 `ITextPdfRenderer.InitializeFonts()` 中扩展加载逻辑。 +如需添加自定义字体: + +1. 将字体文件(.ttf / .ttc / .otf)放入项目 `Fonts/` 目录 +2. 在 `.csproj` 中取消注释嵌入资源配置: + ```xml + + + ``` +3. 在 `ITextPdfRenderer.InitializeFonts()` 中扩展加载逻辑 diff --git a/XP.ReportEngine/Documents/Guidance.md b/XP.ReportEngine/Documents/Guidance.md new file mode 100644 index 0000000..6eea6f2 --- /dev/null +++ b/XP.ReportEngine/Documents/Guidance.md @@ -0,0 +1,269 @@ +# XP.ReportEngine 使用指南 | Usage Guidance + +## 1. 概述 + +本文档说明如何在 XplorePlane 项目中使用 `XP.ReportEngine` 模块生成 PDF 检测报告。 + +模块提供两种调用方式: +- **推荐**:通过 `IReportService` 门面接口(一行调用,自动处理所有细节) +- **高级**:直接使用底层管线接口(`IReportGenerator`、`IReportDataAdapter` 等) + +## 2. 前置条件 + +### 2.1 项目引用 + +在调用方项目的 `.csproj` 中添加项目引用: + +```xml + +``` + +### 2.2 模块注册 + +确保 `ReportEngineModule` 已在 `XP.App` 的模块目录中注册。模块初始化时会自动: +- 注册所有服务到 DI 容器 +- 注册多语言资源到 Fallback Chain +- 后台执行引擎预热(字体加载 + JIT 编译) + +### 2.3 配置文件 + +在 `App.config` 中添加报告引擎配置项,参见 `Documents/App.config.example`。 + +## 3. 通过 IReportService 生成报告(推荐) + +### 3.1 基本用法 + +```csharp +using XP.ReportEngine.Interfaces; +using XP.ReportEngine.Models; + +public class InspectionService +{ + private readonly IReportService _reportService; + + public InspectionService(IReportService reportService) + { + _reportService = reportService; + } + + public async Task GenerateInspectionReport( + List processorOutputs, + string productName, + string operatorName) + { + var request = new ReportRequest + { + // 必填:处理器输出数据 + ProcessorOutputs = processorOutputs, + + // 必填:报告元数据 + Metadata = new ReportMetadata + { + SampleName = productName, + OperatorName = operatorName, + InspectionDate = DateTime.Now, + Description = "BGA 焊球气泡率检测" + }, + + // 可选:文件名占位符参数(用于 FileNamePattern) + FileNameParameters = new Dictionary + { + ["ProductName"] = productName, + ["ProductCode"] = "PCBA-X100", + ["WorkpieceSN"] = "SN20250001", + ["DeviceId"] = "XP-CT-001", + ["MachineId"] = "MC01", + ["Result"] = "Pass" + } + }; + + var result = await _reportService.GenerateAsync(request); + + if (result.IsSuccess) + { + return result.OutputFilePath; // PDF 文件路径 + } + else + { + throw new Exception($"报告生成失败: {result.ErrorMessage}"); + } + } +} +``` + +### 3.2 指定输出路径 + +```csharp +var request = new ReportRequest +{ + ProcessorOutputs = outputs, + Metadata = metadata, + // 指定输出路径后,不再使用 ReportConfig 中的 OutputDirectory 和 FileNamePattern + OutputFilePath = @"D:\CustomPath\MyReport.pdf" +}; +``` + +### 3.3 注入额外图像 + +```csharp +var request = new ReportRequest +{ + ProcessorOutputs = outputs, + Metadata = metadata, + AdditionalImages = new Dictionary + { + // 工件整体图(首页显示) + ["workpieceImage"] = new ImageData + { + SourceType = ImageSourceType.FilePath, + FilePath = @"D:\Images\workpiece.png" + }, + // 自定义图像 + ["customImage"] = new ImageData + { + SourceType = ImageSourceType.Bytes, + Bytes = imageBytes + } + } +}; +``` + +### 3.4 注入自定义属性 + +```csharp +var request = new ReportRequest +{ + ProcessorOutputs = outputs, + Metadata = metadata, + // 自定义属性会合并到 ReportContext.Properties,可在模板中通过 ${key} 绑定 + CustomProperties = new Dictionary + { + ["batchNumber"] = "BATCH-2025-001", + ["inspectionStation"] = "Station-A" + } +}; +``` + +## 4. ReportRequest 完整字段说明 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `ProcessorOutputs` | `List` | 是 | 处理器输出数据列表 | +| `Metadata` | `ReportMetadata` | 是 | 报告元数据 | +| `OutputFilePath` | `string` | 否 | 输出路径,为空时自动生成 | +| `FileNameParameters` | `Dictionary` | 否 | 文件名占位符参数 | +| `AdditionalImages` | `Dictionary` | 否 | 额外图像数据 | +| `CustomProperties` | `Dictionary` | 否 | 自定义属性 | + +### ReportMetadata 字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `ReportId` | `string` | 报告编号(为空时自动生成 RPT-yyyyMMdd-NNN) | +| `InspectionDate` | `DateTime` | 检测日期 | +| `SampleName` | `string` | 样品/产品名称 | +| `OperatorName` | `string` | 操作员 | +| `Description` | `string` | 描述信息 | + +### ProcessorOutput 字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `ProcessorType` | `string` | 处理器类型标识 | +| `OutputData` | `Dictionary` | 输出数据字典 | +| `AnnotatedImage` | `ImageData` | 关联的已标注图像 | + +支持的 ProcessorType: +- `LineMeasurementProcessor` — 线测量 +- `BgaVoidRateProcessor` — BGA 气泡率 +- `VoidMeasurementProcessor` — 空隙测量 +- `FillRateProcessor` — 通孔填锡率 + +## 5. ReportServiceResult 结果说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `IsSuccess` | `bool` | 是否成功 | +| `OutputFilePath` | `string` | 输出文件路径(成功时有值) | +| `ReportId` | `string` | 报告编号 | +| `ErrorMessage` | `string` | 错误信息(失败时有值) | +| `Exception` | `Exception` | 异常对象(失败时有值) | + +## 6. 在 ViewModel 中使用(带 UI 交互) + +如果需要进度条、文件对话框等 UI 交互,参考 `ReportDemoViewModel.cs` 的实现模式: + +```csharp +// 在后台线程执行,避免阻塞 UI +var genResult = await Task.Run(() => _reportService.GenerateAsync(request)); +``` + +关键点: +- `IReportService.GenerateAsync` 内部是 CPU 密集型操作(PDF 渲染) +- 必须用 `Task.Run` 推到线程池,否则会阻塞 UI 线程 +- 进度条、对话框等 UI 逻辑留在 ViewModel 中 + +## 7. 高级用法:直接使用底层管线 + +适用于需要自定义管线某个阶段的场景: + +```csharp +// 1. 数据适配 +var adapter = container.Resolve(); +var context = adapter.Adapt(processorOutputs, metadata); + +// 2. 手动修改上下文 +context.Properties["customKey"] = "customValue"; +context.Images["myImage"] = new ImageData { ... }; + +// 3. 调用管线生成 +var generator = container.Resolve(); +var options = new ReportGenerationOptions +{ + TemplatePath = "Templates/StandardReportTemplate.json", + OutputFilePath = @"D:\output.pdf", + Format = ReportOutputFormat.Pdf +}; + +var result = await generator.GenerateAsync(context, options); +``` + +## 8. 预热机制 + +模块启动时自动在后台执行预热,无需手动调用。如果需要在特定时机手动预热: + +```csharp +var reportService = container.Resolve(); +await reportService.WarmUpAsync(); +``` + +预热内容: +- iText7 程序集加载 +- BouncyCastle 加密提供程序注册 +- 微软雅黑字体文件读取和解析(约 5-6 秒) +- JSON 模板反序列化 JIT 编译 +- 管线各阶段代码 JIT 编译 + +预热完成后,首次正式生成报告不会有额外延迟。 + +## 9. 线程安全 + +`IReportService` 内部使用 `SemaphoreSlim(1, 1)` 互斥锁,确保: +- 预热与正式生成不会并发执行 +- 多个并发的 `GenerateAsync` 调用会串行执行 + +这是因为 `ITextPdfRenderer` 的字体字段在每次渲染时重置,并发渲染会导致 iText7 的 `"belongs to other PDF document"` 错误。 + +## 10. 常见问题 + +### Q: 首次生成报告很慢? +A: 正常现象。首次需要加载字体(~5s)+ 字体子集化(~1.2s)。预热机制会在应用启动时后台完成字体加载,用户首次操作时只需等待子集化时间。 + +### Q: PDF 中图像显示为"无图像"占位矩形? +A: 检查 `ProcessorOutput.AnnotatedImage` 或 `ReportRequest.AdditionalImages` 中的图像路径是否正确、文件是否存在。 + +### Q: 报告中某些字段为空? +A: 检查 `ProcessorOutput.OutputData` 中的键名是否与 `ProcessorDataAdapter` 中的映射一致(区分大小写)。 + +### Q: 如何自定义报告模板? +A: 修改 `Templates/StandardReportTemplate.json`,参考 `Documents/XP.ReportEngineModelDefine.md` 中的模板结构定义。 diff --git a/XP.ReportEngine/Documents/README.md b/XP.ReportEngine/Documents/README.md index ecb8bb8..a53dc39 100644 --- a/XP.ReportEngine/Documents/README.md +++ b/XP.ReportEngine/Documents/README.md @@ -2,26 +2,34 @@ ## 概述 -XP.ReportEngine 是 XplorePlane 平面 CT 检测系统的 PDF 报告生成模块。负责将 XP.ImageProcessing 的检测分析结果(距离测量、BGA 气泡率、空隙测量、通孔填锡率)转换为结构化的 PDF 检测报告。 +XP.ReportEngine 是 XplorePlane 平面 CT 检测系统的 PDF 报告生成模块。负责将检测分析结果(距离测量、BGA 气泡率、空隙测量、通孔填锡率)转换为结构化的 PDF 检测报告。 模块采用管线式架构(Pipeline),将报告生成过程分解为五个独立阶段: **模板加载 → 数据适配 → 数据绑定 → 排版计算 → PDF 渲染** +对外提供 `IReportService` 门面接口,外部模块只需构建 `ReportRequest` 即可一行调用完成报告生成。 + ## 技术栈 | 依赖 | 版本 | 用途 | |------|------|------| | .NET 8.0 | net8.0-windows7.0 | 运行时 | -| itext7 | 8.0.5 | PDF 文档生成核心库 | -| itext7.bouncy-castle-adapter | 8.0.5 | iText 7 加密支持 | +| iText7 | 8.0.5 | PDF 文档生成核心库 | +| itext7.bouncy-castle-adapter | 8.0.5 | iText7 加密支持 | | Newtonsoft.Json | 13.0.3 | JSON 模板反序列化 | | Prism.Wpf | 9.0.537 | 模块化框架与 DI | +| Telerik UI for WPF | 2024.1.408 | UI 控件(演示窗口) | +| XP.Common | — | 日志、本地化、通用窗体、PDF 阅读器 | ## 项目结构 ``` XP.ReportEngine/ +├── Configs/ # 配置加载 +│ ├── ConfigLoader.cs +│ └── ReportConfig.cs ├── Interfaces/ # 核心接口定义 +│ ├── IReportService.cs ★ 门面接口(外部调用入口) │ ├── IReportGenerator.cs │ ├── IReportGeneratorFactory.cs │ ├── ITemplateEngine.cs @@ -30,24 +38,28 @@ XP.ReportEngine/ │ ├── IPdfRenderer.cs │ └── IReportDataAdapter.cs ├── Models/ # 数据模型 +│ ├── ReportRequest.cs ★ 报告生成请求 +│ ├── ReportServiceResult.cs ★ 报告服务结果 │ ├── ReportContext.cs +│ ├── ReportMetadata.cs │ ├── ImageData.cs │ ├── ReportTemplate.cs │ ├── TemplateElement.cs │ ├── LayoutPage.cs │ ├── ReportResult.cs │ ├── ReportGenerationOptions.cs -│ └── ProcessorOutput.cs +│ ├── ProcessorOutput.cs +│ └── TemplateValidationResult.cs ├── Services/ # 服务实现 +│ ├── ReportService.cs ★ 门面服务实现 │ ├── PdfReportGenerator.cs # 管线协调器 │ ├── ReportGeneratorFactory.cs # 工厂 │ ├── JsonTemplateEngine.cs # JSON 模板加载与验证 │ ├── ExpressionDataBinder.cs # ${} 表达式数据绑定 │ ├── PageLayoutEngine.cs # 分页与排版 -│ ├── ITextPdfRenderer.cs # iText 7 PDF 渲染 +│ ├── ITextPdfRenderer.cs # iText7 PDF 渲染 │ ├── ProcessorDataAdapter.cs # 处理器数据适配 -│ └── ReportIdGenerator.cs # 报告编号生成 -├── Fonts/ # 字体目录(当前使用系统字体,目录保留备用) +│ └── ReportIdGenerator.cs # 报告编号生成(RPT-yyyyMMdd-NNN) ├── Templates/ # JSON 报告模板 │ └── StandardReportTemplate.json ├── Resources/ # 多语言资源文件 @@ -55,112 +67,99 @@ XP.ReportEngine/ │ ├── Resources.zh-CN.resx │ ├── Resources.zh-TW.resx │ └── Resources.en-US.resx +├── ViewModels/ # 演示窗口 ViewModel +│ └── ReportDemoViewModel.cs +├── Views/ # 演示窗口 View +│ ├── ReportDemoWindow.xaml +│ └── ReportDemoWindow.xaml.cs ├── Documents/ # 项目文档 +│ ├── README.md ← 本文件 +│ ├── Guidance.md # 使用指南 +│ ├── App.config.example # 配置示例 +│ ├── FontFilesGuidance.md # 字体方案说明 +│ ├── XP.ReportEngineDesign.md # 架构设计文档 +│ └── XP.ReportEngineModelDefine.md # 模型定义文档 ├── ReportEngineModule.cs # Prism 模块入口 └── XP.ReportEngine.csproj ``` -## 快速开始 - -### 1. 字体说明 - -模块使用 Windows 系统自带字体(微软雅黑 + Arial),无需额外添加字体文件。详见 `Documents/FontFilesGuidance.md`。 - -### 2. 通过 DI 容器调用 - -模块通过 `ReportEngineModule` 自动注册到 Prism DI 容器,其他模块可直接注入 `IReportGenerator` 使用: - -```csharp -public class MyService -{ - private readonly IReportGenerator _reportGenerator; - - public MyService(IReportGenerator reportGenerator) - { - _reportGenerator = reportGenerator; - } - - public async Task GenerateReport() - { - var context = new ReportContext - { - Metadata = new ReportMetadata - { - ReportId = "RPT-20250511-001", - InspectionDate = DateTime.Now, - SampleName = "PCB-001", - OperatorName = "张三" - }, - Properties = new Dictionary - { - ["sampleName"] = "PCB-001" - } - }; - - var options = new ReportGenerationOptions - { - TemplatePath = "Templates/StandardReportTemplate.json", - OutputFilePath = @"D:\Reports\report.pdf", - Format = ReportOutputFormat.Pdf - }; - - var result = await _reportGenerator.GenerateAsync(context, options); - - if (result.IsSuccess) - { - // PDF 已保存到 OutputFilePath - } - else - { - // result.ErrorMessage 包含错误信息 - } - } -} -``` - -### 3. 使用数据适配器 - -将 XP.ImageProcessing 处理器输出转换为 ReportContext: - -```csharp -var adapter = container.Resolve(); - -var processorOutputs = new List -{ - new ProcessorOutput - { - ProcessorType = "BgaVoidRateProcessor", - OutputData = new Dictionary - { - ["BgaCount"] = 120, - ["VoidRate"] = 0.035, - ["Classification"] = "Pass" - } - } -}; - -var metadata = new ReportMetadata -{ - ReportId = "RPT-20250511-001", - InspectionDate = DateTime.Now, - SampleName = "PCB-001", - OperatorName = "张三" -}; - -ReportContext context = adapter.Adapt(processorOutputs, metadata); -``` - ## 核心接口 | 接口 | 实现类 | 职责 | |------|--------|------| +| **`IReportService`** | **`ReportService`** | **门面接口,外部模块调用入口** | | `IReportGenerator` | `PdfReportGenerator` | 协调管线各阶段,生成 PDF | | `IReportGeneratorFactory` | `ReportGeneratorFactory` | 根据格式创建对应生成器 | | `ITemplateEngine` | `JsonTemplateEngine` | JSON 模板加载、反序列化、验证 | | `IDataBinder` | `ExpressionDataBinder` | `${}` 表达式解析与数据绑定 | | `ILayoutEngine` | `PageLayoutEngine` | 分页、元素定位、表格跨页 | -| `IPdfRenderer` | `ITextPdfRenderer` | 使用 iText 7 渲染 PDF | -| `IReportDataAdapter` | `ProcessorDataAdapter` | OutputData → ReportContext 转换 | +| `IPdfRenderer` | `ITextPdfRenderer` | 使用 iText7 渲染 PDF | +| `IReportDataAdapter` | `ProcessorDataAdapter` | ProcessorOutput → ReportContext 转换 | + +## 架构分层 + +``` +┌─────────────────────────────────────────────────────┐ +│ 外部调用方(XP.App、其他模块) │ +│ 注入 IReportService,传入 ReportRequest │ +└──────────────────────┬──────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────┐ +│ ReportService(门面层) │ +│ 生成报告ID → 解析输出路径 → 数据适配 → 注入配置数据 │ +└──────────────────────┬──────────────────────────────┘ + │ +┌──────────────────────▼──────────────────────────────┐ +│ PdfReportGenerator(管线协调层) │ +│ 模板加载 → 数据绑定 → 排版计算 → PDF 渲染 → 保存 │ +└─────────────────────────────────────────────────────┘ +``` + +## 快速开始 + +详细使用指南请参阅 `Documents/Guidance.md`。 + +### 最简调用(通过 IReportService) + +```csharp +public class MyService +{ + private readonly IReportService _reportService; + + public MyService(IReportService reportService) + { + _reportService = reportService; + } + + public async Task GenerateReport(List outputs) + { + var request = new ReportRequest + { + ProcessorOutputs = outputs, + Metadata = new ReportMetadata + { + SampleName = "PCB-001", + OperatorName = "张三", + InspectionDate = DateTime.Now + } + }; + + var result = await _reportService.GenerateAsync(request); + + if (result.IsSuccess) + { + // result.OutputFilePath — 生成的 PDF 路径 + // result.ReportId — 报告编号 + } + } +} +``` + +## 预热机制 + +模块在 Prism 初始化时自动在后台执行一次预热(`WarmUpAsync`),触发 iText7 初始化、字体加载、JIT 编译等一次性开销,避免用户首次生成报告时卡顿。 + +预热与正式生成通过 `SemaphoreSlim` 互斥锁串行执行,不会出现并发冲突。 ## 数据绑定表达式 @@ -174,35 +173,32 @@ ReportContext context = adapter.Adapt(processorOutputs, metadata); | `${functionName(param)}` | 格式化函数 | `${formatDate(inspectionDate)}` | | `${loc:ResourceKey}` | 本地化键解析 | `${loc:Report_Title}` | -内置格式化函数: -- `formatDate(value)` — 根据当前语言格式化日期 -- `formatNumber(value, decimals)` — 根据当前语言格式化数字 -- `formatPercent(value)` — 格式化百分比 - ## 多语言支持 支持三种语言:简体中文(zh-CN)、繁体中文(zh-TW)、英文(en-US)。 通过 `ILocalizationService` 自动解析 `${loc:Key}` 表达式为当前语言文本。模块在初始化时注册资源源到 Fallback Chain。 -## 模板定义 +## 模板页面类型 -标准模板位于 `Templates/StandardReportTemplate.json`,包含以下页面类型: -- `homepage` — 报告首页(元数据 + 摘要表格) -- `metricData` — 距离测量数据页 -- `bgaInspection` — BGA 焊球检测页(含数据表格) -- `voidInspection` — 空隙检测页(含数据表格) -- `viaFillInspection` — 通孔填锡检测页 +标准模板 `StandardReportTemplate.json` 包含以下页面类型: -模板 JSON 结构需包含三个必需顶层字段:`document`、`pages`、`styles`。 +| 页面类型 | 说明 | +|---------|------| +| `homepage` | 报告首页(公司信息 + 元数据 + 汇总表格) | +| `summary` | 检测结果汇总页 | +| `metricData` | 距离测量数据页 | +| `bgaInspection` | BGA 焊球检测页(含数据表格) | +| `voidInspection` | 空隙检测页(含数据表格) | +| `viaFillInspection` | 通孔填锡检测页 | ## 错误处理 -模块采用结果对象模式(Result Pattern),所有公共方法返回 `ReportResult` 而非抛出异常: -- `ReportResult.Success(stream)` — 成功,包含 PDF MemoryStream -- `ReportResult.Failure(message, ex)` — 失败,包含错误信息和可选异常 +模块采用结果对象模式(Result Pattern): +- `ReportServiceResult.Success(path, reportId)` — 成功 +- `ReportServiceResult.Failure(message, ex)` — 失败 -非致命性问题(缺失属性、未定义样式、图像缺失)会记录警告日志并继续执行,不会中断报告生成。 +非致命性问题(缺失属性、图像缺失)会记录警告日志并继续执行,不会中断报告生成。 ## 构建 @@ -210,3 +206,12 @@ ReportContext context = adapter.Adapt(processorOutputs, metadata); cd XplorePlane dotnet build XP.ReportEngine/XP.ReportEngine.csproj ``` + +## 相关文档 + +- `Documents/Guidance.md` — 详细使用指南(IReportService 调用方式) +- `Documents/TemplateDevelopment.md` — 模板开发指南(新增/自定义模板) +- `Documents/App.config.example` — 配置文件示例 +- `Documents/FontFilesGuidance.md` — 字体方案说明 +- `Documents/XP.ReportEngineDesign.md` — 架构设计文档 +- `Documents/XP.ReportEngineModelDefine.md` — 模型定义文档 diff --git a/XP.ReportEngine/Documents/TemplateDevelopment.md b/XP.ReportEngine/Documents/TemplateDevelopment.md new file mode 100644 index 0000000..cdc3b6d --- /dev/null +++ b/XP.ReportEngine/Documents/TemplateDevelopment.md @@ -0,0 +1,403 @@ +# 报告模板开发指南 | Template Development Guide + +## 1. 概述 + +XP.ReportEngine 使用 JSON 格式定义报告模板。模板描述了 PDF 报告的页面结构、元素布局、数据绑定和样式定义。 + +模板文件存放在 `XP.ReportEngine/Templates/` 目录下,通过 `App.config` 中的 `Report:TemplatePath` 配置项指定使用哪个模板。 + +## 2. 新增模板步骤 + +### 2.1 创建模板文件 + +1. 在 `Templates/` 目录下创建新的 JSON 文件,如 `CustomReportTemplate.json` +2. 在 `.csproj` 中确认模板文件会被复制到输出目录(已有通配规则): + ```xml + + PreserveNewest + + ``` +3. 修改 `App.config` 中的 `Report:TemplatePath` 指向新模板: + ```xml + + ``` + +### 2.2 模板验证规则 + +模板加载后会自动验证,必须满足以下条件: +- 包含 `document` 顶层字段(页面设置) +- 包含 `pages` 顶层字段(至少一个页面定义) +- 包含 `styles` 顶层字段(样式字典) + +验证失败时报告生成会返回错误,不会生成 PDF。 + +## 3. 模板 JSON 结构 + +```json +{ + "document": { ... }, // 必需:文档级设置(页面尺寸、边距、页眉页脚) + "pages": [ ... ], // 必需:页面定义数组 + "styles": { ... } // 必需:样式定义字典 +} +``` + +## 4. document 配置 + +```json +{ + "document": { + "pageSize": "A4", + "orientation": "Portrait", + "margins": { "top": 40, "bottom": 20, "left": 20, "right": 20 }, + "header": { + "enabled": true, + "left": ["标题文本", "第二行文本"], + "right": ["右侧文本"], + "rightImageKey": "companyLogo", + "leftImageKey": "softwareLogo", + "fontSize": 7, + "color": "#666666", + "showLine": true + }, + "footer": { + "enabled": true, + "left": ["公司名称"], + "right": ["{currentPage} / {totalPages}"], + "fontSize": 8, + "color": "#666666", + "showLine": true + } + } +} +``` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `pageSize` | string | `"A4"` | 页面尺寸 | +| `orientation` | string | `"Portrait"` | 方向:Portrait / Landscape | +| `margins` | object | `{top:20, bottom:20, left:20, right:20}` | 边距(mm) | +| `header` | object | null | 页眉配置(首页不显示) | +| `footer` | object | null | 页脚配置(首页不显示) | + +### 页眉/页脚字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `enabled` | bool | 是否启用 | +| `left` | string[] | 左侧文本行(支持 `${}` 绑定) | +| `right` | string[] | 右侧文本行(支持 `${}` 绑定) | +| `leftImageKey` | string | 左侧图像的 dataKey | +| `rightImageKey` | string | 右侧图像的 dataKey | +| `fontSize` | float | 字体大小 | +| `color` | string | 字体颜色(十六进制) | +| `showLine` | bool | 是否显示分隔线 | + +页脚特殊占位符: +- `{currentPage}` — 当前页码 +- `{totalPages}` — 总页数 + +## 5. pages 页面定义 + +```json +{ + "pages": [ + { + "type": "homepage", + "elements": [ ... ] + }, + { + "type": "customPage", + "elements": [ ... ] + } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `type` | string | 页面类型标识(`homepage` 类型不显示页眉页脚) | +| `elements` | array | 页面内的元素列表 | + +### 内置页面类型 + +| 类型 | 说明 | 特殊行为 | +|------|------|---------| +| `homepage` | 首页 | 不显示页眉页脚 | +| `summary` | 汇总页 | 无 | +| `metricData` | 距离测量页 | 无 | +| `bgaInspection` | BGA 检测页 | 无 | +| `voidInspection` | 空隙检测页 | 无 | +| `viaFillInspection` | 通孔填锡页 | 无 | + +你可以自定义任意 `type` 名称,只有 `homepage` 有特殊行为(不显示页眉页脚)。 + +## 6. 元素类型 + +### 6.1 text — 文本元素 + +```json +{ + "type": "text", + "content": "${loc:Report_Title}", + "style": "heading", + "positioning": "flow", + "align": "center", + "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `content` | string | 文本内容,支持 `${}` 绑定表达式 | +| `style` | string | 引用 `styles` 中定义的样式名 | +| `align` | string | 对齐:left / center / right | +| `colorRules` | object | 条件颜色规则(内容包含关键词时变色) | + +### 6.2 image — 图像元素 + +```json +{ + "type": "image", + "dataKey": "workpieceImage", + "size": [160, 110], + "border": true, + "align": "center", + "style": "imageDefault", + "positioning": "flow" +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `dataKey` | string | 图像数据键(对应 `ReportContext.Images` 中的键) | +| `size` | float[2] | [宽, 高](mm),图像会等比缩放适应 | +| `border` | bool | 是否显示边框 | +| `align` | string | 对齐:left / center / right | + +图像缺失时会渲染灰色占位矩形,不会中断报告生成。 + +### 6.3 table — 表格元素 + +```json +{ + "type": "table", + "dataKey": "bgaBallsTable", + "positioning": "flow", + "size": [170, 0], + "style": "tableDefault", + "columns": [ + { "header": "序号", "field": "index", "width": 25, "align": "center" }, + { "header": "气泡率", "field": "voidRate", "width": 40, "align": "center" }, + { "header": "面积", "field": "area", "width": 40, "align": "center" }, + { + "header": "分类", + "field": "classification", + "width": 35, + "align": "center", + "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } + } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `dataKey` | string | 表格数据键(对应 `ReportContext.Properties` 中的 `List>` 数据) | +| `size` | float[2] | [宽, 高](mm),高度为 0 表示自动 | +| `columns` | array | 列定义数组 | + +#### 列定义(ColumnDefinition) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `header` | string | 表头文本(支持 `${}` 绑定) | +| `field` | string | 数据字段名(对应行数据字典中的键) | +| `width` | float | 列宽(mm) | +| `align` | string | 对齐:left / center / right | +| `colorRules` | object | 条件颜色规则(单元格值匹配时变色) | + +### 6.4 row — 水平布局容器 + +```json +{ + "type": "row", + "size": [170, 100], + "widths": [6, 4], + "positioning": "flow", + "children": [ + { + "type": "column", "align": "left", + "children": [ + { "type": "image", "dataKey": "myImage", "size": [110, 90] } + ] + }, + { + "type": "column", "align": "left", + "children": [ + { "type": "text", "content": "文本内容", "style": "body" } + ] + } + ] +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `size` | float[2] | [总宽, 总高](mm) | +| `widths` | float[] | 列宽比例数组(如 `[6, 4]` 表示 60%:40%) | +| `children` | array | 子列元素(`type: "column"`) | + +### 6.5 spacer — 间距元素 + +```json +{ "type": "spacer", "size": [170, 10], "positioning": "flow" } +``` + +用于在元素之间添加垂直间距。`size[1]` 为间距高度(mm)。 + +### 6.6 divider — 分隔线 + +```json +{ "type": "divider", "positioning": "flow" } +``` + +渲染一条水平分隔线。 + +### 6.7 pagebreak — 强制分页 + +```json +{ "type": "pagebreak" } +``` + +在当前位置强制插入分页符。 + +## 7. 样式定义 + +```json +{ + "styles": { + "heading": { + "font": "auto", + "size": 16, + "bold": true, + "italic": false, + "color": "#333333", + "align": "left", + "marginTop": 0, + "marginBottom": 3, + "paddingLeft": 0, + "lineHeight": 0, + "backgroundColor": "" + } + } +} +``` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `font` | string | `"auto"` | 字体(`auto` 根据语言自动选择) | +| `size` | float | 12 | 字体大小(pt) | +| `bold` | bool | false | 粗体 | +| `italic` | bool | false | 斜体 | +| `color` | string | `"#000000"` | 字体颜色(十六进制) | +| `align` | string | `"left"` | 对齐:left / center / right | +| `backgroundColor` | string | — | 背景色(十六进制) | +| `marginTop` | float | 0 | 上边距(mm) | +| `marginBottom` | float | 0 | 下边距(mm) | +| `paddingLeft` | float | 0 | 左缩进(mm) | +| `lineHeight` | float | 0 | 行高倍数(0 = 默认) | + +元素引用未定义的样式名时,会使用默认样式(12pt、黑色、左对齐),不会报错。 + +## 8. 数据绑定表达式 + +模板中的 `content`、`header` 等文本字段支持 `${}` 绑定表达式: + +| 语法 | 说明 | 示例 | +|------|------|------| +| `${key}` | 从 Properties 中取值 | `${sampleName}` | +| `${metadata.field}` | 从 Metadata 中取值 | `${metadata.reportId}` | +| `${loc:Key}` | 多语言资源键 | `${loc:Report_Title}` | +| `${formatDate(value)}` | 格式化日期 | `${formatDate(metadata.inspectionDate)}` | +| `${formatNumber(value, decimals)}` | 格式化数字 | `${formatNumber(totalArea, 2)}` | +| `${formatPercent(value)}` | 格式化百分比 | `${formatPercent(voidRate)}` | + +### 数据来源 + +绑定表达式从 `ReportContext` 中查找数据: +- `Properties` 字典 — 扁平化的键值对(由 `ProcessorDataAdapter` 从处理器输出转换而来) +- `Metadata` — 报告元数据对象 +- `Images` 字典 — 图像数据(通过 `dataKey` 引用) + +### ProcessorDataAdapter 输出的键名 + +| 处理器类型 | Properties 中的键 | Images 中的键 | 表格 dataKey | +|-----------|------------------|--------------|-------------| +| LineMeasurementProcessor | measurementType, point1, point2, pixelDistance, actualDistance, unit, angle | lineMeasurementImage | — | +| BgaVoidRateProcessor | bgaCount, voidRate, fillRate, totalBgaArea, totalVoidArea, voidLimit, classification | bgaInspectionImage | bgaBallsTable | +| VoidMeasurementProcessor | roiArea, totalVoidArea, voidRate, voidLimit, voidCount, maxVoidArea, classification | voidInspectionImage | voidsTable | +| FillRateProcessor | fillRate, voidRate, fullDistance, fillDistance, thtLimit, classification, e1-e4 | viaFillImage | — | + +### ReportService 自动注入的键 + +| 键 | 来源 | 说明 | +|----|------|------| +| `CompanyName` | ReportConfig | 公司名称 | +| `SoftwareName` | ReportConfig | 软件名称 | +| `companyLogo` | ReportConfig.CompanyLogo | 公司 Logo(Images) | +| `softwareLogo` | ReportConfig.SoftwareLogo | 软件 Logo(Images) | +| `summaryTable` | 自动生成 | 首页汇总表数据 | + +## 9. 定位方式 + +| 值 | 说明 | +|----|------| +| `"flow"` | 流式布局,元素按顺序从上到下排列(推荐) | +| `"absolute"` | 绝对定位,使用 `position` 坐标(不推荐,兼容性差) | + +建议所有元素使用 `"positioning": "flow"`。 + +## 10. 完整模板示例(最小化) + +```json +{ + "document": { + "pageSize": "A4", + "orientation": "Portrait", + "margins": { "top": 20, "bottom": 20, "left": 20, "right": 20 } + }, + "pages": [ + { + "type": "homepage", + "elements": [ + { "type": "text", "content": "检测报告", "style": "title", "positioning": "flow" }, + { "type": "text", "content": "报告编号:${metadata.reportId}", "style": "body", "positioning": "flow" }, + { "type": "text", "content": "检测日期:${formatDate(metadata.inspectionDate)}", "style": "body", "positioning": "flow" } + ] + }, + { + "type": "dataPage", + "elements": [ + { "type": "text", "content": "检测数据", "style": "heading", "positioning": "flow" }, + { "type": "image", "dataKey": "inspectionImage", "size": [150, 100], "positioning": "flow" } + ] + } + ], + "styles": { + "title": { "size": 24, "bold": true, "align": "center" }, + "heading": { "size": 16, "bold": true }, + "body": { "size": 12 } + } +} +``` + +## 11. 注意事项 + +1. **尺寸单位**:所有尺寸(size、margins、width)单位为 **mm**(毫米) +2. **颜色格式**:使用十六进制格式 `#RRGGBB`(如 `#FF0000` 为红色) +3. **表格自动跨页**:当表格数据行超出当前页面剩余空间时,排版引擎会自动分页 +4. **图像缺失容错**:图像 dataKey 对应的数据不存在时,渲染占位矩形,不中断生成 +5. **样式缺失容错**:引用未定义的样式名时使用默认样式,不中断生成 +6. **绑定表达式缺失**:`${}` 表达式对应的数据不存在时,替换为空字符串 +7. **首页特殊处理**:`type: "homepage"` 的页面不显示页眉页脚 +8. **JSON 编码**:模板文件必须使用 **UTF-8** 编码保存 diff --git a/XP.ReportEngine/Interfaces/IReportService.cs b/XP.ReportEngine/Interfaces/IReportService.cs new file mode 100644 index 0000000..8b8d63f --- /dev/null +++ b/XP.ReportEngine/Interfaces/IReportService.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using XP.ReportEngine.Models; + +namespace XP.ReportEngine.Interfaces +{ + /// + /// 报告服务接口(门面)| Report service interface (Facade) + /// 提供完整的报告生成流程,外部模块通过此接口调用报告功能 + /// Provides complete report generation workflow for external modules + /// + /// + /// 使用示例 | Usage example: + /// + /// var request = new ReportRequest + /// { + /// ProcessorOutputs = processorOutputs, + /// Metadata = new ReportMetadata + /// { + /// SampleName = "PCB-001", + /// OperatorName = "Operator", + /// InspectionDate = DateTime.Now + /// } + /// }; + /// var result = await _reportService.GenerateAsync(request); + /// + /// + public interface IReportService + { + /// + /// 生成报告 | Generate report + /// 执行完整流程:生成报告ID → 数据适配 → 上下文组装 → 管线生成 → 文件保存 + /// Executes full workflow: generate report ID → data adaptation → context assembly → pipeline generation → file saving + /// + /// 报告生成请求 | Report generation request + /// 报告生成结果 | Report generation result + Task GenerateAsync(ReportRequest request); + + /// + /// 预热报告引擎(建议在应用启动后台调用)| Warm up report engine (recommended to call in background on app startup) + /// 触发 iText7 初始化、字体加载、JIT 编译等一次性开销,避免首次生成报告时卡顿 + /// Triggers iText7 initialization, font loading, JIT compilation to avoid first-run latency + /// + Task WarmUpAsync(); + } +} diff --git a/XP.ReportEngine/Models/ReportRequest.cs b/XP.ReportEngine/Models/ReportRequest.cs new file mode 100644 index 0000000..0c3f50a --- /dev/null +++ b/XP.ReportEngine/Models/ReportRequest.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace XP.ReportEngine.Models +{ + /// + /// 报告生成请求 | Report generation request + /// 封装外部调用方生成报告所需的全部输入参数 + /// Encapsulates all input parameters needed by external callers to generate a report + /// + public class ReportRequest + { + /// + /// 处理器输出数据列表 | List of processor output data + /// + public List ProcessorOutputs { get; set; } = new(); + + /// + /// 报告元数据(产品名、操作员、描述等)| Report metadata (product name, operator, description, etc.) + /// + public ReportMetadata Metadata { get; set; } + + /// + /// 额外图像数据(如工件整体图、自定义图像)| Additional image data (e.g., workpiece overview, custom images) + /// 键为 dataKey,值为图像数据 + /// Key is dataKey, value is image data + /// + public Dictionary AdditionalImages { get; set; } = new(); + + /// + /// 输出文件路径(可选)| Output file path (optional) + /// 为空时根据 ReportConfig 和 FileNameParameters 自动生成 + /// When empty, auto-generated based on ReportConfig and FileNameParameters + /// + public string OutputFilePath { get; set; } + + /// + /// 文件名占位符参数(可选)| File name placeholder parameters (optional) + /// 用于 ReportConfig.FileNamePattern 中的占位符替换 + /// Used for placeholder replacement in ReportConfig.FileNamePattern + /// + public Dictionary FileNameParameters { get; set; } = new(); + + /// + /// 自定义属性(可选)| Custom properties (optional) + /// 额外的键值对数据,会合并到 ReportContext.Properties 中 + /// Additional key-value data merged into ReportContext.Properties + /// + public Dictionary CustomProperties { get; set; } = new(); + } +} diff --git a/XP.ReportEngine/Models/ReportServiceResult.cs b/XP.ReportEngine/Models/ReportServiceResult.cs new file mode 100644 index 0000000..622a412 --- /dev/null +++ b/XP.ReportEngine/Models/ReportServiceResult.cs @@ -0,0 +1,49 @@ +using System; + +namespace XP.ReportEngine.Models +{ + /// + /// 报告服务结果 | Report service result + /// 封装报告生成的最终结果信息 + /// Encapsulates the final result of report generation + /// + public class ReportServiceResult + { + /// + /// 是否成功 | Whether successful + /// + public bool IsSuccess { get; set; } + + /// + /// 输出文件路径(成功时有值)| Output file path (has value when successful) + /// + public string OutputFilePath { get; set; } + + /// + /// 报告编号 | Report ID + /// + public string ReportId { get; set; } + + /// + /// 错误信息(失败时有值)| Error message (has value when failed) + /// + public string ErrorMessage { get; set; } + + /// + /// 异常对象(失败时有值)| Exception object (has value when failed) + /// + public Exception Exception { get; set; } + + /// + /// 创建成功结果 | Create success result + /// + public static ReportServiceResult Success(string outputFilePath, string reportId) + => new() { IsSuccess = true, OutputFilePath = outputFilePath, ReportId = reportId }; + + /// + /// 创建失败结果 | Create failure result + /// + public static ReportServiceResult Failure(string message, Exception ex = null) + => new() { IsSuccess = false, ErrorMessage = message, Exception = ex }; + } +} diff --git a/XP.ReportEngine/ReportEngineModule.cs b/XP.ReportEngine/ReportEngineModule.cs index 79701d8..a6cd48a 100644 --- a/XP.ReportEngine/ReportEngineModule.cs +++ b/XP.ReportEngine/ReportEngineModule.cs @@ -37,6 +37,11 @@ namespace XP.ReportEngine // Initialize LocalizationHelper to use ILocalizationService for string lookup (supports Fallback Chain) LocalizationHelper.Initialize(localizationService); + // 后台预热报告引擎(触发 iText7 初始化、字体加载、JIT 编译,避免首次生成卡顿) + // Background warm-up report engine (triggers iText7 init, font loading, JIT to avoid first-run latency) + var reportService = containerProvider.Resolve(); + _ = System.Threading.Tasks.Task.Run(() => reportService.WarmUpAsync()); + System.Console.WriteLine("[ReportEngineModule] 模块已初始化 | Module initialized"); } @@ -62,6 +67,9 @@ namespace XP.ReportEngine // 注册报告生成器工厂(单例)| Register report generator factory (singleton) containerRegistry.RegisterSingleton(); + // 注册报告服务门面(单例)| Register report service facade (singleton) + containerRegistry.RegisterSingleton(); + // 注册模板引擎(瞬态)| Register template engine (transient) containerRegistry.Register(); diff --git a/XP.ReportEngine/Services/ITextPdfRenderer.cs b/XP.ReportEngine/Services/ITextPdfRenderer.cs index 28ac981..f97f21a 100644 --- a/XP.ReportEngine/Services/ITextPdfRenderer.cs +++ b/XP.ReportEngine/Services/ITextPdfRenderer.cs @@ -85,6 +85,12 @@ namespace XP.ReportEngine.Services _logger.Info("开始 PDF 渲染,共 {PageCount} 页 | Starting PDF rendering, {PageCount} pages", pages?.Count ?? 0); _currentTemplate = template; + // 每次渲染重置字体,避免跨 PdfDocument 复用导致 "belongs to other PDF document" 错误 + // Reset fonts on each render to avoid cross-PdfDocument reuse error + _fontsInitialized = false; + _cjkFont = null; + _westernFont = null; + var memoryStream = new MemoryStream(); try @@ -130,6 +136,8 @@ namespace XP.ReportEngine.Services { for (int i = 0; i < pages.Count; i++) { + var pageStopwatch = System.Diagnostics.Stopwatch.StartNew(); + if (i > 0) { // 添加新页面 | Add new page @@ -148,6 +156,10 @@ namespace XP.ReportEngine.Services } RenderPage(document, pages[i]); + + pageStopwatch.Stop(); + _logger.Info("第 {PageIndex}/{TotalPages} 页渲染完成,类型: {PageType},元素数: {ElementCount},耗时: {ElapsedMs}ms | Page {PageIndex}/{TotalPages} rendered, type: {PageType}, elements: {ElementCount}, elapsed: {ElapsedMs}ms", + i + 1, pages.Count, pages[i].PageType ?? "unknown", pages[i].Elements?.Count ?? 0, pageStopwatch.ElapsedMilliseconds); } } @@ -157,9 +169,13 @@ namespace XP.ReportEngine.Services headerFooterHandler.WriteTotal(pdfDocument); } - // 仅关闭 Document(它会级联关闭 PdfDocument 和 PdfWriter) - // Only close Document (it cascades to PdfDocument and PdfWriter) + // 关闭文档(触发字体子集化嵌入 + PDF 交叉引用表写入 + 流压缩) + // Close document (triggers font subsetting + PDF cross-reference table writing + stream compression) + _logger.Info("开始关闭文档(字体嵌入 + 压缩)| Starting document close (font embedding + compression)"); + var closeStopwatch = System.Diagnostics.Stopwatch.StartNew(); document.Close(); + closeStopwatch.Stop(); + _logger.Info("文档关闭完成,耗时: {ElapsedMs}ms | Document close completed, elapsed: {ElapsedMs}ms", closeStopwatch.ElapsedMilliseconds); // 重置流位置以便后续读取 | Reset stream position for subsequent reading memoryStream.Position = 0; diff --git a/XP.ReportEngine/Services/ProcessorDataAdapter.cs b/XP.ReportEngine/Services/ProcessorDataAdapter.cs index a5fe73c..5e794e5 100644 --- a/XP.ReportEngine/Services/ProcessorDataAdapter.cs +++ b/XP.ReportEngine/Services/ProcessorDataAdapter.cs @@ -187,7 +187,7 @@ namespace XP.ReportEngine.Services /// /// 将 BgaBalls 列表转换为表格行 | Convert BgaBalls list to table rows - /// 每个焊球一行,包含 index、voidRate、classification + /// 每个焊球一行,包含 index、voidRate、area、classification /// private List> ConvertBgaBallsToTableRows(Dictionary data) { @@ -206,6 +206,7 @@ namespace XP.ReportEngine.Services { ["index"] = i + 1, ["voidRate"] = GetNestedValue(ball, "VoidRate", 0.0), + ["area"] = GetNestedValue(ball, "Area", 0.0), ["classification"] = GetNestedValue(ball, "Classification", string.Empty) }; tableRows.Add(row); diff --git a/XP.ReportEngine/Services/ReportService.cs b/XP.ReportEngine/Services/ReportService.cs new file mode 100644 index 0000000..20c68b5 --- /dev/null +++ b/XP.ReportEngine/Services/ReportService.cs @@ -0,0 +1,390 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using XP.Common.Logging.Interfaces; +using XP.ReportEngine.Configs; +using XP.ReportEngine.Interfaces; +using XP.ReportEngine.Models; + +namespace XP.ReportEngine.Services +{ + /// + /// 报告服务实现(门面)| Report service implementation (Facade) + /// 协调报告生成的完整流程,将 UI 无关的业务逻辑封装为可复用服务 + /// Orchestrates the complete report generation workflow, encapsulating UI-independent business logic as a reusable service + /// + public class ReportService : IReportService + { + private readonly IReportGenerator _reportGenerator; + private readonly IReportDataAdapter _dataAdapter; + private readonly ILoggerService _logger; + private readonly ReportIdGenerator _reportIdGenerator; + private readonly ReportConfig _reportConfig; + + /// + /// 生成互斥锁,防止并发渲染导致 iText7 字体对象跨文档引用错误 + /// Generation mutex to prevent concurrent rendering causing iText7 cross-document font reference errors + /// + private readonly SemaphoreSlim _generateLock = new(1, 1); + + /// + /// 构造函数 | Constructor + /// + /// 报告生成器 | Report generator + /// 数据适配器 | Data adapter + /// 日志服务 | Logger service + /// 报告编号生成器 | Report ID generator + /// 报告配置 | Report config + public ReportService( + IReportGenerator reportGenerator, + IReportDataAdapter dataAdapter, + ILoggerService logger, + ReportIdGenerator reportIdGenerator, + ReportConfig reportConfig) + { + _reportGenerator = reportGenerator ?? throw new ArgumentNullException(nameof(reportGenerator)); + _dataAdapter = dataAdapter ?? throw new ArgumentNullException(nameof(dataAdapter)); + _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); + _reportIdGenerator = reportIdGenerator ?? throw new ArgumentNullException(nameof(reportIdGenerator)); + _reportConfig = reportConfig ?? throw new ArgumentNullException(nameof(reportConfig)); + } + + /// + /// 生成报告 | Generate report + /// + public async Task GenerateAsync(ReportRequest request) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + // 获取互斥锁,防止并发渲染 | Acquire mutex to prevent concurrent rendering + await _generateLock.WaitAsync(); + try + { + _logger.Info("报告服务开始生成 | Report service starting generation"); + + // 步骤 1:生成报告编号 | Step 1: Generate report ID + var reportId = GenerateReportId(request); + _logger.Info("报告编号: {ReportId} | Report ID: {ReportId}", reportId); + + // 步骤 2:确定输出路径 | Step 2: Determine output path + var outputPath = ResolveOutputPath(request, reportId); + _logger.Info("输出路径: {Path} | Output path: {Path}", outputPath); + + // 步骤 3:数据适配 | Step 3: Data adaptation + var metadata = request.Metadata ?? new ReportMetadata(); + if (string.IsNullOrEmpty(metadata.ReportId)) + { + metadata.ReportId = reportId; + } + if (metadata.InspectionDate == default) + { + metadata.InspectionDate = DateTime.Now; + } + + var context = _dataAdapter.Adapt(request.ProcessorOutputs ?? new List(), metadata); + _logger.Info("数据适配完成 | Data adaptation completed"); + + // 步骤 4:注入额外图像 | Step 4: Inject additional images + InjectAdditionalImages(context, request); + + // 步骤 5:注入配置中的 Logo 和公司信息 | Step 5: Inject logo and company info from config + InjectConfigData(context); + + // 步骤 6:注入自定义属性 | Step 6: Inject custom properties + InjectCustomProperties(context, request); + + // 步骤 7:生成首页汇总表 | Step 7: Generate homepage summary table + context.Properties["summaryTable"] = CreateSummaryTableData(context); + + // 步骤 8:调用管线生成 PDF | Step 8: Call pipeline to generate PDF + var templatePath = _reportConfig.GetResolvedTemplatePath(); + var options = new ReportGenerationOptions + { + TemplatePath = templatePath, + OutputFilePath = outputPath, + Format = ReportOutputFormat.Pdf + }; + + var genResult = await _reportGenerator.GenerateAsync(context, options); + + // 步骤 9:处理结果 | Step 9: Handle result + if (genResult.IsSuccess) + { + _logger.Info("报告生成成功: {Path} | Report generated successfully: {Path}", outputPath); + return ReportServiceResult.Success(outputPath, reportId); + } + else + { + _logger.Error(null, "报告生成失败: {Message} | Report generation failed: {Message}", genResult.ErrorMessage); + return ReportServiceResult.Failure(genResult.ErrorMessage, genResult.Exception); + } + } + catch (Exception ex) + { + _logger.Error(ex, "报告服务异常: {Message} | Report service exception: {Message}", ex.Message); + return ReportServiceResult.Failure($"报告生成过程中发生异常: {ex.Message}", ex); + } + finally + { + _generateLock.Release(); + } + } + + /// + /// 预热报告引擎 | Warm up report engine + /// 通过生成一个最小化的空白 PDF 来触发所有一次性初始化: + /// iText7 程序集加载、BouncyCastle 注册、字体子系统初始化、JIT 编译 + /// + public async Task WarmUpAsync() + { + // 获取互斥锁,确保预热与正式生成不并发 | Acquire mutex to ensure warm-up doesn't overlap with generation + await _generateLock.WaitAsync(); + try + { + _logger.Info("报告引擎预热开始 | Report engine warm-up started"); + + await Task.Run(() => + { + // 加载模板(触发 JSON 反序列化 JIT)| Load template (triggers JSON deserialization JIT) + var templatePath = _reportConfig.GetResolvedTemplatePath(); + if (!File.Exists(templatePath)) + { + _logger.Warn("预热跳过:模板文件不存在 {Path} | Warm-up skipped: template not found {Path}", templatePath); + return; + } + + // 构建最小上下文 | Build minimal context + var context = new ReportContext + { + Metadata = new ReportMetadata + { + ReportId = "WARMUP", + InspectionDate = DateTime.Now, + SampleName = "WarmUp", + OperatorName = "System" + }, + ResultGroups = new List(), + Images = new Dictionary(), + Properties = new Dictionary + { + ["summaryTable"] = new List>() + } + }; + + var options = new ReportGenerationOptions + { + TemplatePath = templatePath, + OutputFilePath = null, // 不保存文件 | Don't save file + Format = ReportOutputFormat.Pdf + }; + + // 执行完整管线(触发 iText7 初始化 + 字体加载 + JIT) + // Execute full pipeline (triggers iText7 init + font loading + JIT) + var result = _reportGenerator.GenerateAsync(context, options).GetAwaiter().GetResult(); + + // 释放预热产生的流 | Dispose warm-up stream + result.PdfStream?.Dispose(); + }); + + _logger.Info("报告引擎预热完成 | Report engine warm-up completed"); + } + catch (Exception ex) + { + // 预热失败不影响正常功能 | Warm-up failure doesn't affect normal functionality + _logger.Warn("报告引擎预热失败(不影响正常使用)| Report engine warm-up failed (doesn't affect normal usage): {Message}", ex.Message); + } + finally + { + _generateLock.Release(); + } + } + + #region 私有方法 | Private Methods + + /// + /// 生成报告编号 | Generate report ID + /// 优先使用请求中已有的 ReportId,否则自动生成 + /// Prefer existing ReportId from request, otherwise auto-generate + /// + private string GenerateReportId(ReportRequest request) + { + // 如果 Metadata 中已有 ReportId,直接使用 | If Metadata already has ReportId, use it directly + if (request.Metadata != null && !string.IsNullOrEmpty(request.Metadata.ReportId)) + { + return request.Metadata.ReportId; + } + + // 如果 FileNameParameters 中已有 ReportId,直接使用 | If FileNameParameters already has ReportId, use it + if (request.FileNameParameters != null && + request.FileNameParameters.TryGetValue("ReportId", out var existingId) && + !string.IsNullOrEmpty(existingId)) + { + return existingId; + } + + // 自动生成 | Auto-generate + return _reportIdGenerator.GenerateNext(); + } + + /// + /// 解析输出文件路径 | Resolve output file path + /// + private string ResolveOutputPath(ReportRequest request, string reportId) + { + // 如果请求中指定了输出路径,直接使用 | If output path specified in request, use it directly + if (!string.IsNullOrEmpty(request.OutputFilePath)) + { + // 确保目录存在 | Ensure directory exists + var dir = Path.GetDirectoryName(request.OutputFilePath); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + return request.OutputFilePath; + } + + // 使用配置和参数自动生成路径 | Auto-generate path using config and parameters + var fileNameParams = request.FileNameParameters ?? new Dictionary(); + + // 确保 ReportId 在参数中 | Ensure ReportId is in parameters + if (!fileNameParams.ContainsKey("ReportId")) + { + fileNameParams["ReportId"] = reportId; + } + + return _reportConfig.ResolveOutputFilePath(fileNameParams); + } + + /// + /// 注入额外图像到上下文 | Inject additional images into context + /// + private void InjectAdditionalImages(ReportContext context, ReportRequest request) + { + if (request.AdditionalImages == null || request.AdditionalImages.Count == 0) + return; + + foreach (var kvp in request.AdditionalImages) + { + if (kvp.Value != null) + { + context.Images[kvp.Key] = kvp.Value; + _logger.Debug("注入额外图像: {Key} | Injected additional image: {Key}", kvp.Key); + } + } + } + + /// + /// 注入配置中的 Logo 和公司信息 | Inject logo and company info from config + /// + private void InjectConfigData(ReportContext context) + { + // 注入公司名称 | Inject company name + if (!string.IsNullOrEmpty(_reportConfig.CompanyName)) + { + context.Properties["CompanyName"] = _reportConfig.CompanyName; + } + + // 注入软件名称 | Inject software name + if (!string.IsNullOrEmpty(_reportConfig.SoftwareName)) + { + context.Properties["SoftwareName"] = _reportConfig.SoftwareName; + } + + // 注入公司 Logo | Inject company logo + if (!string.IsNullOrEmpty(_reportConfig.CompanyLogo)) + { + var logoPath = Path.IsPathRooted(_reportConfig.CompanyLogo) + ? _reportConfig.CompanyLogo + : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _reportConfig.CompanyLogo); + + if (File.Exists(logoPath)) + { + context.Images["companyLogo"] = new ImageData + { + SourceType = ImageSourceType.FilePath, + FilePath = logoPath + }; + _logger.Info("公司 Logo 已加载: {Path} | Company logo loaded: {Path}", logoPath); + } + else + { + _logger.Warn("公司 Logo 文件不存在: {Path} | Company logo file not found: {Path}", logoPath); + } + } + + // 注入软件 Logo | Inject software logo + if (!string.IsNullOrEmpty(_reportConfig.SoftwareLogo)) + { + var softwareLogoPath = Path.IsPathRooted(_reportConfig.SoftwareLogo) + ? _reportConfig.SoftwareLogo + : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _reportConfig.SoftwareLogo); + + if (File.Exists(softwareLogoPath)) + { + context.Images["softwareLogo"] = new ImageData + { + SourceType = ImageSourceType.FilePath, + FilePath = softwareLogoPath + }; + _logger.Info("软件 Logo 已加载: {Path} | Software logo loaded: {Path}", softwareLogoPath); + } + else + { + _logger.Warn("软件 Logo 文件不存在: {Path} | Software logo file not found: {Path}", softwareLogoPath); + } + } + } + + /// + /// 注入自定义属性 | Inject custom properties + /// + private void InjectCustomProperties(ReportContext context, ReportRequest request) + { + if (request.CustomProperties == null || request.CustomProperties.Count == 0) + return; + + foreach (var kvp in request.CustomProperties) + { + context.Properties[kvp.Key] = kvp.Value; + } + } + + /// + /// 根据 ReportContext 中的结果分组生成首页汇总表数据 + /// Generate homepage summary table data from ReportContext result groups + /// + private List> CreateSummaryTableData(ReportContext context) + { + var rows = new List>(); + + foreach (var group in context.ResultGroups) + { + var inspectionType = group.ProcessorType switch + { + "LineMeasurementProcessor" => "线测量 | Line Measurement", + "BgaVoidRateProcessor" => "BGA 气泡率检测 | BGA Void Rate", + "VoidMeasurementProcessor" => "空隙测量 | Void Measurement", + "FillRateProcessor" => "通孔填锡率 | Via Fill Rate", + _ => group.ProcessorType + }; + + var classification = string.IsNullOrEmpty(group.Classification) ? "N/A" : group.Classification; + var status = classification == "Pass" ? "[PASS] 合格" : classification == "Fail" ? "[FAIL] 不合格" : "—"; + + rows.Add(new Dictionary + { + ["inspectionType"] = inspectionType, + ["classification"] = classification, + ["status"] = status + }); + } + + return rows; + } + + #endregion + } +} diff --git a/XP.ReportEngine/ViewModels/ReportDemoViewModel.cs b/XP.ReportEngine/ViewModels/ReportDemoViewModel.cs index 852fb78..f7d75b6 100644 --- a/XP.ReportEngine/ViewModels/ReportDemoViewModel.cs +++ b/XP.ReportEngine/ViewModels/ReportDemoViewModel.cs @@ -17,13 +17,12 @@ namespace XP.ReportEngine.ViewModels { /// /// 报告生成演示窗口 ViewModel | Report generation demo window ViewModel - /// 演示如何使用 XP.ReportEngine 生成 PDF 报告 - /// Demonstrates how to use XP.ReportEngine to generate PDF reports + /// 演示如何使用 IReportService 生成 PDF 报告 + /// Demonstrates how to use IReportService to generate PDF reports /// public class ReportDemoViewModel : BindableBase { - private readonly IReportGenerator _reportGenerator; - private readonly IReportDataAdapter _dataAdapter; + private readonly IReportService _reportService; private readonly IPdfViewerService _pdfViewerService; private readonly IPdfPrintService _pdfPrintService; private readonly ILoggerService _logger; @@ -167,16 +166,14 @@ namespace XP.ReportEngine.ViewModels /// 构造函数 | Constructor /// public ReportDemoViewModel( - IReportGenerator reportGenerator, - IReportDataAdapter dataAdapter, + IReportService reportService, IPdfViewerService pdfViewerService, IPdfPrintService pdfPrintService, ILoggerService logger, ReportIdGenerator reportIdGenerator, ReportConfig reportConfig) { - _reportGenerator = reportGenerator ?? throw new ArgumentNullException(nameof(reportGenerator)); - _dataAdapter = dataAdapter ?? throw new ArgumentNullException(nameof(dataAdapter)); + _reportService = reportService ?? throw new ArgumentNullException(nameof(reportService)); _pdfViewerService = pdfViewerService ?? throw new ArgumentNullException(nameof(pdfViewerService)); _pdfPrintService = pdfPrintService ?? throw new ArgumentNullException(nameof(pdfPrintService)); _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); @@ -274,123 +271,52 @@ namespace XP.ReportEngine.ViewModels var processorOutputs = CreateMockProcessorOutputs(); - // 步骤 2:数据适配 | Step 2: Data adaptation - progressWindow.UpdateProgress("正在适配数据...", 30); + // 步骤 2:构建报告请求 | Step 2: Build report request + progressWindow.UpdateProgress("正在组装报告数据...", 30); await Task.Delay(200); - var metadata = new ReportMetadata + var request = new ReportRequest { - ReportId = fileNameParams["ReportId"], - InspectionDate = DateTime.Now, - SampleName = ProductName, - OperatorName = OperatorName, - Description = Description + ProcessorOutputs = processorOutputs, + Metadata = new ReportMetadata + { + ReportId = fileNameParams["ReportId"], + InspectionDate = DateTime.Now, + SampleName = ProductName, + OperatorName = OperatorName, + Description = Description + }, + OutputFilePath = outputPath, + FileNameParameters = fileNameParams }; - var context = _dataAdapter.Adapt(processorOutputs, metadata); - - // 注入公司 Logo 图像数据 | Inject company logo image data - if (!string.IsNullOrEmpty(_reportConfig.CompanyLogo)) - { - var logoPath = System.IO.Path.IsPathRooted(_reportConfig.CompanyLogo) - ? _reportConfig.CompanyLogo - : System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _reportConfig.CompanyLogo); - - if (File.Exists(logoPath)) - { - context.Images["companyLogo"] = new ImageData - { - SourceType = ImageSourceType.FilePath, - FilePath = logoPath - }; - _logger.Info("公司 Logo 已加载:{Path} | Company logo loaded: {Path}", logoPath); - } - else - { - _logger.Warn("公司 Logo 文件不存在:{Path} | Company logo file not found: {Path}", logoPath); - } - } - - // 注入公司名称到上下文属性 | Inject company name into context properties - if (!string.IsNullOrEmpty(_reportConfig.CompanyName)) - { - context.Properties["CompanyName"] = _reportConfig.CompanyName; - } - - // 注入软件名称到上下文属性 | Inject software name into context properties - if (!string.IsNullOrEmpty(_reportConfig.SoftwareName)) - { - context.Properties["SoftwareName"] = _reportConfig.SoftwareName; - } - - // 注入软件 Logo 图像数据 | Inject software logo image data - if (!string.IsNullOrEmpty(_reportConfig.SoftwareLogo)) - { - var softwareLogoPath = System.IO.Path.IsPathRooted(_reportConfig.SoftwareLogo) - ? _reportConfig.SoftwareLogo - : System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _reportConfig.SoftwareLogo); - - if (File.Exists(softwareLogoPath)) - { - context.Images["softwareLogo"] = new ImageData - { - SourceType = ImageSourceType.FilePath, - FilePath = softwareLogoPath - }; - _logger.Info("软件 Logo 已加载:{Path} | Software logo loaded: {Path}", softwareLogoPath); - } - else - { - _logger.Warn("软件 Logo 文件不存在:{Path} | Software logo file not found: {Path}", softwareLogoPath); - } - } - // 注入工件整体图片 | Inject workpiece overview image var workpieceImagePath = System.IO.Path.Combine(MockImageDirectory, "OverView.png"); if (File.Exists(workpieceImagePath)) { - context.Images["workpieceImage"] = new ImageData + request.AdditionalImages["workpieceImage"] = new ImageData { SourceType = ImageSourceType.FilePath, FilePath = workpieceImagePath }; - _logger.Info("工件整体图片已加载:{Path} | Workpiece image loaded: {Path}", workpieceImagePath); - } - else - { - _logger.Warn("工件整体图片不存在:{Path} | Workpiece image not found: {Path}", workpieceImagePath); } - // 注入首页汇总表数据 | Inject homepage summary table data - context.Properties["summaryTable"] = CreateSummaryTableData(context); - - // 步骤 3:确定模板 | Step 3: Determine template - progressWindow.UpdateProgress("正在加载模板...", 50); + // 步骤 3:调用报告服务生成(在后台线程执行,避免阻塞 UI) + // Step 3: Call report service (on background thread to avoid blocking UI) + progressWindow.UpdateProgress("正在生成 PDF...", 60); await Task.Delay(200); - var templatePath = _reportConfig.GetResolvedTemplatePath(); + var genResult = await Task.Run(() => _reportService.GenerateAsync(request)); - var options = new ReportGenerationOptions - { - TemplatePath = templatePath, - OutputFilePath = outputPath, - Format = ReportOutputFormat.Pdf - }; - - // 步骤 4:生成 PDF | Step 4: Generate PDF - progressWindow.UpdateProgress("正在生成 PDF...", 70); - - var genResult = await _reportGenerator.GenerateAsync(context, options); - - // 步骤 5:处理结果 | Step 5: Handle result + // 步骤 4:处理结果 | Step 4: Handle result progressWindow.UpdateProgress("正在完成...", 95); await Task.Delay(200); if (genResult.IsSuccess) { - _lastOutputPath = outputPath; - StatusMessage = $"报告生成成功:{outputPath}"; - _logger.Info("报告生成成功:{Path} | Report generated successfully: {Path}", outputPath); + _lastOutputPath = genResult.OutputFilePath; + StatusMessage = $"报告生成成功:{genResult.OutputFilePath}"; + _logger.Info("报告生成成功:{Path} | Report generated successfully: {Path}", genResult.OutputFilePath); progressWindow.UpdateProgress("报告生成完成!", 100); await Task.Delay(500); @@ -400,7 +326,7 @@ namespace XP.ReportEngine.ViewModels { try { - _pdfViewerService.OpenViewer(outputPath); + _pdfViewerService.OpenViewer(genResult.OutputFilePath); } catch (Exception viewerEx) { @@ -687,41 +613,6 @@ namespace XP.ReportEngine.ViewModels }; } - /// - /// 根据 ReportContext 中的结果分组生成首页汇总表数据 - /// Generate homepage summary table data from ReportContext result groups - /// - /// 报告上下文 | Report context - /// 汇总表行数据 | Summary table row data - private List> CreateSummaryTableData(ReportContext context) - { - var rows = new List>(); - - foreach (var group in context.ResultGroups) - { - var inspectionType = group.ProcessorType switch - { - "LineMeasurementProcessor" => "线测量 | Line Measurement", - "BgaVoidRateProcessor" => "BGA 气泡率检测 | BGA Void Rate", - "VoidMeasurementProcessor" => "空隙测量 | Void Measurement", - "FillRateProcessor" => "通孔填锡率 | Via Fill Rate", - _ => group.ProcessorType - }; - - var classification = string.IsNullOrEmpty(group.Classification) ? "N/A" : group.Classification; - var status = classification == "Pass" ? "[PASS] 合格" : classification == "Fail" ? "[FAIL] 不合格" : "—"; - - rows.Add(new Dictionary - { - ["inspectionType"] = inspectionType, - ["classification"] = classification, - ["status"] = status - }); - } - - return rows; - } - #endregion } } diff --git a/XP.ReportEngine/Views/ReportDemoWindow.xaml b/XP.ReportEngine/Views/ReportDemoWindow.xaml index f0c0d97..8c28b9e 100644 --- a/XP.ReportEngine/Views/ReportDemoWindow.xaml +++ b/XP.ReportEngine/Views/ReportDemoWindow.xaml @@ -1,8 +1,9 @@ @@ -10,98 +11,98 @@ - + FontSize="18" FontWeight="SemiBold" Margin="0,0,0,16" + Foreground="#FF333333"/> - + - + - - - - - + + + + + - + - + - + - + - + - + - + - + - -