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"/>
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
+
+
+
+
-
-
-
- 使用说明:
-
- 1. 填写产品信息、设备信息和操作员
-
- 2. 点击"生成报告",系统将使用模拟检测数据生成 PDF
-
- 3. 生成完成后,可点击"查看 PDF"在阅读器中预览
-
- 4. 点击"打印报告"可将 PDF 发送到打印机
-
-
- 文件名模式(App.config 配置):
-
- {ReportId} {ProductName} {ProductCode} {WorkpieceSN}
-
- {CncProgram} {DeviceId} {MachineId} {Date} {Time} {Result}
-
-
- 重复文件处理:
- AutoIncrementOnDuplicate=true 时自动累加 (1)(2)...
-
-
-
-
-
+