修复字体文件在Terelik中无法pdf正确加载的问题,优化报告模板,新增丰富的测试i数据和图片,优化报告输出流程,根据优化修改文档和移除文件。
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.ReportEngine.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告引擎配置加载器(读取 App.config)| Report engine configuration loader (reads from App.config)
|
||||
/// </summary>
|
||||
public class ConfigLoader
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
public ConfigLoader(ILoggerService logger)
|
||||
{
|
||||
_logger = logger.ForModule<ConfigLoader>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 App.config 加载报告引擎配置 | Load report engine configuration from App.config
|
||||
/// </summary>
|
||||
/// <param name="prefix">配置前缀,默认为 "Report" | Configuration prefix, default is "Report"</param>
|
||||
/// <returns>报告配置对象 | Report configuration object</returns>
|
||||
public ReportConfig LoadReportConfig(string prefix = "Report")
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Info("开始从 App.config 加载报告引擎配置,前缀: {Prefix} | Loading report config from App.config, prefix: {Prefix}", prefix);
|
||||
|
||||
var config = new ReportConfig();
|
||||
|
||||
// 读取输出目录 | Read output directory
|
||||
var outputDirectory = ConfigurationManager.AppSettings[$"{prefix}:OutputDirectory"];
|
||||
if (!string.IsNullOrEmpty(outputDirectory))
|
||||
{
|
||||
config.OutputDirectory = outputDirectory;
|
||||
}
|
||||
|
||||
// 读取模板路径 | Read template path
|
||||
var templatePath = ConfigurationManager.AppSettings[$"{prefix}:TemplatePath"];
|
||||
if (!string.IsNullOrEmpty(templatePath))
|
||||
{
|
||||
config.TemplatePath = templatePath;
|
||||
}
|
||||
|
||||
// 读取文件名模式 | Read file name pattern
|
||||
var fileNamePattern = ConfigurationManager.AppSettings[$"{prefix}:FileNamePattern"];
|
||||
if (!string.IsNullOrEmpty(fileNamePattern))
|
||||
{
|
||||
config.FileNamePattern = fileNamePattern;
|
||||
}
|
||||
|
||||
// 读取重复文件名自动累加设置 | Read auto-increment on duplicate setting
|
||||
var autoIncrement = ConfigurationManager.AppSettings[$"{prefix}:AutoIncrementOnDuplicate"];
|
||||
if (bool.TryParse(autoIncrement, out var autoIncrementValue))
|
||||
{
|
||||
config.AutoIncrementOnDuplicate = autoIncrementValue;
|
||||
}
|
||||
|
||||
// 读取自动打开设置 | Read auto-open setting
|
||||
var autoOpen = ConfigurationManager.AppSettings[$"{prefix}:AutoOpenAfterGenerate"];
|
||||
if (bool.TryParse(autoOpen, out var autoOpenValue))
|
||||
{
|
||||
config.AutoOpenAfterGenerate = autoOpenValue;
|
||||
}
|
||||
|
||||
// 读取页面尺寸 | Read page size
|
||||
var pageSize = ConfigurationManager.AppSettings[$"{prefix}:DefaultPageSize"];
|
||||
if (!string.IsNullOrEmpty(pageSize))
|
||||
{
|
||||
config.DefaultPageSize = pageSize;
|
||||
}
|
||||
|
||||
// 读取页面方向 | Read page orientation
|
||||
var orientation = ConfigurationManager.AppSettings[$"{prefix}:DefaultOrientation"];
|
||||
if (!string.IsNullOrEmpty(orientation))
|
||||
{
|
||||
config.DefaultOrientation = orientation;
|
||||
}
|
||||
|
||||
// 读取边距配置 | Read margin configuration
|
||||
var marginTop = ConfigurationManager.AppSettings[$"{prefix}:MarginTop"];
|
||||
if (float.TryParse(marginTop, out var marginTopValue))
|
||||
{
|
||||
config.MarginTop = marginTopValue;
|
||||
}
|
||||
|
||||
var marginBottom = ConfigurationManager.AppSettings[$"{prefix}:MarginBottom"];
|
||||
if (float.TryParse(marginBottom, out var marginBottomValue))
|
||||
{
|
||||
config.MarginBottom = marginBottomValue;
|
||||
}
|
||||
|
||||
var marginLeft = ConfigurationManager.AppSettings[$"{prefix}:MarginLeft"];
|
||||
if (float.TryParse(marginLeft, out var marginLeftValue))
|
||||
{
|
||||
config.MarginLeft = marginLeftValue;
|
||||
}
|
||||
|
||||
var marginRight = ConfigurationManager.AppSettings[$"{prefix}:MarginRight"];
|
||||
if (float.TryParse(marginRight, out var marginRightValue))
|
||||
{
|
||||
config.MarginRight = marginRightValue;
|
||||
}
|
||||
|
||||
// 读取公司名称 | Read company name
|
||||
var companyName = ConfigurationManager.AppSettings[$"{prefix}:CompanyName"];
|
||||
if (!string.IsNullOrEmpty(companyName))
|
||||
{
|
||||
config.CompanyName = companyName;
|
||||
}
|
||||
|
||||
// 读取公司 Logo 路径 | Read company logo path
|
||||
var companyLogo = ConfigurationManager.AppSettings[$"{prefix}:CompanyLogo"];
|
||||
if (!string.IsNullOrEmpty(companyLogo))
|
||||
{
|
||||
config.CompanyLogo = companyLogo;
|
||||
}
|
||||
|
||||
// 验证配置 | Validate configuration
|
||||
ValidateConfig(config);
|
||||
|
||||
_logger.Info("报告引擎配置加载成功:输出目录={OutputDir}, 模板={Template} | Report config loaded: OutputDir={OutputDir}, Template={Template}",
|
||||
config.OutputDirectory, config.TemplatePath);
|
||||
|
||||
return config;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn("加载报告引擎配置失败,使用默认配置 | Failed to load report config, using defaults: {Message}", ex.Message);
|
||||
return new ReportConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证配置参数 | Validate configuration parameters
|
||||
/// </summary>
|
||||
/// <param name="config">报告配置对象 | Report configuration object</param>
|
||||
private void ValidateConfig(ReportConfig config)
|
||||
{
|
||||
// 验证输出目录(不存在则尝试创建)| Validate output directory (create if not exists)
|
||||
if (string.IsNullOrWhiteSpace(config.OutputDirectory))
|
||||
{
|
||||
_logger.Warn("OutputDirectory 为空,使用默认值 | OutputDirectory is empty, using default");
|
||||
config.OutputDirectory = new ReportConfig().OutputDirectory;
|
||||
}
|
||||
|
||||
// 验证模板路径 | Validate template path
|
||||
if (string.IsNullOrWhiteSpace(config.TemplatePath))
|
||||
{
|
||||
_logger.Warn("TemplatePath 为空,使用默认值 | TemplatePath is empty, using default");
|
||||
config.TemplatePath = @"Templates\StandardReportTemplate.json";
|
||||
}
|
||||
|
||||
// 验证文件名模式 | Validate file name pattern
|
||||
if (string.IsNullOrWhiteSpace(config.FileNamePattern))
|
||||
{
|
||||
_logger.Warn("FileNamePattern 为空,使用默认值 | FileNamePattern is empty, using default");
|
||||
config.FileNamePattern = "{ReportId}";
|
||||
}
|
||||
|
||||
// 验证页面方向 | Validate page orientation
|
||||
if (config.DefaultOrientation != "Portrait" && config.DefaultOrientation != "Landscape")
|
||||
{
|
||||
_logger.Warn("DefaultOrientation 无效: {Value},使用默认值 Portrait | DefaultOrientation invalid: {Value}, using default Portrait", config.DefaultOrientation);
|
||||
config.DefaultOrientation = "Portrait";
|
||||
}
|
||||
|
||||
// 验证边距范围(0-100mm)| Validate margin range (0-100mm)
|
||||
config.MarginTop = ClampMargin(config.MarginTop, "MarginTop");
|
||||
config.MarginBottom = ClampMargin(config.MarginBottom, "MarginBottom");
|
||||
config.MarginLeft = ClampMargin(config.MarginLeft, "MarginLeft");
|
||||
config.MarginRight = ClampMargin(config.MarginRight, "MarginRight");
|
||||
|
||||
// 验证 Logo 路径(如果配置了则检查文件是否存在)| Validate logo path (check file exists if configured)
|
||||
if (!string.IsNullOrEmpty(config.CompanyLogo))
|
||||
{
|
||||
var logoPath = Path.IsPathRooted(config.CompanyLogo)
|
||||
? config.CompanyLogo
|
||||
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, config.CompanyLogo);
|
||||
|
||||
if (!File.Exists(logoPath))
|
||||
{
|
||||
_logger.Warn("公司 Logo 文件不存在: {Path},将不显示 Logo | Company logo file not found: {Path}, logo will not be displayed", logoPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将边距值限制在有效范围内 | Clamp margin value to valid range
|
||||
/// </summary>
|
||||
private float ClampMargin(float value, string name)
|
||||
{
|
||||
if (value < 0 || value > 100)
|
||||
{
|
||||
_logger.Warn("{Name} 超出有效范围 [0, 100]: {Value},使用默认值 20 | {Name} out of valid range [0, 100]: {Value}, using default 20", name, value);
|
||||
return 20f;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace XP.ReportEngine.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告引擎配置模型 | Report engine configuration model
|
||||
/// </summary>
|
||||
public class ReportConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告输出文件夹路径 | Report output directory path
|
||||
/// </summary>
|
||||
public string OutputDirectory { get; set; } = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
||||
"XplorePlane", "Reports");
|
||||
|
||||
/// <summary>
|
||||
/// 报告模板文件路径(相对或绝对)| Report template file path (relative or absolute)
|
||||
/// </summary>
|
||||
public string TemplatePath { get; set; } = @"Templates\StandardReportTemplate.json";
|
||||
|
||||
/// <summary>
|
||||
/// 输出文件名模式,支持占位符 | Output file name pattern, supports placeholders
|
||||
/// 支持的占位符 | Supported placeholders:
|
||||
/// {ReportId} - 报告编号(如 RPT-20250512-001)
|
||||
/// {CncProgram} - CNC 程序名称
|
||||
/// {ProductName} - 产品名称
|
||||
/// {ProductCode} - 产品类型码
|
||||
/// {WorkpieceSN} - 工件 SN 码
|
||||
/// {DeviceId} - 检测设备编号(本机)
|
||||
/// {MachineId} - 生产机台号
|
||||
/// {Date} - 日期(yyyyMMdd)
|
||||
/// {Time} - 时间(HHmmss)
|
||||
/// {Result} - 综合检测结论(Pass/Fail)
|
||||
/// </summary>
|
||||
public string FileNamePattern { get; set; } = "{ReportId}";
|
||||
|
||||
/// <summary>
|
||||
/// 文件名重复时是否自动累加序号 | Whether to auto-increment suffix when file name duplicates
|
||||
/// true: 重复时生成 filename(1).pdf, filename(2).pdf ...
|
||||
/// false: 直接覆盖同名文件
|
||||
/// </summary>
|
||||
public bool AutoIncrementOnDuplicate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 生成后是否自动打开 PDF 阅读器 | Whether to auto-open PDF viewer after generation
|
||||
/// </summary>
|
||||
public bool AutoOpenAfterGenerate { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 默认页面尺寸 | Default page size
|
||||
/// </summary>
|
||||
public string DefaultPageSize { get; set; } = "A4";
|
||||
|
||||
/// <summary>
|
||||
/// 默认页面方向(Portrait / Landscape)| Default page orientation
|
||||
/// </summary>
|
||||
public string DefaultOrientation { get; set; } = "Portrait";
|
||||
|
||||
/// <summary>
|
||||
/// 默认上边距(mm)| Default top margin (mm)
|
||||
/// </summary>
|
||||
public float MarginTop { get; set; } = 20f;
|
||||
|
||||
/// <summary>
|
||||
/// 默认下边距(mm)| Default bottom margin (mm)
|
||||
/// </summary>
|
||||
public float MarginBottom { get; set; } = 20f;
|
||||
|
||||
/// <summary>
|
||||
/// 默认左边距(mm)| Default left margin (mm)
|
||||
/// </summary>
|
||||
public float MarginLeft { get; set; } = 20f;
|
||||
|
||||
/// <summary>
|
||||
/// 默认右边距(mm)| Default right margin (mm)
|
||||
/// </summary>
|
||||
public float MarginRight { get; set; } = 20f;
|
||||
|
||||
/// <summary>
|
||||
/// 报告中显示的公司名称 | Company name displayed in report
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = "海克斯康制造智能技术(青岛)有限公司";
|
||||
|
||||
/// <summary>
|
||||
/// 公司 Logo 图片路径(可选,为空则不显示)| Company logo image path (optional, empty means no logo)
|
||||
/// </summary>
|
||||
public string CompanyLogo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取解析后的模板绝对路径 | Get resolved absolute template path
|
||||
/// 如果 TemplatePath 是相对路径,则基于应用程序目录解析
|
||||
/// If TemplatePath is relative, resolves based on application directory
|
||||
/// </summary>
|
||||
public string GetResolvedTemplatePath()
|
||||
{
|
||||
if (Path.IsPathRooted(TemplatePath))
|
||||
{
|
||||
return TemplatePath;
|
||||
}
|
||||
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, TemplatePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件名模式和上下文参数生成实际文件名 | Generate actual file name based on pattern and context parameters
|
||||
/// </summary>
|
||||
/// <param name="parameters">占位符参数字典 | Placeholder parameter dictionary</param>
|
||||
/// <returns>生成的文件名(不含扩展名)| Generated file name (without extension)</returns>
|
||||
public string ResolveFileName(Dictionary<string, string> parameters)
|
||||
{
|
||||
var fileName = FileNamePattern;
|
||||
|
||||
// 替换所有已知占位符 | Replace all known placeholders
|
||||
if (parameters != null)
|
||||
{
|
||||
foreach (var kvp in parameters)
|
||||
{
|
||||
fileName = fileName.Replace($"{{{kvp.Key}}}", SanitizeFileName(kvp.Value ?? ""));
|
||||
}
|
||||
}
|
||||
|
||||
// 替换日期和时间(始终可用)| Replace date and time (always available)
|
||||
fileName = fileName.Replace("{Date}", DateTime.Now.ToString("yyyyMMdd"));
|
||||
fileName = fileName.Replace("{Time}", DateTime.Now.ToString("HHmmss"));
|
||||
|
||||
// 清理未被替换的占位符(替换为空)| Clean up unreplaced placeholders
|
||||
fileName = System.Text.RegularExpressions.Regex.Replace(fileName, @"\{[^}]+\}", "");
|
||||
|
||||
// 移除连续的分隔符 | Remove consecutive separators
|
||||
fileName = System.Text.RegularExpressions.Regex.Replace(fileName, @"[_\-]{2,}", "_");
|
||||
fileName = fileName.Trim('_', '-');
|
||||
|
||||
return string.IsNullOrWhiteSpace(fileName) ? "Report" : fileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析最终输出文件完整路径(含重复累加逻辑)| Resolve final output file full path (with duplicate increment logic)
|
||||
/// </summary>
|
||||
/// <param name="parameters">占位符参数字典 | Placeholder parameter dictionary</param>
|
||||
/// <param name="extension">文件扩展名(含点号,如 ".pdf")| File extension (with dot, e.g. ".pdf")</param>
|
||||
/// <returns>最终输出文件完整路径 | Final output file full path</returns>
|
||||
public string ResolveOutputFilePath(Dictionary<string, string> parameters, string extension = ".pdf")
|
||||
{
|
||||
var baseName = ResolveFileName(parameters);
|
||||
var outputDir = OutputDirectory;
|
||||
|
||||
// 确保输出目录存在 | Ensure output directory exists
|
||||
if (!Directory.Exists(outputDir))
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(outputDir, baseName + extension);
|
||||
|
||||
// 重复累加逻辑 | Duplicate increment logic
|
||||
if (AutoIncrementOnDuplicate && File.Exists(filePath))
|
||||
{
|
||||
int counter = 1;
|
||||
string newPath;
|
||||
do
|
||||
{
|
||||
newPath = Path.Combine(outputDir, $"{baseName}({counter}){extension}");
|
||||
counter++;
|
||||
} while (File.Exists(newPath));
|
||||
|
||||
filePath = newPath;
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理文件名中的非法字符 | Sanitize illegal characters in file name
|
||||
/// </summary>
|
||||
private static string SanitizeFileName(string name)
|
||||
{
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
foreach (var c in invalidChars)
|
||||
{
|
||||
name = name.Replace(c, '_');
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<!-- ============================================================ -->
|
||||
<!-- 报告引擎配置 | Report Engine Configuration -->
|
||||
<!-- 前缀: Report -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
|
||||
<!-- 默认值: {我的文档}\XplorePlane\Reports | Default: {MyDocuments}\XplorePlane\Reports -->
|
||||
<add key="Report:OutputDirectory" value="D:\Reports\XplorePlane" />
|
||||
|
||||
<!-- 报告模板文件路径(相对于应用程序目录或绝对路径)| Template file path (relative to app dir or absolute) -->
|
||||
<!-- 默认值: Templates\StandardReportTemplate.json -->
|
||||
<add key="Report:TemplatePath" value="Templates\StandardReportTemplate.json" />
|
||||
|
||||
<!--
|
||||
输出文件名模式 | File name pattern
|
||||
支持以下占位符 | Supported placeholders:
|
||||
{ReportId} - 报告编号(如 RPT-20250512-001)| Report ID (e.g. RPT-20250512-001)
|
||||
{CncProgram} - CNC 程序名称 | CNC program name
|
||||
{ProductName} - 产品名称 | Product name
|
||||
{ProductCode} - 产品类型码 | Product type code
|
||||
{WorkpieceSN} - 工件 SN 码 | Workpiece serial number
|
||||
{DeviceId} - 检测设备编号(本机)| Inspection device ID (local machine)
|
||||
{MachineId} - 生产机台号 | Production machine ID
|
||||
{Date} - 日期(yyyyMMdd)| Date (yyyyMMdd)
|
||||
{Time} - 时间(HHmmss)| Time (HHmmss)
|
||||
{Result} - 综合检测结论(Pass/Fail)| Overall inspection result (Pass/Fail)
|
||||
|
||||
示例 | Examples:
|
||||
"{ReportId}" → RPT-20250512-001.pdf
|
||||
"{Date}_{ProductName}_{WorkpieceSN}" → 20250512_PCB-A01_SN20250001.pdf
|
||||
"{ProductCode}_{WorkpieceSN}_{Result}" → PCBA-X100_SN20250001_Pass.pdf
|
||||
"{MachineId}_{CncProgram}_{Date}_{Time}" → MC01_Prog001_20250512_143025.pdf
|
||||
"{DeviceId}_{Date}_{ReportId}" → XP-CT-001_20250512_RPT-20250512-001.pdf
|
||||
-->
|
||||
<add key="Report:FileNamePattern" value="{Date}_{ProductName}_{WorkpieceSN}_{ReportId}" />
|
||||
|
||||
<!--
|
||||
文件名重复时是否自动累加序号 | Auto-increment suffix when file name duplicates
|
||||
true: 重复时生成 filename(1).pdf, filename(2).pdf ... 不覆盖已有文件
|
||||
false: 直接覆盖同名文件
|
||||
-->
|
||||
<add key="Report:AutoIncrementOnDuplicate" value="true" />
|
||||
|
||||
<!-- 生成后是否自动打开 PDF 阅读器 | Auto-open PDF viewer after generation (true/false) -->
|
||||
<add key="Report:AutoOpenAfterGenerate" value="false" />
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- 页面布局配置 | Page Layout Configuration -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- 默认页面尺寸 | Default page size (A4) -->
|
||||
<add key="Report:DefaultPageSize" value="A4" />
|
||||
|
||||
<!-- 默认页面方向 | Default orientation (Portrait / Landscape) -->
|
||||
<add key="Report:DefaultOrientation" value="Portrait" />
|
||||
|
||||
<!-- 页面边距(mm),有效范围 0-100 | Page margins (mm), valid range 0-100 -->
|
||||
<add key="Report:MarginTop" value="20" />
|
||||
<add key="Report:MarginBottom" value="20" />
|
||||
<add key="Report:MarginLeft" value="20" />
|
||||
<add key="Report:MarginRight" value="20" />
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- 公司信息配置 | Company Information Configuration -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<!-- 报告中显示的公司名称 | Company name displayed in report -->
|
||||
<add key="Report:CompanyName" value="海克斯康制造智能技术(青岛)有限公司" />
|
||||
|
||||
<!-- 公司 Logo 图片路径(可选,为空则不显示)| Company logo path (optional, empty = no logo) -->
|
||||
<!-- 支持相对路径(基于应用程序目录)或绝对路径 | Supports relative (based on app dir) or absolute path -->
|
||||
<add key="Report:CompanyLogo" value="Resources\company_logo.png" />
|
||||
</appSettings>
|
||||
</configuration>
|
||||
@@ -1,54 +1,34 @@
|
||||
# 字体文件目录 | Font Files Directory
|
||||
# 字体方案说明 | Font Strategy
|
||||
|
||||
## 所需字体文件 | Required Font Files
|
||||
## 当前方案 | Current Approach
|
||||
|
||||
请将以下字体文件手动添加到 `XP.ReportEngine/Fonts/` 目录:
|
||||
XP.ReportEngine 使用 **Windows 系统自带字体** 生成 PDF,无需额外下载或嵌入字体文件。
|
||||
|
||||
1. **NotoSansCJKsc-Regular.otf** — 中文字体(CJK 字符支持)| Chinese font (CJK character support)
|
||||
2. **NotoSans-Regular.ttf** — 西文字体 | Western font
|
||||
| 语言 | 字体 | 文件 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 简体中文 / 繁体中文 | 微软雅黑 | `C:\Windows\Fonts\msyh.ttc` | Windows 10+ 自带,支持简繁体 |
|
||||
| 西文(英文等) | Arial | `C:\Windows\Fonts\arial.ttf` | Windows 自带 |
|
||||
|
||||
## 下载地址 | Download Links
|
||||
## 为什么使用系统字体 | Why System Fonts
|
||||
|
||||
- Noto Sans CJK: https://github.com/googlefonts/noto-cjk/releases
|
||||
- 下载 `NotoSansCJKsc-Regular.otf`(简体中文版本)
|
||||
- 文件大小约 16MB
|
||||
- Noto Sans: https://fonts.google.com/noto/specimen/Noto+Sans
|
||||
- 下载 Regular 字重的 TTF 文件
|
||||
- 文件大小约 500KB
|
||||
|
||||
## 下载步骤 | Download Steps
|
||||
|
||||
### NotoSansCJKsc-Regular.otf
|
||||
|
||||
1. 访问 https://github.com/googlefonts/noto-cjk/releases
|
||||
2. 找到最新 Release,展开 Assets
|
||||
3. 下载 `NotoSansCJKsc-Regular.otf`(或从 Sans 包中提取)
|
||||
4. 将文件放入 `XP.ReportEngine/Fonts/` 目录
|
||||
|
||||
### NotoSans-Regular.ttf
|
||||
|
||||
1. 访问 https://fonts.google.com/noto/specimen/Noto+Sans
|
||||
2. 点击 "Download family"
|
||||
3. 解压后找到 `NotoSans-Regular.ttf`
|
||||
4. 将文件放入 `XP.ReportEngine/Fonts/` 目录
|
||||
|
||||
## 配置说明 | Configuration Notes
|
||||
|
||||
字体文件已在 `.csproj` 中配置为嵌入资源(EmbeddedResource),添加文件后无需额外配置。
|
||||
Font files are configured as EmbeddedResource in .csproj, no additional configuration needed after adding files.
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Fonts\*.otf" />
|
||||
<EmbeddedResource Include="Fonts\*.ttf" />
|
||||
</ItemGroup>
|
||||
```
|
||||
1. **Telerik RadPdfViewer 兼容性** — 系统字体生成的 PDF 在 Telerik RadPdfViewer 中显示正常,不会出现中文乱码
|
||||
2. **无需额外部署** — 不需要随应用程序分发字体文件,减小安装包体积
|
||||
3. **跨阅读器兼容** — 字体以子集嵌入 PDF 中(`PREFER_EMBEDDED`),在任何 PDF 阅读器中都能正确显示
|
||||
|
||||
## 后备机制 | Fallback Mechanism
|
||||
|
||||
如果字体文件缺失,ITextPdfRenderer 会按以下顺序降级:
|
||||
1. 尝试加载嵌入的 CJK 字体和西文字体
|
||||
2. 加载失败时记录警告日志
|
||||
3. 最终使用 iText 内置 Helvetica 字体(不支持中文字符)
|
||||
ITextPdfRenderer 按以下顺序加载字体:
|
||||
|
||||
因此,如果需要生成包含中文内容的报告,**必须**添加 NotoSansCJKsc-Regular.otf 字体文件。
|
||||
1. 微软雅黑(`msyh.ttc`)→ 加载失败时尝试宋体(`simsun.ttc`)
|
||||
2. Arial(`arial.ttf`)→ 加载失败时使用 iText 内置 Helvetica
|
||||
3. 最终后备:Helvetica(不支持中文字符)
|
||||
|
||||
## 系统要求 | System Requirements
|
||||
|
||||
- Windows 10 或更高版本(微软雅黑为系统预装字体)
|
||||
- 如果在精简版 Windows 上运行,需确保系统已安装微软雅黑字体
|
||||
|
||||
## Fonts 目录 | Fonts Directory
|
||||
|
||||
`XP.ReportEngine/Fonts/` 目录当前为空,保留用于未来可能的自定义字体扩展。
|
||||
如需添加自定义字体,可在 `ITextPdfRenderer.InitializeFonts()` 中扩展加载逻辑。
|
||||
|
||||
@@ -47,7 +47,7 @@ XP.ReportEngine/
|
||||
│ ├── ITextPdfRenderer.cs # iText 7 PDF 渲染
|
||||
│ ├── ProcessorDataAdapter.cs # 处理器数据适配
|
||||
│ └── ReportIdGenerator.cs # 报告编号生成
|
||||
├── Fonts/ # 嵌入字体文件(需手动添加)
|
||||
├── Fonts/ # 字体目录(当前使用系统字体,目录保留备用)
|
||||
├── Templates/ # JSON 报告模板
|
||||
│ └── StandardReportTemplate.json
|
||||
├── Resources/ # 多语言资源文件
|
||||
@@ -62,11 +62,9 @@ XP.ReportEngine/
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 添加字体文件
|
||||
### 1. 字体说明
|
||||
|
||||
将以下字体文件放入 `Fonts/` 目录(详见 `Documents/字体文件说明.md`):
|
||||
- `NotoSansCJKsc-Regular.otf` — 中文 CJK 字体
|
||||
- `NotoSans-Regular.ttf` — 西文字体
|
||||
模块使用 Windows 系统自带字体(微软雅黑 + Arial),无需额外添加字体文件。详见 `Documents/FontFilesGuidance.md`。
|
||||
|
||||
### 2. 通过 DI 容器调用
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
|
||||
@@ -3,8 +3,12 @@ using Prism.Modularity;
|
||||
using System.Resources;
|
||||
using XP.Common.Localization;
|
||||
using XP.Common.Localization.Interfaces;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.ReportEngine.Configs;
|
||||
using XP.ReportEngine.Interfaces;
|
||||
using XP.ReportEngine.Services;
|
||||
using XP.ReportEngine.ViewModels;
|
||||
using XP.ReportEngine.Views;
|
||||
|
||||
namespace XP.ReportEngine
|
||||
{
|
||||
@@ -41,6 +45,17 @@ namespace XP.ReportEngine
|
||||
/// </summary>
|
||||
public void RegisterTypes(IContainerRegistry containerRegistry)
|
||||
{
|
||||
// 注册配置加载器(瞬态)| Register config loader (transient)
|
||||
containerRegistry.Register<Configs.ConfigLoader>();
|
||||
|
||||
// 加载并注册配置为单例 | Load and register config as singleton
|
||||
containerRegistry.RegisterSingleton<ReportConfig>(container =>
|
||||
{
|
||||
var logger = container.Resolve<ILoggerService>();
|
||||
var loader = new Configs.ConfigLoader(logger);
|
||||
return loader.LoadReportConfig();
|
||||
});
|
||||
|
||||
// 注册报告生成器(瞬态)| Register report generator (transient)
|
||||
containerRegistry.Register<IReportGenerator, PdfReportGenerator>();
|
||||
|
||||
@@ -65,6 +80,10 @@ namespace XP.ReportEngine
|
||||
// 注册报告编号生成器(单例,维护每日计数器状态)| Register report ID generator (singleton, maintains daily counter state)
|
||||
containerRegistry.RegisterSingleton<ReportIdGenerator>();
|
||||
|
||||
// 注册演示窗口 ViewModel 和 View | Register demo window ViewModel and View
|
||||
containerRegistry.Register<ReportDemoViewModel>();
|
||||
containerRegistry.Register<ReportDemoWindow>();
|
||||
|
||||
System.Console.WriteLine("[ReportEngineModule] 类型注册完成 | Type registration completed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -61,8 +61,44 @@ namespace XP.ReportEngine.Services
|
||||
|
||||
private void BindElement(TemplateElement element, ReportContext context)
|
||||
{
|
||||
if (string.IsNullOrEmpty(element.Content)) return;
|
||||
element.Content = ResolveAllExpressions(element.Content, context);
|
||||
// 文本内容绑定 | Text content binding
|
||||
if (!string.IsNullOrEmpty(element.Content))
|
||||
{
|
||||
element.Content = ResolveAllExpressions(element.Content, context);
|
||||
}
|
||||
|
||||
// 图像元素绑定:通过 DataKey 从 ReportContext.Images 获取 ImageData
|
||||
// Image element binding: get ImageData from ReportContext.Images via DataKey
|
||||
if (string.Equals(element.Type, "image", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrEmpty(element.DataKey)
|
||||
&& context.Images != null
|
||||
&& context.Images.TryGetValue(element.DataKey, out var imageData))
|
||||
{
|
||||
element.ImageData = imageData;
|
||||
}
|
||||
|
||||
// 表格数据绑定:通过 DataKey 从 ReportContext.Properties 获取表格行数据
|
||||
// Table data binding: get table row data from ReportContext.Properties via DataKey
|
||||
if (string.Equals(element.Type, "table", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrEmpty(element.DataKey)
|
||||
&& context.Properties != null
|
||||
&& context.Properties.TryGetValue(element.DataKey, out var tableValue)
|
||||
&& tableValue is List<Dictionary<string, object>> tableRows)
|
||||
{
|
||||
element.TableData = tableRows;
|
||||
}
|
||||
|
||||
// 表格列头绑定 | Table column header binding
|
||||
if (element.Columns != null)
|
||||
{
|
||||
foreach (var column in element.Columns)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(column.Header))
|
||||
{
|
||||
column.Header = ResolveAllExpressions(column.Header, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolveAllExpressions(string input, ReportContext context)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Windows.Media.Imaging;
|
||||
using iText.IO.Font;
|
||||
using iText.Kernel.Colors;
|
||||
@@ -59,15 +58,16 @@ namespace XP.ReportEngine.Services
|
||||
/// </summary>
|
||||
private const string EvenRowBackgroundColor = "#F5F5F5";
|
||||
|
||||
private PdfFont _cjkScFont;
|
||||
private PdfFont _cjkTcFont;
|
||||
private PdfFont _cjkFont;
|
||||
private PdfFont _westernFont;
|
||||
private bool _fontsInitialized;
|
||||
private readonly object _fontLock = new();
|
||||
|
||||
public ITextPdfRenderer(ILoggerService logger, ILocalizationService localizationService)
|
||||
{
|
||||
_logger = logger?.ForModule<ITextPdfRenderer>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
|
||||
InitializeFonts();
|
||||
// 字体延迟加载,不在构造函数中阻塞 | Fonts loaded lazily, not blocking in constructor
|
||||
}
|
||||
|
||||
#region 7.1 基础 PDF 文档创建 | Basic PDF document creation
|
||||
@@ -86,43 +86,42 @@ namespace XP.ReportEngine.Services
|
||||
|
||||
try
|
||||
{
|
||||
using (var writer = new PdfWriter(memoryStream, new WriterProperties().SetFullCompressionMode(true)))
|
||||
var writer = new PdfWriter(memoryStream, new WriterProperties().SetFullCompressionMode(true));
|
||||
// 防止 PdfWriter 关闭时关闭底层流 | Prevent PdfWriter from closing the underlying stream
|
||||
writer.SetCloseStream(false);
|
||||
|
||||
var pdfDocument = new PdfDocument(writer);
|
||||
// 设置 A4 页面尺寸 | Set A4 page size
|
||||
pdfDocument.SetDefaultPageSize(PageSize.A4);
|
||||
|
||||
var document = new Document(pdfDocument);
|
||||
|
||||
// 设置默认边距(使用默认 20mm)| Set default margins (20mm default)
|
||||
float marginTop = 20f * MmToPoints;
|
||||
float marginBottom = 20f * MmToPoints;
|
||||
float marginLeft = 20f * MmToPoints;
|
||||
float marginRight = 20f * MmToPoints;
|
||||
|
||||
document.SetMargins(marginTop, marginRight, marginBottom, marginLeft);
|
||||
|
||||
if (pages != null && pages.Count > 0)
|
||||
{
|
||||
// 防止 PdfWriter 关闭时关闭底层流 | Prevent PdfWriter from closing the underlying stream
|
||||
writer.SetCloseStream(false);
|
||||
|
||||
using (var pdfDocument = new PdfDocument(writer))
|
||||
for (int i = 0; i < pages.Count; i++)
|
||||
{
|
||||
// 设置 A4 页面尺寸 | Set A4 page size
|
||||
pdfDocument.SetDefaultPageSize(PageSize.A4);
|
||||
|
||||
using (var document = new Document(pdfDocument))
|
||||
if (i > 0)
|
||||
{
|
||||
// 设置默认边距(使用默认 20mm)| Set default margins (20mm default)
|
||||
float marginTop = 20f * MmToPoints;
|
||||
float marginBottom = 20f * MmToPoints;
|
||||
float marginLeft = 20f * MmToPoints;
|
||||
float marginRight = 20f * MmToPoints;
|
||||
|
||||
document.SetMargins(marginTop, marginRight, marginBottom, marginLeft);
|
||||
|
||||
if (pages != null && pages.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < pages.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
// 添加新页面 | Add new page
|
||||
document.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
|
||||
}
|
||||
|
||||
RenderPage(document, pages[i]);
|
||||
}
|
||||
}
|
||||
// 添加新页面 | Add new page
|
||||
document.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
|
||||
}
|
||||
|
||||
RenderPage(document, pages[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 仅关闭 Document(它会级联关闭 PdfDocument 和 PdfWriter)
|
||||
// Only close Document (it cascades to PdfDocument and PdfWriter)
|
||||
document.Close();
|
||||
|
||||
// 重置流位置以便后续读取 | Reset stream position for subsequent reading
|
||||
memoryStream.Position = 0;
|
||||
_logger.Info("PDF 渲染完成 | PDF rendering completed");
|
||||
@@ -252,97 +251,94 @@ namespace XP.ReportEngine.Services
|
||||
#region 7.3 字体管理 | Font management
|
||||
|
||||
/// <summary>
|
||||
/// 初始化字体(加载嵌入资源字体)| Initialize fonts (load embedded resource fonts)
|
||||
/// 确保字体已初始化(线程安全的延迟加载)| Ensure fonts are initialized (thread-safe lazy loading)
|
||||
/// </summary>
|
||||
private void InitializeFonts()
|
||||
private void EnsureFontsInitialized()
|
||||
{
|
||||
// 加载简体中文字体 | Load Simplified Chinese font
|
||||
try
|
||||
if (_fontsInitialized) return;
|
||||
lock (_fontLock)
|
||||
{
|
||||
_cjkScFont = LoadEmbeddedFont("XP.ReportEngine.Fonts.NotoSerifSC-Regular.otf");
|
||||
_logger.Info("简体中文字体加载成功 | Simplified Chinese font loaded successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn("简体中文嵌入字体加载失败,将使用后备字体 | SC embedded font load failed, will use fallback: {Message}", ex.Message);
|
||||
_cjkScFont = null;
|
||||
}
|
||||
|
||||
// 加载繁体中文字体 | Load Traditional Chinese font
|
||||
try
|
||||
{
|
||||
_cjkTcFont = LoadEmbeddedFont("XP.ReportEngine.Fonts.NotoSerifCJKtc-Regular.otf");
|
||||
_logger.Info("繁体中文字体加载成功 | Traditional Chinese font loaded successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn("繁体中文嵌入字体加载失败,将使用后备字体 | TC embedded font load failed, will use fallback: {Message}", ex.Message);
|
||||
_cjkTcFont = null;
|
||||
}
|
||||
|
||||
// 加载西文字体 | Load Western font
|
||||
try
|
||||
{
|
||||
_westernFont = LoadEmbeddedFont("XP.ReportEngine.Fonts.NotoSans-Regular.ttf");
|
||||
_logger.Info("西文字体加载成功 | Western font loaded successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn("西文嵌入字体加载失败,将使用后备字体 | Western embedded font load failed, will use fallback: {Message}", ex.Message);
|
||||
_westernFont = null;
|
||||
}
|
||||
|
||||
// 如果嵌入字体都不可用,使用 iText 内置 Helvetica | If embedded fonts unavailable, use built-in Helvetica
|
||||
if (_cjkScFont == null && _cjkTcFont == null && _westernFont == null)
|
||||
{
|
||||
_logger.Warn("所有嵌入字体不可用,使用 Helvetica 后备字体 | All embedded fonts unavailable, using Helvetica fallback");
|
||||
if (_fontsInitialized) return;
|
||||
InitializeFonts();
|
||||
_fontsInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从嵌入资源加载字体 | Load font from embedded resource
|
||||
/// 初始化字体(从系统字体目录加载)| Initialize fonts (load from system fonts directory)
|
||||
/// 使用 Windows 系统自带字体,确保 Telerik RadPdfViewer 兼容性
|
||||
/// Uses Windows built-in fonts to ensure Telerik RadPdfViewer compatibility
|
||||
/// </summary>
|
||||
/// <param name="resourceName">嵌入资源名称 | Embedded resource name</param>
|
||||
/// <returns>PDF 字体对象 | PDF font object</returns>
|
||||
private PdfFont LoadEmbeddedFont(string resourceName)
|
||||
private void InitializeFonts()
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
using (var stream = assembly.GetManifestResourceStream(resourceName))
|
||||
var fontsDir = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts");
|
||||
|
||||
// 加载微软雅黑(支持简体中文、繁体中文)| Load Microsoft YaHei (supports Simplified & Traditional Chinese)
|
||||
try
|
||||
{
|
||||
if (stream == null)
|
||||
var msyhPath = System.IO.Path.Combine(fontsDir, "msyh.ttc");
|
||||
_cjkFont = PdfFontFactory.CreateFont(msyhPath + ",0", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
|
||||
_logger.Info("中文字体加载成功(微软雅黑)| Chinese font loaded successfully (Microsoft YaHei)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn("微软雅黑加载失败,尝试后备字体 | Microsoft YaHei load failed, trying fallback: {Message}", ex.Message);
|
||||
try
|
||||
{
|
||||
throw new FileNotFoundException($"嵌入资源未找到 | Embedded resource not found: {resourceName}");
|
||||
// 后备:宋体 | Fallback: SimSun
|
||||
var simsunPath = System.IO.Path.Combine(fontsDir, "simsun.ttc");
|
||||
_cjkFont = PdfFontFactory.CreateFont(simsunPath + ",0", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
|
||||
_logger.Info("中文后备字体加载成功(宋体)| Chinese fallback font loaded successfully (SimSun)");
|
||||
}
|
||||
catch (Exception ex2)
|
||||
{
|
||||
_logger.Warn("宋体加载失败 | SimSun load failed: {Message}", ex2.Message);
|
||||
_cjkFont = null;
|
||||
}
|
||||
}
|
||||
|
||||
var fontBytes = new byte[stream.Length];
|
||||
stream.Read(fontBytes, 0, fontBytes.Length);
|
||||
// 加载 Arial(西文字体)| Load Arial (Western font)
|
||||
try
|
||||
{
|
||||
var arialPath = System.IO.Path.Combine(fontsDir, "arial.ttf");
|
||||
_westernFont = PdfFontFactory.CreateFont(arialPath, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
|
||||
_logger.Info("西文字体加载成功(Arial)| Western font loaded successfully (Arial)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn("Arial 加载失败,使用 Helvetica 后备 | Arial load failed, using Helvetica fallback: {Message}", ex.Message);
|
||||
_westernFont = null;
|
||||
}
|
||||
|
||||
var fontProgram = FontProgramFactory.CreateFont(fontBytes);
|
||||
return PdfFontFactory.CreateFont(fontProgram, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
|
||||
// 如果系统字体都不可用,使用 iText 内置 Helvetica | If system fonts unavailable, use built-in Helvetica
|
||||
if (_cjkFont == null && _westernFont == null)
|
||||
{
|
||||
_logger.Warn("所有系统字体不可用,使用 Helvetica 后备字体 | All system fonts unavailable, using Helvetica fallback");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前语言获取合适的字体 | Get appropriate font based on current language
|
||||
/// zh-CN → 简体中文字体;zh-TW → 繁体中文字体;en-US → 西文字体
|
||||
/// zh-CN / zh-TW → 微软雅黑(支持简繁体);en-US → Arial
|
||||
/// </summary>
|
||||
/// <returns>PDF 字体 | PDF font</returns>
|
||||
private PdfFont GetFontForCurrentLanguage()
|
||||
{
|
||||
// 延迟初始化字体 | Lazy initialize fonts
|
||||
EnsureFontsInitialized();
|
||||
|
||||
var language = _localizationService.CurrentLanguage;
|
||||
|
||||
PdfFont selectedFont;
|
||||
switch (language)
|
||||
{
|
||||
case SupportedLanguage.ZhCN:
|
||||
selectedFont = _cjkScFont;
|
||||
break;
|
||||
case SupportedLanguage.ZhTW:
|
||||
selectedFont = _cjkTcFont ?? _cjkScFont; // 繁体优先,简体后备 | TC preferred, SC fallback
|
||||
selectedFont = _cjkFont;
|
||||
break;
|
||||
case SupportedLanguage.EnUS:
|
||||
default:
|
||||
selectedFont = _westernFont;
|
||||
selectedFont = _westernFont ?? _cjkFont; // 西文优先,中文后备(微软雅黑也支持西文)| Western preferred, CJK fallback (YaHei supports Western too)
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -352,9 +348,8 @@ namespace XP.ReportEngine.Services
|
||||
return selectedFont;
|
||||
}
|
||||
|
||||
// 尝试使用其他嵌入字体 | Try other embedded fonts
|
||||
if (_cjkScFont != null) return _cjkScFont;
|
||||
if (_cjkTcFont != null) return _cjkTcFont;
|
||||
// 尝试使用其他字体 | Try other fonts
|
||||
if (_cjkFont != null) return _cjkFont;
|
||||
if (_westernFont != null) return _westernFont;
|
||||
|
||||
// 最终后备:使用 iText 内置 Helvetica | Final fallback: use built-in Helvetica
|
||||
|
||||
@@ -59,11 +59,43 @@ namespace XP.ReportEngine.Services
|
||||
var group = AdaptProcessorOutput(output, i);
|
||||
context.ResultGroups.Add(group);
|
||||
|
||||
// 关联标注图像 | Associate annotated image
|
||||
// 将结果数据扁平化到 Properties(供模板 ${key} 表达式绑定)
|
||||
// Flatten result data to Properties (for template ${key} expression binding)
|
||||
if (group.Data != null)
|
||||
{
|
||||
foreach (var kvp in group.Data)
|
||||
{
|
||||
context.Properties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 将 Classification 也放入 Properties | Also put Classification into Properties
|
||||
if (!string.IsNullOrEmpty(group.Classification))
|
||||
{
|
||||
context.Properties["classification"] = group.Classification;
|
||||
}
|
||||
|
||||
// 将表格数据以模板期望的 dataKey 存入 Properties
|
||||
// Store table data with template-expected dataKey into Properties
|
||||
if (group.TableRows != null && group.TableRows.Count > 0)
|
||||
{
|
||||
var tableKey = GetTableDataKey(output.ProcessorType);
|
||||
if (!string.IsNullOrEmpty(tableKey))
|
||||
{
|
||||
context.Properties[tableKey] = group.TableRows;
|
||||
}
|
||||
}
|
||||
|
||||
// 关联标注图像(使用模板期望的 dataKey)
|
||||
// Associate annotated image (using template-expected dataKey)
|
||||
if (output.AnnotatedImage != null)
|
||||
{
|
||||
var imageKey = $"{output.ProcessorType}_{i}_annotated";
|
||||
var imageKey = GetImageDataKey(output.ProcessorType);
|
||||
context.Images[imageKey] = output.AnnotatedImage;
|
||||
|
||||
// 同时保留原始键名以兼容其他调用方 | Also keep original key for other callers
|
||||
var originalKey = $"{output.ProcessorType}_{i}_annotated";
|
||||
context.Images[originalKey] = output.AnnotatedImage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +499,34 @@ namespace XP.ReportEngine.Services
|
||||
return char.ToLowerInvariant(input[0]) + input.Substring(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据处理器类型获取模板中对应的图像 dataKey | Get template image dataKey by processor type
|
||||
/// </summary>
|
||||
private static string GetImageDataKey(string processorType)
|
||||
{
|
||||
return processorType switch
|
||||
{
|
||||
LineMeasurementProcessor => "lineMeasurementImage",
|
||||
BgaVoidRateProcessor => "bgaInspectionImage",
|
||||
VoidMeasurementProcessor => "voidInspectionImage",
|
||||
FillRateProcessor => "viaFillImage",
|
||||
_ => $"{processorType}_image"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据处理器类型获取模板中对应的表格 dataKey | Get template table dataKey by processor type
|
||||
/// </summary>
|
||||
private static string GetTableDataKey(string processorType)
|
||||
{
|
||||
return processorType switch
|
||||
{
|
||||
BgaVoidRateProcessor => "bgaBallsTable",
|
||||
VoidMeasurementProcessor => "voidsTable",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
{
|
||||
"type": "homepage",
|
||||
"elements": [
|
||||
{ "type": "image", "dataKey": "companyLogo", "position": [0, 0], "size": [30, 15], "positioning": "absolute" },
|
||||
{ "type": "text", "content": "${CompanyName}", "style": "body", "position": [32, 3], "positioning": "absolute" },
|
||||
{ "type": "text", "content": "${loc:Report_Title}", "style": "title", "position": [60, 40], "positioning": "absolute" },
|
||||
{ "type": "divider", "position": [0, 60], "size": [170, 1], "positioning": "absolute" },
|
||||
{ "type": "text", "content": "${loc:Report_Id}:${metadata.reportId}", "style": "body", "position": [0, 75], "positioning": "absolute" },
|
||||
|
||||
@@ -0,0 +1,664 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.GeneralForm.Views;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
using XP.ReportEngine.Configs;
|
||||
using XP.ReportEngine.Interfaces;
|
||||
using XP.ReportEngine.Models;
|
||||
using XP.ReportEngine.Services;
|
||||
|
||||
namespace XP.ReportEngine.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告生成演示窗口 ViewModel | Report generation demo window ViewModel
|
||||
/// 演示如何使用 XP.ReportEngine 生成 PDF 报告
|
||||
/// Demonstrates how to use XP.ReportEngine to generate PDF reports
|
||||
/// </summary>
|
||||
public class ReportDemoViewModel : BindableBase
|
||||
{
|
||||
private readonly IReportGenerator _reportGenerator;
|
||||
private readonly IReportDataAdapter _dataAdapter;
|
||||
private readonly IPdfViewerService _pdfViewerService;
|
||||
private readonly IPdfPrintService _pdfPrintService;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly ReportIdGenerator _reportIdGenerator;
|
||||
private readonly ReportConfig _reportConfig;
|
||||
|
||||
private string _productName = "PCB-TEST-001";
|
||||
private string _operatorName = "戚明轩 mingxuan.qi@hexagon.com";
|
||||
private string _description = "BGA 焊球气泡率检测";
|
||||
private string _cncProgram = "Prog001";
|
||||
private string _productCode = "PCBA-X100";
|
||||
private string _workpieceSN = "SN20250001";
|
||||
private string _deviceId = "XP-CT-001";
|
||||
private string _machineId = "MC01";
|
||||
private string _statusMessage = "就绪";
|
||||
private string _lastOutputPath;
|
||||
private bool _isGenerating;
|
||||
|
||||
#region 属性 | Properties
|
||||
|
||||
/// <summary>
|
||||
/// 产品名称 | Product name
|
||||
/// </summary>
|
||||
public string ProductName
|
||||
{
|
||||
get => _productName;
|
||||
set => SetProperty(ref _productName, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 操作员 | Operator
|
||||
/// </summary>
|
||||
public string OperatorName
|
||||
{
|
||||
get => _operatorName;
|
||||
set => SetProperty(ref _operatorName, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 描述 | Description
|
||||
/// </summary>
|
||||
public string Description
|
||||
{
|
||||
get => _description;
|
||||
set => SetProperty(ref _description, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CNC 程序名称 | CNC program name
|
||||
/// </summary>
|
||||
public string CncProgram
|
||||
{
|
||||
get => _cncProgram;
|
||||
set => SetProperty(ref _cncProgram, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 产品类型码 | Product type code
|
||||
/// </summary>
|
||||
public string ProductCode
|
||||
{
|
||||
get => _productCode;
|
||||
set => SetProperty(ref _productCode, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工件 SN 码 | Workpiece serial number
|
||||
/// </summary>
|
||||
public string WorkpieceSN
|
||||
{
|
||||
get => _workpieceSN;
|
||||
set => SetProperty(ref _workpieceSN, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测设备编号 | Inspection device ID
|
||||
/// </summary>
|
||||
public string DeviceId
|
||||
{
|
||||
get => _deviceId;
|
||||
set => SetProperty(ref _deviceId, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生产机台号 | Production machine ID
|
||||
/// </summary>
|
||||
public string MachineId
|
||||
{
|
||||
get => _machineId;
|
||||
set => SetProperty(ref _machineId, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态信息 | Status message
|
||||
/// </summary>
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否正在生成 | Whether generating
|
||||
/// </summary>
|
||||
public bool IsGenerating
|
||||
{
|
||||
get => _isGenerating;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isGenerating, value))
|
||||
{
|
||||
GenerateReportCommand.RaiseCanExecuteChanged();
|
||||
OpenViewerCommand.RaiseCanExecuteChanged();
|
||||
PrintReportCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 命令 | Commands
|
||||
|
||||
/// <summary>
|
||||
/// 生成报告命令 | Generate report command
|
||||
/// </summary>
|
||||
public DelegateCommand GenerateReportCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 打开 PDF 阅读器命令 | Open PDF viewer command
|
||||
/// </summary>
|
||||
public DelegateCommand OpenViewerCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 打印报告命令 | Print report command
|
||||
/// </summary>
|
||||
public DelegateCommand PrintReportCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
public ReportDemoViewModel(
|
||||
IReportGenerator reportGenerator,
|
||||
IReportDataAdapter dataAdapter,
|
||||
IPdfViewerService pdfViewerService,
|
||||
IPdfPrintService pdfPrintService,
|
||||
ILoggerService logger,
|
||||
ReportIdGenerator reportIdGenerator,
|
||||
ReportConfig reportConfig)
|
||||
{
|
||||
_reportGenerator = reportGenerator ?? throw new ArgumentNullException(nameof(reportGenerator));
|
||||
_dataAdapter = dataAdapter ?? throw new ArgumentNullException(nameof(dataAdapter));
|
||||
_pdfViewerService = pdfViewerService ?? throw new ArgumentNullException(nameof(pdfViewerService));
|
||||
_pdfPrintService = pdfPrintService ?? throw new ArgumentNullException(nameof(pdfPrintService));
|
||||
_logger = logger?.ForModule<ReportDemoViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_reportIdGenerator = reportIdGenerator ?? throw new ArgumentNullException(nameof(reportIdGenerator));
|
||||
_reportConfig = reportConfig ?? throw new ArgumentNullException(nameof(reportConfig));
|
||||
|
||||
GenerateReportCommand = new DelegateCommand(async () => await GenerateReportAsync(), () => !IsGenerating);
|
||||
OpenViewerCommand = new DelegateCommand(OpenViewer, () => !IsGenerating && !string.IsNullOrEmpty(_lastOutputPath));
|
||||
PrintReportCommand = new DelegateCommand(PrintReport, () => !IsGenerating && !string.IsNullOrEmpty(_lastOutputPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成报告(带进度条)| Generate report (with progress window)
|
||||
/// </summary>
|
||||
private async Task GenerateReportAsync()
|
||||
{
|
||||
IsGenerating = true;
|
||||
StatusMessage = "正在生成报告...";
|
||||
|
||||
try
|
||||
{
|
||||
// 构建文件名占位符参数 | Build file name placeholder parameters
|
||||
var fileNameParams = new Dictionary<string, string>
|
||||
{
|
||||
["ReportId"] = _reportIdGenerator.GenerateNext(),
|
||||
["ProductName"] = ProductName,
|
||||
["CncProgram"] = CncProgram,
|
||||
["ProductCode"] = ProductCode,
|
||||
["WorkpieceSN"] = WorkpieceSN,
|
||||
["DeviceId"] = DeviceId,
|
||||
["MachineId"] = MachineId,
|
||||
["Result"] = "Pass"
|
||||
};
|
||||
|
||||
// 确定输出路径:提示用户是否使用默认位置 | Determine output path: ask user whether to use default location
|
||||
var defaultOutputPath = _reportConfig.ResolveOutputFilePath(fileNameParams);
|
||||
var defaultFileName = System.IO.Path.GetFileName(defaultOutputPath);
|
||||
|
||||
var result = MessageBox.Show(
|
||||
$"是否将报告输出到默认位置?\n\n{defaultOutputPath}",
|
||||
"输出位置确认",
|
||||
MessageBoxButton.YesNoCancel,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Cancel)
|
||||
{
|
||||
StatusMessage = "已取消生成";
|
||||
IsGenerating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
string outputPath;
|
||||
if (result == MessageBoxResult.No)
|
||||
{
|
||||
// 用户选择自定义位置 | User chooses custom location
|
||||
var saveDialog = new Microsoft.Win32.SaveFileDialog
|
||||
{
|
||||
Title = "选择报告保存位置",
|
||||
Filter = "PDF 文件 (*.pdf)|*.pdf",
|
||||
FileName = defaultFileName,
|
||||
DefaultExt = ".pdf",
|
||||
InitialDirectory = System.IO.Path.GetDirectoryName(defaultOutputPath)
|
||||
};
|
||||
|
||||
if (saveDialog.ShowDialog() != true)
|
||||
{
|
||||
StatusMessage = "已取消生成";
|
||||
IsGenerating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
outputPath = saveDialog.FileName;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 使用默认路径 | Use default path
|
||||
outputPath = defaultOutputPath;
|
||||
}
|
||||
|
||||
// 创建进度条窗口 | Create progress window
|
||||
var progressWindow = new ProgressWindow(
|
||||
title: "报告生成中",
|
||||
message: "正在准备数据...",
|
||||
isCancelable: false,
|
||||
logger: _logger);
|
||||
|
||||
progressWindow.Owner = Application.Current.MainWindow;
|
||||
progressWindow.Show();
|
||||
|
||||
try
|
||||
{
|
||||
// 步骤 1:准备模拟数据 | Step 1: Prepare mock data
|
||||
progressWindow.UpdateProgress("正在准备检测数据...", 10);
|
||||
await Task.Delay(300); // 模拟耗时 | Simulate delay
|
||||
|
||||
var processorOutputs = CreateMockProcessorOutputs();
|
||||
|
||||
// 步骤 2:数据适配 | Step 2: Data adaptation
|
||||
progressWindow.UpdateProgress("正在适配数据...", 30);
|
||||
await Task.Delay(200);
|
||||
|
||||
var metadata = new ReportMetadata
|
||||
{
|
||||
ReportId = fileNameParams["ReportId"],
|
||||
InspectionDate = DateTime.Now,
|
||||
SampleName = ProductName,
|
||||
OperatorName = OperatorName,
|
||||
Description = Description
|
||||
};
|
||||
|
||||
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 homepage summary table data
|
||||
context.Properties["summaryTable"] = CreateSummaryTableData(context);
|
||||
|
||||
// 步骤 3:确定模板 | Step 3: Determine template
|
||||
progressWindow.UpdateProgress("正在加载模板...", 50);
|
||||
await Task.Delay(200);
|
||||
|
||||
var templatePath = _reportConfig.GetResolvedTemplatePath();
|
||||
|
||||
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
|
||||
progressWindow.UpdateProgress("正在完成...", 95);
|
||||
await Task.Delay(200);
|
||||
|
||||
if (genResult.IsSuccess)
|
||||
{
|
||||
_lastOutputPath = outputPath;
|
||||
StatusMessage = $"报告生成成功:{outputPath}";
|
||||
_logger.Info("报告生成成功:{Path} | Report generated successfully: {Path}", outputPath);
|
||||
|
||||
progressWindow.UpdateProgress("报告生成完成!", 100);
|
||||
await Task.Delay(500);
|
||||
|
||||
// 根据配置自动打开 PDF 阅读器 | Auto-open PDF viewer based on config
|
||||
if (_reportConfig.AutoOpenAfterGenerate)
|
||||
{
|
||||
try
|
||||
{
|
||||
_pdfViewerService.OpenViewer(outputPath);
|
||||
}
|
||||
catch (Exception viewerEx)
|
||||
{
|
||||
_logger.Warn("自动打开 PDF 失败 | Auto-open PDF failed: {Message}", viewerEx.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"报告生成失败:{genResult.ErrorMessage}";
|
||||
_logger.Error(null, "报告生成失败:{Message} | Report generation failed: {Message}", genResult.ErrorMessage);
|
||||
|
||||
MessageBox.Show(
|
||||
$"报告生成失败:\n{genResult.ErrorMessage}",
|
||||
"错误",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
progressWindow.Close();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"报告生成异常:{ex.Message}";
|
||||
_logger.Error(ex, "报告生成异常 | Report generation exception: {Message}", ex.Message);
|
||||
|
||||
MessageBox.Show(
|
||||
$"报告生成过程中发生异常:\n{ex.Message}",
|
||||
"错误",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsGenerating = false;
|
||||
OpenViewerCommand.RaiseCanExecuteChanged();
|
||||
PrintReportCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开 PDF 阅读器 | Open PDF viewer
|
||||
/// </summary>
|
||||
private void OpenViewer()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_lastOutputPath) || !File.Exists(_lastOutputPath))
|
||||
{
|
||||
MessageBox.Show("PDF 文件不存在,请先生成报告。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 使用 XP.Common PDF 阅读器打开 | Open with XP.Common PDF viewer
|
||||
_pdfViewerService.OpenViewer(_lastOutputPath);
|
||||
_logger.Info("使用内置 PDF 阅读器打开:{Path} | Opened with built-in PDF viewer: {Path}", _lastOutputPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "打开 PDF 失败 | Failed to open PDF: {Message}", ex.Message);
|
||||
MessageBox.Show($"打开 PDF 失败:\n{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打印报告 | Print report
|
||||
/// </summary>
|
||||
private void PrintReport()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_lastOutputPath) || !File.Exists(_lastOutputPath))
|
||||
{
|
||||
MessageBox.Show("PDF 文件不存在,请先生成报告。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var confirmed = _pdfPrintService.PrintWithDialog(_lastOutputPath);
|
||||
if (confirmed)
|
||||
{
|
||||
_logger.Info("报告已发送到打印机:{Path} | Report sent to printer: {Path}", _lastOutputPath);
|
||||
StatusMessage = "报告已发送到打印机";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "打印报告失败 | Failed to print report: {Message}", ex.Message);
|
||||
MessageBox.Show($"打印失败:\n{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
#region 模拟数据 | Mock Data
|
||||
|
||||
/// <summary>
|
||||
/// 模拟图像目录路径 | Mock image directory path
|
||||
/// </summary>
|
||||
private const string MockImageDirectory = @"D:\XplorePlane\DetectorImages";
|
||||
|
||||
/// <summary>
|
||||
/// 创建模拟处理器输出数据(演示用,覆盖所有检测类型)
|
||||
/// Create mock processor outputs (for demo, covers all inspection types)
|
||||
/// </summary>
|
||||
private List<ProcessorOutput> CreateMockProcessorOutputs()
|
||||
{
|
||||
return new List<ProcessorOutput>
|
||||
{
|
||||
CreateLineMeasurementOutput(),
|
||||
CreateBgaVoidRateOutput(),
|
||||
CreateVoidMeasurementOutput(),
|
||||
CreateFillRateOutput()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建线测量处理器模拟数据 | Create line measurement processor mock data
|
||||
/// </summary>
|
||||
private ProcessorOutput CreateLineMeasurementOutput()
|
||||
{
|
||||
return new ProcessorOutput
|
||||
{
|
||||
ProcessorType = "LineMeasurementProcessor",
|
||||
OutputData = new Dictionary<string, object>
|
||||
{
|
||||
["MeasurementType"] = "TwoPointDistance",
|
||||
["Point1"] = "X=125.32, Y=80.15",
|
||||
["Point2"] = "X=340.78, Y=80.15",
|
||||
["PixelDistance"] = 215.46,
|
||||
["ActualDistance"] = 3.256,
|
||||
["Unit"] = "mm",
|
||||
["Angle"] = 0.0
|
||||
},
|
||||
AnnotatedImage = LoadMockImage("BGA.png")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 BGA 气泡率处理器模拟数据 | Create BGA void rate processor mock data
|
||||
/// </summary>
|
||||
private ProcessorOutput CreateBgaVoidRateOutput()
|
||||
{
|
||||
return new ProcessorOutput
|
||||
{
|
||||
ProcessorType = "BgaVoidRateProcessor",
|
||||
OutputData = new Dictionary<string, object>
|
||||
{
|
||||
["BgaCount"] = 64,
|
||||
["VoidRate"] = 0.028,
|
||||
["FillRate"] = 0.972,
|
||||
["TotalBgaArea"] = 12500.5,
|
||||
["TotalVoidArea"] = 350.2,
|
||||
["Classification"] = "Pass",
|
||||
["VoidLimit"] = 0.05,
|
||||
["BgaBalls"] = new List<Dictionary<string, object>>
|
||||
{
|
||||
new() { ["Index"] = 1, ["VoidRate"] = 0.012, ["Area"] = 195.3, ["Classification"] = "Pass" },
|
||||
new() { ["Index"] = 2, ["VoidRate"] = 0.035, ["Area"] = 198.1, ["Classification"] = "Pass" },
|
||||
new() { ["Index"] = 3, ["VoidRate"] = 0.008, ["Area"] = 192.7, ["Classification"] = "Pass" },
|
||||
new() { ["Index"] = 4, ["VoidRate"] = 0.042, ["Area"] = 201.0, ["Classification"] = "Pass" },
|
||||
new() { ["Index"] = 5, ["VoidRate"] = 0.015, ["Area"] = 196.5, ["Classification"] = "Pass" },
|
||||
new() { ["Index"] = 6, ["VoidRate"] = 0.028, ["Area"] = 199.8, ["Classification"] = "Pass" },
|
||||
new() { ["Index"] = 7, ["VoidRate"] = 0.005, ["Area"] = 194.2, ["Classification"] = "Pass" },
|
||||
new() { ["Index"] = 8, ["VoidRate"] = 0.048, ["Area"] = 200.5, ["Classification"] = "Pass" },
|
||||
new() { ["Index"] = 9, ["VoidRate"] = 0.019, ["Area"] = 197.0, ["Classification"] = "Pass" },
|
||||
new() { ["Index"] = 10, ["VoidRate"] = 0.031, ["Area"] = 196.1, ["Classification"] = "Pass" }
|
||||
}
|
||||
},
|
||||
AnnotatedImage = LoadMockImage("BGA.png")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建空隙测量处理器模拟数据 | Create void measurement processor mock data
|
||||
/// </summary>
|
||||
private ProcessorOutput CreateVoidMeasurementOutput()
|
||||
{
|
||||
return new ProcessorOutput
|
||||
{
|
||||
ProcessorType = "VoidMeasurementProcessor",
|
||||
OutputData = new Dictionary<string, object>
|
||||
{
|
||||
["RoiArea"] = 5000.0,
|
||||
["TotalVoidArea"] = 125.8,
|
||||
["VoidRate"] = 0.025,
|
||||
["VoidLimit"] = 0.05,
|
||||
["VoidCount"] = 5,
|
||||
["MaxVoidArea"] = 65.2,
|
||||
["Classification"] = "Pass",
|
||||
["Voids"] = new List<Dictionary<string, object>>
|
||||
{
|
||||
new() { ["Index"] = 1, ["Area"] = 65.2, ["AreaPercent"] = 1.30, ["CenterX"] = 120.5, ["CenterY"] = 85.3 },
|
||||
new() { ["Index"] = 2, ["Area"] = 38.4, ["AreaPercent"] = 0.77, ["CenterX"] = 200.1, ["CenterY"] = 150.8 },
|
||||
new() { ["Index"] = 3, ["Area"] = 22.2, ["AreaPercent"] = 0.44, ["CenterX"] = 80.0, ["CenterY"] = 220.5 },
|
||||
new() { ["Index"] = 4, ["Area"] = 15.6, ["AreaPercent"] = 0.31, ["CenterX"] = 310.2, ["CenterY"] = 95.7 },
|
||||
new() { ["Index"] = 5, ["Area"] = 8.9, ["AreaPercent"] = 0.18, ["CenterX"] = 155.8, ["CenterY"] = 280.1 }
|
||||
}
|
||||
},
|
||||
AnnotatedImage = LoadMockImage("Void.png")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建通孔填锡率处理器模拟数据 | Create via fill rate processor mock data
|
||||
/// </summary>
|
||||
private ProcessorOutput CreateFillRateOutput()
|
||||
{
|
||||
return new ProcessorOutput
|
||||
{
|
||||
ProcessorType = "FillRateProcessor",
|
||||
OutputData = new Dictionary<string, object>
|
||||
{
|
||||
["FillRate"] = 0.85,
|
||||
["VoidRate"] = 0.15,
|
||||
["FullDistance"] = 1.60,
|
||||
["FillDistance"] = 1.36,
|
||||
["THTLimit"] = 0.75,
|
||||
["Classification"] = "Pass",
|
||||
["E1"] = new Dictionary<string, object>
|
||||
{
|
||||
["CenterX"] = 256.0, ["CenterY"] = 256.0,
|
||||
["SemiAxisA"] = 120.5, ["SemiAxisB"] = 118.2, ["Angle"] = 2.3
|
||||
},
|
||||
["E2"] = new Dictionary<string, object>
|
||||
{
|
||||
["CenterX"] = 256.0, ["CenterY"] = 256.0,
|
||||
["SemiAxisA"] = 95.8, ["SemiAxisB"] = 93.1, ["Angle"] = 2.3
|
||||
},
|
||||
["E3"] = new Dictionary<string, object>
|
||||
{
|
||||
["CenterX"] = 256.0, ["CenterY"] = 256.0,
|
||||
["SemiAxisA"] = 70.2, ["SemiAxisB"] = 68.5, ["Angle"] = 1.8
|
||||
},
|
||||
["E4"] = new Dictionary<string, object>
|
||||
{
|
||||
["CenterX"] = 256.0, ["CenterY"] = 256.0,
|
||||
["SemiAxisA"] = 45.0, ["SemiAxisB"] = 43.7, ["Angle"] = 1.5
|
||||
}
|
||||
},
|
||||
AnnotatedImage = LoadMockImage("Void.png")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载模拟图像文件 | Load mock image file
|
||||
/// 从指定目录加载图像,文件不存在时返回 null
|
||||
/// Loads image from specified directory, returns null if file not found
|
||||
/// </summary>
|
||||
/// <param name="fileName">图像文件名 | Image file name</param>
|
||||
/// <returns>图像数据对象或 null | ImageData object or null</returns>
|
||||
private ImageData LoadMockImage(string fileName)
|
||||
{
|
||||
var filePath = System.IO.Path.Combine(MockImageDirectory, fileName);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
_logger.Warn("模拟图像文件不存在:{Path} | Mock image file not found: {Path}", filePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImageData
|
||||
{
|
||||
SourceType = ImageSourceType.FilePath,
|
||||
FilePath = filePath
|
||||
};
|
||||
}
|
||||
|
||||
/// <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" ? "✓ 合格" : classification == "Fail" ? "✗ 不合格" : "—";
|
||||
|
||||
rows.Add(new Dictionary<string, object>
|
||||
{
|
||||
["inspectionType"] = inspectionType,
|
||||
["classification"] = classification,
|
||||
["status"] = status
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.ReportEngine.ViewModels
|
||||
{
|
||||
public class ViewAViewModel : BindableBase
|
||||
{
|
||||
private string _message;
|
||||
public string Message
|
||||
{
|
||||
get { return _message; }
|
||||
set { SetProperty(ref _message, value); }
|
||||
}
|
||||
|
||||
public ViewAViewModel()
|
||||
{
|
||||
Message = "View A from your Prism Module";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<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"
|
||||
Title="报告生成演示 | Report Generation Demo"
|
||||
Width="560" Height="580"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize">
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<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"/>
|
||||
|
||||
<!-- 输入表单 | Input form -->
|
||||
<Grid Grid.Row="1" Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="20"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="28"/>
|
||||
<RowDefinition Height="28"/>
|
||||
</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"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="产品类型码:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ProductCode, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
|
||||
<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"/>
|
||||
|
||||
<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"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="操作员:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding OperatorName, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
|
||||
<!-- 第二列 | 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"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="3" Text="生产机台号:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="4" Text="{Binding MachineId, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="3" Text="描述:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Row="2" Grid.Column="4" Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}" VerticalContentAlignment="Center"/>
|
||||
</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>
|
||||
|
||||
<!-- 说明区域 | 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"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Windows;
|
||||
using XP.ReportEngine.ViewModels;
|
||||
|
||||
namespace XP.ReportEngine.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 报告生成演示窗口 | Report generation demo window
|
||||
/// </summary>
|
||||
public partial class ReportDemoWindow : Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数(通过 DI 注入 ViewModel)| Constructor (ViewModel injected via DI)
|
||||
/// </summary>
|
||||
/// <param name="viewModel">报告演示 ViewModel | Report demo ViewModel</param>
|
||||
public ReportDemoWindow(ReportDemoViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<UserControl x:Class="XP.ReportEngine.Views.ViewA"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:XP.ReportEngine.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="300" d:DesignWidth="300"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True" >
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding Message}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,28 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace XP.ReportEngine.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for ViewA.xaml
|
||||
/// </summary>
|
||||
public partial class ViewA : UserControl
|
||||
{
|
||||
public ViewA()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,18 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Documents\" />
|
||||
</ItemGroup>
|
||||
<!-- 字体文件嵌入资源配置 | Font files embedded resource configuration -->
|
||||
<!-- 注意:需要手动将实际字体文件添加到 Fonts/ 目录 | Note: Actual font files need to be added manually to Fonts/ directory -->
|
||||
<!-- 字体目录保留备用,当前使用系统字体 | Fonts directory reserved, currently using system fonts -->
|
||||
<!-- 如需添加自定义字体,取消以下注释 | Uncomment below to add custom fonts -->
|
||||
<!--
|
||||
<ItemGroup>
|
||||
<None Remove="Fonts\*.otf" />
|
||||
<None Remove="Fonts\*.ttf" />
|
||||
<Resource Remove="Fonts\*.ttf" />
|
||||
<Resource Remove="Fonts\*.otf" />
|
||||
<EmbeddedResource Include="Fonts\*.otf" />
|
||||
<EmbeddedResource Include="Fonts\*.ttf" />
|
||||
</ItemGroup>
|
||||
-->
|
||||
<!-- 模板文件复制到输出目录 | Copy template files to output directory -->
|
||||
<ItemGroup>
|
||||
<Content Include="Templates\*.json">
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.37203.1 d17.14
|
||||
VisualStudioVersion = 17.14.37203.1
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XplorePlane", "XplorePlane\XplorePlane.csproj", "{07978DB9-4B88-4F42-9054-73992742BD6A}"
|
||||
EndProject
|
||||
|
||||
+28
-2
@@ -8,7 +8,9 @@
|
||||
<!-- 语言配置 可选值: ZhCN, ZhTW, EnUS| Language Configuration -->
|
||||
<add key="Language" value="ZhCN" />
|
||||
<add key="XpData:RootPath" value="D:\XPData" />
|
||||
<add key="UserManual" value="D:\HMQProject\XplorePlane_CT\Code\XplorePlane\XP.App\bin\Debug\net8.0-windows7.0\UserManual.pdf" />
|
||||
<add key="UserManual" value="UserManual.pdf" />
|
||||
<add key="DeviceId" value="PlanerCT001" />
|
||||
|
||||
|
||||
<!-- Serilog日志配置 -->
|
||||
<add key="Serilog:LogPath" value="D:\XplorePlane\Logs" />
|
||||
@@ -123,7 +125,6 @@
|
||||
<!-- 自动重连 | Auto Reconnection -->
|
||||
<add key="Plc:bReConnect" value="true" />
|
||||
|
||||
<!-- ==================== 运动控制配置 | Motion Control Configuration ==================== -->
|
||||
<!-- 直线轴配置(单位:mm)| Linear axis config (unit: mm) -->
|
||||
<add key="MotionControl:SourceZ:Min" value="-500" />
|
||||
<add key="MotionControl:SourceZ:Max" value="500" />
|
||||
@@ -162,6 +163,31 @@
|
||||
<!-- 运行参数 | Runtime parameters -->
|
||||
<add key="MotionControl:PollingInterval" value="500" />
|
||||
<add key="MotionControl:DefaultVelocity" value="500" />
|
||||
|
||||
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
|
||||
<add key="Report:OutputDirectory" value="D:\XplorePlane\Report" />
|
||||
<!-- 报告模板文件路径(相对于应用程序目录或绝对路径)| Template file path (relative to app dir or absolute) -->
|
||||
<add key="Report:TemplatePath" value="Templates\StandardReportTemplate.json" />
|
||||
<!-- 输出文件名模式 | File name pattern -->
|
||||
<add key="Report:FileNamePattern" value="{Date}_{ProductName}_{WorkpieceSN}_{ReportId}" />
|
||||
<!-- 文件名重复时是否自动累加序号 | Auto-increment suffix when file name duplicates-->
|
||||
<add key="Report:AutoIncrementOnDuplicate" value="true" />
|
||||
<!-- 生成后是否自动打开 PDF 阅读器 | Auto-open PDF viewer after generation (true/false) -->
|
||||
<add key="Report:AutoOpenAfterGenerate" value="false" />
|
||||
<!-- 默认页面尺寸 | Default page size (A4) -->
|
||||
<add key="Report:DefaultPageSize" value="A4" />
|
||||
<!-- 默认页面方向 | Default orientation (Portrait / Landscape) -->
|
||||
<add key="Report:DefaultOrientation" value="Portrait" />
|
||||
<!-- 页面边距(mm),有效范围 0-100 | Page margins (mm), valid range 0-100 -->
|
||||
<add key="Report:MarginTop" value="20" />
|
||||
<add key="Report:MarginBottom" value="20" />
|
||||
<add key="Report:MarginLeft" value="20" />
|
||||
<add key="Report:MarginRight" value="20" />
|
||||
<!-- 报告中显示的公司名称 | Company name displayed in report -->
|
||||
<add key="Report:CompanyName" value="海克斯康制造智能技术(青岛)有限公司" />
|
||||
<!-- 公司 Logo 图片路径(可选,为空则不显示)| Company logo path (optional, empty = no logo) -->
|
||||
<add key="Report:CompanyLogo" value="Templates\Logo.png" />
|
||||
|
||||
</appSettings>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
|
||||
@@ -31,6 +31,7 @@ using XP.Hardware.MotionControl.Module;
|
||||
using XP.Hardware.PLC;
|
||||
using XP.Hardware.RaySource.Module;
|
||||
using XP.Hardware.RaySource.Services;
|
||||
using XP.ReportEngine;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Camera;
|
||||
@@ -444,6 +445,7 @@ namespace XplorePlane
|
||||
moduleCatalog.AddModule<DetectorModule>();
|
||||
moduleCatalog.AddModule<RaySourceModule>();
|
||||
moduleCatalog.AddModule<MotionControlModule>();
|
||||
moduleCatalog.AddModule<ReportEngineModule>();
|
||||
|
||||
base.ConfigureModuleCatalog(moduleCatalog);
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand OpenMotionDebugCommand { get; }
|
||||
public DelegateCommand OpenPlcAddrConfigCommand { get; }
|
||||
public DelegateCommand OpenRaySourceConfigCommand { get; }
|
||||
public DelegateCommand OpenReportConfigCommand { get; }
|
||||
public DelegateCommand WarmUpCommand { get; }
|
||||
|
||||
// 测量命令
|
||||
@@ -195,6 +196,7 @@ namespace XplorePlane.ViewModels
|
||||
private Window _settingsWindow;
|
||||
private Window _toolboxWindow;
|
||||
private Window _raySourceConfigWindow;
|
||||
private Window _reportConfigWindow;
|
||||
private object _imagePanelContent;
|
||||
private GridLength _viewportPanelWidth = new(1, GridUnitType.Star);
|
||||
private GridLength _imagePanelWidth = new(320);
|
||||
@@ -308,6 +310,7 @@ namespace XplorePlane.ViewModels
|
||||
OpenMotionDebugCommand = new DelegateCommand(ExecuteOpenMotionDebug);
|
||||
OpenPlcAddrConfigCommand = new DelegateCommand(ExecuteOpenPlcAddrConfig);
|
||||
OpenRaySourceConfigCommand = new DelegateCommand(ExecuteOpenRaySourceConfig);
|
||||
OpenReportConfigCommand = new DelegateCommand(ExecuteOpenReportConfig);
|
||||
WarmUpCommand = new DelegateCommand(ExecuteWarmUp);
|
||||
|
||||
OpenLanguageSwitcherCommand = new DelegateCommand(ExecuteOpenLanguageSwitcher);
|
||||
@@ -399,6 +402,9 @@ namespace XplorePlane.ViewModels
|
||||
try
|
||||
{
|
||||
var manualPath = ConfigurationManager.AppSettings["UserManual"];
|
||||
manualPath = Path.IsPathRooted(manualPath)
|
||||
? manualPath
|
||||
: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, manualPath);
|
||||
if (string.IsNullOrEmpty(manualPath))
|
||||
{
|
||||
_logger.Warn("User manual path is not configured.");
|
||||
@@ -679,6 +685,16 @@ namespace XplorePlane.ViewModels
|
||||
() => new XP.Hardware.RaySource.Views.RaySourceConfigWindow(), "Ray Source Config");
|
||||
}
|
||||
|
||||
private void ExecuteOpenReportConfig()
|
||||
{
|
||||
ShowOrActivate(_reportConfigWindow, w => _reportConfigWindow = w,
|
||||
() =>
|
||||
{
|
||||
var viewModel = _containerProvider.Resolve<XP.ReportEngine.ViewModels.ReportDemoViewModel>();
|
||||
return new XP.ReportEngine.Views.ReportDemoWindow(viewModel);
|
||||
}, "报告配置");
|
||||
}
|
||||
|
||||
private void ExecuteLoadImage()
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
|
||||
@@ -472,6 +472,13 @@
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Text="PLC 地址" />
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开检测报告配置窗口"
|
||||
telerik:ScreenTip.Title="报告配置"
|
||||
Command="{Binding OpenReportConfigCommand}"
|
||||
Size="Medium"
|
||||
SmallImage="/Assets/Icons/message.png"
|
||||
Text="报告配置" />
|
||||
</StackPanel>
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
<ProjectReference Include="..\XP.ImageProcessing.Processors\XP.ImageProcessing.Processors.csproj" />
|
||||
<ProjectReference Include="..\XP.ImageProcessing.RoiControl\XP.ImageProcessing.RoiControl.csproj" />
|
||||
<ProjectReference Include="..\XP.Camera\XP.Camera.csproj" />
|
||||
<ProjectReference Include="..\XP.ReportEngine\XP.ReportEngine.csproj" />
|
||||
</ItemGroup>
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="xcopy "$(MSBuildProjectDirectory)\..\ExternalLibraries\*.*" "$(TargetDir)" /d /y" />
|
||||
|
||||
Reference in New Issue
Block a user