报告XP.ReportEngine:新增 IReportService接口封装完整报告生成流程,支持外部类库直接调用;新增 ReportRequest、ReportServiceResult 请求/响应模型;新增引擎预热机制;PDF 生成改为 Task.Run 后台线程执行,解决进度窗口和主窗口卡死问题;完善文档。
This commit is contained in:
@@ -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
|
||||
<EmbeddedResource Include="Fonts\*.otf" />
|
||||
<EmbeddedResource Include="Fonts\*.ttf" />
|
||||
```
|
||||
3. 在 `ITextPdfRenderer.InitializeFonts()` 中扩展加载逻辑
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
# XP.ReportEngine 使用指南 | Usage Guidance
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文档说明如何在 XplorePlane 项目中使用 `XP.ReportEngine` 模块生成 PDF 检测报告。
|
||||
|
||||
模块提供两种调用方式:
|
||||
- **推荐**:通过 `IReportService` 门面接口(一行调用,自动处理所有细节)
|
||||
- **高级**:直接使用底层管线接口(`IReportGenerator`、`IReportDataAdapter` 等)
|
||||
|
||||
## 2. 前置条件
|
||||
|
||||
### 2.1 项目引用
|
||||
|
||||
在调用方项目的 `.csproj` 中添加项目引用:
|
||||
|
||||
```xml
|
||||
<ProjectReference Include="..\XP.ReportEngine\XP.ReportEngine.csproj" />
|
||||
```
|
||||
|
||||
### 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<string> GenerateInspectionReport(
|
||||
List<ProcessorOutput> 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<string, string>
|
||||
{
|
||||
["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<string, ImageData>
|
||||
{
|
||||
// 工件整体图(首页显示)
|
||||
["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<string, object>
|
||||
{
|
||||
["batchNumber"] = "BATCH-2025-001",
|
||||
["inspectionStation"] = "Station-A"
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 4. ReportRequest 完整字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `ProcessorOutputs` | `List<ProcessorOutput>` | 是 | 处理器输出数据列表 |
|
||||
| `Metadata` | `ReportMetadata` | 是 | 报告元数据 |
|
||||
| `OutputFilePath` | `string` | 否 | 输出路径,为空时自动生成 |
|
||||
| `FileNameParameters` | `Dictionary<string, string>` | 否 | 文件名占位符参数 |
|
||||
| `AdditionalImages` | `Dictionary<string, ImageData>` | 否 | 额外图像数据 |
|
||||
| `CustomProperties` | `Dictionary<string, object>` | 否 | 自定义属性 |
|
||||
|
||||
### ReportMetadata 字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `ReportId` | `string` | 报告编号(为空时自动生成 RPT-yyyyMMdd-NNN) |
|
||||
| `InspectionDate` | `DateTime` | 检测日期 |
|
||||
| `SampleName` | `string` | 样品/产品名称 |
|
||||
| `OperatorName` | `string` | 操作员 |
|
||||
| `Description` | `string` | 描述信息 |
|
||||
|
||||
### ProcessorOutput 字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `ProcessorType` | `string` | 处理器类型标识 |
|
||||
| `OutputData` | `Dictionary<string, object>` | 输出数据字典 |
|
||||
| `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<IReportDataAdapter>();
|
||||
var context = adapter.Adapt(processorOutputs, metadata);
|
||||
|
||||
// 2. 手动修改上下文
|
||||
context.Properties["customKey"] = "customValue";
|
||||
context.Images["myImage"] = new ImageData { ... };
|
||||
|
||||
// 3. 调用管线生成
|
||||
var generator = container.Resolve<IReportGenerator>();
|
||||
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<IReportService>();
|
||||
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` 中的模板结构定义。
|
||||
+121
-116
@@ -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<string, object>
|
||||
{
|
||||
["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<IReportDataAdapter>();
|
||||
|
||||
var processorOutputs = new List<ProcessorOutput>
|
||||
{
|
||||
new ProcessorOutput
|
||||
{
|
||||
ProcessorType = "BgaVoidRateProcessor",
|
||||
OutputData = new Dictionary<string, object>
|
||||
{
|
||||
["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<ProcessorOutput> 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` — 模型定义文档
|
||||
|
||||
@@ -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
|
||||
<Content Include="Templates\*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
```
|
||||
3. 修改 `App.config` 中的 `Report:TemplatePath` 指向新模板:
|
||||
```xml
|
||||
<add key="Report:TemplatePath" value="Templates\CustomReportTemplate.json" />
|
||||
```
|
||||
|
||||
### 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<Dictionary<string, object>>` 数据) |
|
||||
| `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** 编码保存
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Threading.Tasks;
|
||||
using XP.ReportEngine.Models;
|
||||
|
||||
namespace XP.ReportEngine.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告服务接口(门面)| Report service interface (Facade)
|
||||
/// 提供完整的报告生成流程,外部模块通过此接口调用报告功能
|
||||
/// Provides complete report generation workflow for external modules
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 使用示例 | Usage example:
|
||||
/// <code>
|
||||
/// var request = new ReportRequest
|
||||
/// {
|
||||
/// ProcessorOutputs = processorOutputs,
|
||||
/// Metadata = new ReportMetadata
|
||||
/// {
|
||||
/// SampleName = "PCB-001",
|
||||
/// OperatorName = "Operator",
|
||||
/// InspectionDate = DateTime.Now
|
||||
/// }
|
||||
/// };
|
||||
/// var result = await _reportService.GenerateAsync(request);
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public interface IReportService
|
||||
{
|
||||
/// <summary>
|
||||
/// 生成报告 | Generate report
|
||||
/// 执行完整流程:生成报告ID → 数据适配 → 上下文组装 → 管线生成 → 文件保存
|
||||
/// Executes full workflow: generate report ID → data adaptation → context assembly → pipeline generation → file saving
|
||||
/// </summary>
|
||||
/// <param name="request">报告生成请求 | Report generation request</param>
|
||||
/// <returns>报告生成结果 | Report generation result</returns>
|
||||
Task<ReportServiceResult> GenerateAsync(ReportRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 预热报告引擎(建议在应用启动后台调用)| 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
|
||||
/// </summary>
|
||||
Task WarmUpAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XP.ReportEngine.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告生成请求 | Report generation request
|
||||
/// 封装外部调用方生成报告所需的全部输入参数
|
||||
/// Encapsulates all input parameters needed by external callers to generate a report
|
||||
/// </summary>
|
||||
public class ReportRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理器输出数据列表 | List of processor output data
|
||||
/// </summary>
|
||||
public List<ProcessorOutput> ProcessorOutputs { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 报告元数据(产品名、操作员、描述等)| Report metadata (product name, operator, description, etc.)
|
||||
/// </summary>
|
||||
public ReportMetadata Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 额外图像数据(如工件整体图、自定义图像)| Additional image data (e.g., workpiece overview, custom images)
|
||||
/// 键为 dataKey,值为图像数据
|
||||
/// Key is dataKey, value is image data
|
||||
/// </summary>
|
||||
public Dictionary<string, ImageData> AdditionalImages { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件路径(可选)| Output file path (optional)
|
||||
/// 为空时根据 ReportConfig 和 FileNameParameters 自动生成
|
||||
/// When empty, auto-generated based on ReportConfig and FileNameParameters
|
||||
/// </summary>
|
||||
public string OutputFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件名占位符参数(可选)| File name placeholder parameters (optional)
|
||||
/// 用于 ReportConfig.FileNamePattern 中的占位符替换
|
||||
/// Used for placeholder replacement in ReportConfig.FileNamePattern
|
||||
/// </summary>
|
||||
public Dictionary<string, string> FileNameParameters { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 自定义属性(可选)| Custom properties (optional)
|
||||
/// 额外的键值对数据,会合并到 ReportContext.Properties 中
|
||||
/// Additional key-value data merged into ReportContext.Properties
|
||||
/// </summary>
|
||||
public Dictionary<string, object> CustomProperties { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
namespace XP.ReportEngine.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告服务结果 | Report service result
|
||||
/// 封装报告生成的最终结果信息
|
||||
/// Encapsulates the final result of report generation
|
||||
/// </summary>
|
||||
public class ReportServiceResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否成功 | Whether successful
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件路径(成功时有值)| Output file path (has value when successful)
|
||||
/// </summary>
|
||||
public string OutputFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 报告编号 | Report ID
|
||||
/// </summary>
|
||||
public string ReportId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息(失败时有值)| Error message (has value when failed)
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 异常对象(失败时有值)| Exception object (has value when failed)
|
||||
/// </summary>
|
||||
public Exception Exception { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建成功结果 | Create success result
|
||||
/// </summary>
|
||||
public static ReportServiceResult Success(string outputFilePath, string reportId)
|
||||
=> new() { IsSuccess = true, OutputFilePath = outputFilePath, ReportId = reportId };
|
||||
|
||||
/// <summary>
|
||||
/// 创建失败结果 | Create failure result
|
||||
/// </summary>
|
||||
public static ReportServiceResult Failure(string message, Exception ex = null)
|
||||
=> new() { IsSuccess = false, ErrorMessage = message, Exception = ex };
|
||||
}
|
||||
}
|
||||
@@ -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<IReportService>();
|
||||
_ = 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<IReportGeneratorFactory, ReportGeneratorFactory>();
|
||||
|
||||
// 注册报告服务门面(单例)| Register report service facade (singleton)
|
||||
containerRegistry.RegisterSingleton<IReportService, ReportService>();
|
||||
|
||||
// 注册模板引擎(瞬态)| Register template engine (transient)
|
||||
containerRegistry.Register<ITemplateEngine, JsonTemplateEngine>();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -187,7 +187,7 @@ namespace XP.ReportEngine.Services
|
||||
|
||||
/// <summary>
|
||||
/// 将 BgaBalls 列表转换为表格行 | Convert BgaBalls list to table rows
|
||||
/// 每个焊球一行,包含 index、voidRate、classification
|
||||
/// 每个焊球一行,包含 index、voidRate、area、classification
|
||||
/// </summary>
|
||||
private List<Dictionary<string, object>> ConvertBgaBallsToTableRows(Dictionary<string, object> data)
|
||||
{
|
||||
@@ -206,6 +206,7 @@ namespace XP.ReportEngine.Services
|
||||
{
|
||||
["index"] = i + 1,
|
||||
["voidRate"] = GetNestedValue<double>(ball, "VoidRate", 0.0),
|
||||
["area"] = GetNestedValue<double>(ball, "Area", 0.0),
|
||||
["classification"] = GetNestedValue<string>(ball, "Classification", string.Empty)
|
||||
};
|
||||
tableRows.Add(row);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告服务实现(门面)| Report service implementation (Facade)
|
||||
/// 协调报告生成的完整流程,将 UI 无关的业务逻辑封装为可复用服务
|
||||
/// Orchestrates the complete report generation workflow, encapsulating UI-independent business logic as a reusable service
|
||||
/// </summary>
|
||||
public class ReportService : IReportService
|
||||
{
|
||||
private readonly IReportGenerator _reportGenerator;
|
||||
private readonly IReportDataAdapter _dataAdapter;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly ReportIdGenerator _reportIdGenerator;
|
||||
private readonly ReportConfig _reportConfig;
|
||||
|
||||
/// <summary>
|
||||
/// 生成互斥锁,防止并发渲染导致 iText7 字体对象跨文档引用错误
|
||||
/// Generation mutex to prevent concurrent rendering causing iText7 cross-document font reference errors
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim _generateLock = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="reportGenerator">报告生成器 | Report generator</param>
|
||||
/// <param name="dataAdapter">数据适配器 | Data adapter</param>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
/// <param name="reportIdGenerator">报告编号生成器 | Report ID generator</param>
|
||||
/// <param name="reportConfig">报告配置 | Report config</param>
|
||||
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<ReportService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_reportIdGenerator = reportIdGenerator ?? throw new ArgumentNullException(nameof(reportIdGenerator));
|
||||
_reportConfig = reportConfig ?? throw new ArgumentNullException(nameof(reportConfig));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成报告 | Generate report
|
||||
/// </summary>
|
||||
public async Task<ReportServiceResult> 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<ProcessorOutput>(), 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预热报告引擎 | Warm up report engine
|
||||
/// 通过生成一个最小化的空白 PDF 来触发所有一次性初始化:
|
||||
/// iText7 程序集加载、BouncyCastle 注册、字体子系统初始化、JIT 编译
|
||||
/// </summary>
|
||||
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<InspectionResultGroup>(),
|
||||
Images = new Dictionary<string, ImageData>(),
|
||||
Properties = new Dictionary<string, object>
|
||||
{
|
||||
["summaryTable"] = new List<Dictionary<string, object>>()
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 生成报告编号 | Generate report ID
|
||||
/// 优先使用请求中已有的 ReportId,否则自动生成
|
||||
/// Prefer existing ReportId from request, otherwise auto-generate
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析输出文件路径 | Resolve output file path
|
||||
/// </summary>
|
||||
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<string, string>();
|
||||
|
||||
// 确保 ReportId 在参数中 | Ensure ReportId is in parameters
|
||||
if (!fileNameParams.ContainsKey("ReportId"))
|
||||
{
|
||||
fileNameParams["ReportId"] = reportId;
|
||||
}
|
||||
|
||||
return _reportConfig.ResolveOutputFilePath(fileNameParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注入额外图像到上下文 | Inject additional images into context
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注入配置中的 Logo 和公司信息 | Inject logo and company info from config
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注入自定义属性 | Inject custom properties
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ReportContext 中的结果分组生成首页汇总表数据
|
||||
/// Generate homepage summary table data from ReportContext result groups
|
||||
/// </summary>
|
||||
private List<Dictionary<string, object>> CreateSummaryTableData(ReportContext context)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object>>();
|
||||
|
||||
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<string, object>
|
||||
{
|
||||
["inspectionType"] = inspectionType,
|
||||
["classification"] = classification,
|
||||
["status"] = status
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,12 @@ namespace XP.ReportEngine.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告生成演示窗口 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
|
||||
/// </summary>
|
||||
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
|
||||
/// </summary>
|
||||
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<ReportDemoViewModel>() ?? 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
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ReportContext 中的结果分组生成首页汇总表数据
|
||||
/// Generate homepage summary table data from ReportContext result groups
|
||||
/// </summary>
|
||||
/// <param name="context">报告上下文 | Report context</param>
|
||||
/// <returns>汇总表行数据 | Summary table row data</returns>
|
||||
private List<Dictionary<string, object>> CreateSummaryTableData(ReportContext context)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object>>();
|
||||
|
||||
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<string, object>
|
||||
{
|
||||
["inspectionType"] = inspectionType,
|
||||
["classification"] = classification,
|
||||
["status"] = status
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<Window x:Class="XP.ReportEngine.Views.ReportDemoWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
Title="报告生成演示 | Report Generation Demo"
|
||||
Width="560" Height="580"
|
||||
Width="580" Height="380"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize">
|
||||
<Grid Margin="20">
|
||||
@@ -10,98 +11,98 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题 | Title -->
|
||||
<TextBlock Grid.Row="0" Text="XP.ReportEngine 报告生成演示"
|
||||
FontSize="18" FontWeight="Bold" Margin="0,0,0,12"/>
|
||||
FontSize="18" FontWeight="SemiBold" Margin="0,0,0,16"
|
||||
Foreground="#FF333333"/>
|
||||
|
||||
<!-- 输入表单 | Input form -->
|
||||
<Grid Grid.Row="1" Margin="0,0,0,12">
|
||||
<Grid Grid.Row="1" Margin="0,0,0,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="20"/>
|
||||
<ColumnDefinition Width="24"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="32"/>
|
||||
<RowDefinition Height="32"/>
|
||||
<RowDefinition Height="32"/>
|
||||
<RowDefinition Height="32"/>
|
||||
<RowDefinition Height="32"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 第一列 | Column 1 -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="产品名称:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ProductName, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
<telerik:RadWatermarkTextBox Grid.Row="0" Grid.Column="1"
|
||||
Text="{Binding ProductName, UpdateSourceTrigger=PropertyChanged}"
|
||||
WatermarkContent="输入产品名称"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="产品类型码:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ProductCode, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
<telerik:RadWatermarkTextBox Grid.Row="1" Grid.Column="1"
|
||||
Text="{Binding ProductCode, UpdateSourceTrigger=PropertyChanged}"
|
||||
WatermarkContent="输入产品类型码"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="工件 SN 码:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding WorkpieceSN, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
<telerik:RadWatermarkTextBox Grid.Row="2" Grid.Column="1"
|
||||
Text="{Binding WorkpieceSN, UpdateSourceTrigger=PropertyChanged}"
|
||||
WatermarkContent="输入工件序列号"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="CNC 程序:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding CncProgram, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
<telerik:RadWatermarkTextBox Grid.Row="3" Grid.Column="1"
|
||||
Text="{Binding CncProgram, UpdateSourceTrigger=PropertyChanged}"
|
||||
WatermarkContent="输入 CNC 程序名"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="操作员:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding OperatorName, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
<telerik:RadWatermarkTextBox Grid.Row="4" Grid.Column="1"
|
||||
Text="{Binding OperatorName, UpdateSourceTrigger=PropertyChanged}"
|
||||
WatermarkContent="输入操作员"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
|
||||
<!-- 第二列 | Column 2 -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="3" Text="设备编号:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="0" Grid.Column="4" Text="{Binding DeviceId, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
<telerik:RadWatermarkTextBox Grid.Row="0" Grid.Column="4"
|
||||
Text="{Binding DeviceId, UpdateSourceTrigger=PropertyChanged}"
|
||||
WatermarkContent="输入设备编号"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="3" Text="生产机台号:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="4" Text="{Binding MachineId, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
<telerik:RadWatermarkTextBox Grid.Row="1" Grid.Column="4"
|
||||
Text="{Binding MachineId, UpdateSourceTrigger=PropertyChanged}"
|
||||
WatermarkContent="输入机台号"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="3" Text="描述:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="4" Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
<telerik:RadWatermarkTextBox Grid.Row="2" Grid.Column="4"
|
||||
Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}"
|
||||
WatermarkContent="输入描述信息"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 按钮区域 | Button area -->
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,12">
|
||||
<Button Content="生成报告" Command="{Binding GenerateReportCommand}"
|
||||
Width="100" Height="32" Margin="0,0,10,0"/>
|
||||
<Button Content="查看 PDF" Command="{Binding OpenViewerCommand}"
|
||||
Width="100" Height="32" Margin="0,0,10,0"/>
|
||||
<Button Content="打印报告" Command="{Binding PrintReportCommand}"
|
||||
Width="100" Height="32"/>
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,16">
|
||||
<telerik:RadButton Content="生成报告" Command="{Binding GenerateReportCommand}"
|
||||
Width="110" Height="30" Margin="0,0,12,0"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
<telerik:RadButton Content="查看 PDF" Command="{Binding OpenViewerCommand}"
|
||||
Width="110" Height="30" Margin="0,0,12,0"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
<telerik:RadButton Content="打印报告" Command="{Binding PrintReportCommand}"
|
||||
Width="110" Height="30"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 说明区域 | Instructions -->
|
||||
<Border Grid.Row="3" BorderBrush="#DDDDDD" BorderThickness="1"
|
||||
Background="#F9F9F9" CornerRadius="4" Padding="10">
|
||||
<TextBlock TextWrapping="Wrap" FontSize="12" Foreground="#555555">
|
||||
<Run FontWeight="Bold">使用说明:</Run>
|
||||
<LineBreak/>
|
||||
<Run>1. 填写产品信息、设备信息和操作员</Run>
|
||||
<LineBreak/>
|
||||
<Run>2. 点击"生成报告",系统将使用模拟检测数据生成 PDF</Run>
|
||||
<LineBreak/>
|
||||
<Run>3. 生成完成后,可点击"查看 PDF"在阅读器中预览</Run>
|
||||
<LineBreak/>
|
||||
<Run>4. 点击"打印报告"可将 PDF 发送到打印机</Run>
|
||||
<LineBreak/>
|
||||
<LineBreak/>
|
||||
<Run FontWeight="Bold">文件名模式(App.config 配置):</Run>
|
||||
<LineBreak/>
|
||||
<Run>{ReportId} {ProductName} {ProductCode} {WorkpieceSN}</Run>
|
||||
<LineBreak/>
|
||||
<Run>{CncProgram} {DeviceId} {MachineId} {Date} {Time} {Result}</Run>
|
||||
<LineBreak/>
|
||||
<LineBreak/>
|
||||
<Run FontWeight="Bold">重复文件处理:</Run>
|
||||
<Run>AutoIncrementOnDuplicate=true 时自动累加 (1)(2)...</Run>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
|
||||
<!-- 状态栏 | Status bar -->
|
||||
<Border Grid.Row="4" Background="#F0F0F0" Margin="0,10,0,0" Padding="8,5">
|
||||
<TextBlock Text="{Binding StatusMessage}" FontSize="11" Foreground="#333333"
|
||||
<Border Grid.Row="3" Background="#F5F5F7" CornerRadius="4" Padding="10,8">
|
||||
<TextBlock Text="{Binding StatusMessage}" FontSize="12" Foreground="#FF333333"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user