Files
XplorePlane/XP.ReportEngine/Configs/ReportConfig.cs
T
QI Mingxuan d72b583319 [XP.ReportEngine] 新增 TXT/CSV/Excel 三种报告输出格式支持;Excel (.xlsx) 报告生成器,基于 ClosedXML,支持结构化生成和模板填充两种模式; 新增 XplorePlane_ReportTemplate.xlsx:Excel 报告模板文件。
[XP.ReportEngine] 模型与配置扩展:ReportOutputFormat 枚举:新增 Excel=1, Csv=2, Txt=3(显式序号,向后兼容);新增多格式相关配置加载。
2026-07-01 11:31:41 +08:00

345 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
/// 报告中显示的软件名称 | Software name displayed in report
/// </summary>
public string SoftwareName { get; set; } = "XplorePlane";
/// <summary>
/// 软件 Logo 图片路径(可选,为空则不显示)| Software logo image path (optional, empty means no logo)
/// </summary>
public string SoftwareLogo { get; set; } = string.Empty;
/// <summary>
/// 默认 Excel 模板文件路径(可选,相对或绝对)| Default Excel template file path (optional, relative or absolute)
/// 配置且指向有效 .xlsx 时走基于模板生成(需求 12);为空时走默认结构化生成(需求 11)
/// Configured and pointing to a valid .xlsx → template-based generation (Req 12); empty → default structured generation (Req 11)
/// </summary>
public string DefaultExcelTemplatePath { get; set; } = string.Empty;
/// <summary>
/// TXT/CSV 文本编码(可选)| Text encoding for TXT/CSV (optional)
/// 取值:Utf8Bom(默认)/ Utf8 / GB2312 / GBK | Values: Utf8Bom (default) / Utf8 / GB2312 / GBK
/// </summary>
public string TextEncoding { get; set; } = "Utf8Bom";
/// <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>
/// 获取解析后的默认 Excel 模板绝对路径 | Get resolved absolute default Excel template path
/// 与 GetResolvedTemplatePath 采用相同的相对/绝对路径解析逻辑
/// Uses the same relative/absolute path resolution logic as GetResolvedTemplatePath
/// 当 DefaultExcelTemplatePath 为空时返回空字符串(表示未配置模板)
/// Returns empty string when DefaultExcelTemplatePath is empty (means no template configured)
/// </summary>
public string GetResolvedExcelTemplatePath()
{
if (string.IsNullOrWhiteSpace(DefaultExcelTemplatePath))
{
return string.Empty;
}
if (Path.IsPathRooted(DefaultExcelTemplatePath))
{
return DefaultExcelTemplatePath;
}
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DefaultExcelTemplatePath);
}
/// <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>
/// 自动累加序号的上限 | Upper limit for auto-increment suffix
/// 需求 7.4/7.6:最多尝试至 9999,超过则视为无法生成唯一文件名。
/// Requirement 7.4/7.6: try up to 9999; beyond that a unique file name cannot be generated.
/// </summary>
public const int MaxDuplicateIncrement = 9999;
/// <summary>
/// 解析输出文件路径的结果(含成功标志与错误信息)| Result of resolving output file path (with success flag and error message)
/// 供报告服务在多格式编排时区分“路径已就绪”“文件已存在(不覆盖)”“累加上限”“路径非法”等情形。
/// Used by the report service during multi-format orchestration to distinguish
/// "path ready", "file exists (no overwrite)", "increment limit reached", and "illegal path".
/// </summary>
public sealed class OutputPathResolution
{
/// <summary>是否解析成功 | Whether the resolution succeeded</summary>
public bool IsSuccess { get; private set; }
/// <summary>解析得到的完整文件路径(成功时非空)| Resolved full file path (non-null on success)</summary>
public string FilePath { get; private set; }
/// <summary>错误信息(失败时非空)| Error message (non-null on failure)</summary>
public string ErrorMessage { get; private set; }
/// <summary>构造成功结果 | Build a success result</summary>
public static OutputPathResolution Success(string filePath)
=> new OutputPathResolution { IsSuccess = true, FilePath = filePath };
/// <summary>构造失败结果 | Build a failure result</summary>
public static OutputPathResolution Failure(string errorMessage)
=> new OutputPathResolution { IsSuccess = false, ErrorMessage = errorMessage };
}
/// <summary>
/// 解析最终输出文件完整路径并返回带错误信息的结果 | Resolve final output file full path and return a result with error info
/// 覆盖需求 7.4(累加不重复)、7.5(不覆盖时文件已存在错误)、7.6(累加上限错误)、7.8(路径非法错误)。
/// Covers Req 7.4 (increment without duplicate), 7.5 (file-exists error when not overwriting),
/// 7.6 (increment-limit error), 7.8 (illegal path error).
/// </summary>
/// <param name="parameters">占位符参数字典 | Placeholder parameter dictionary</param>
/// <param name="extension">文件扩展名(含点号,如 ".txt"| File extension (with dot, e.g. ".txt")</param>
/// <returns>解析结果 | Resolution result</returns>
public OutputPathResolution TryResolveOutputFilePath(Dictionary<string, string> parameters, string extension)
{
var baseName = ResolveFileName(parameters);
var outputDir = OutputDirectory;
// 需求 7.8:校验输出目录路径是否含非法字符 | Req 7.8: validate output directory path for illegal characters
if (string.IsNullOrWhiteSpace(outputDir) || outputDir.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
{
return OutputPathResolution.Failure(
$"输出目录路径非法: {outputDir} | Illegal output directory path: {outputDir}");
}
// 需求 7.8:校验最终文件名是否含非法字符(扩展名一并校验)| Req 7.8: validate file name (including extension) for illegal chars
var fileName = baseName + extension;
if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
{
return OutputPathResolution.Failure(
$"输出文件名非法: {fileName} | Illegal output file name: {fileName}");
}
// 确保输出目录存在 | Ensure output directory exists
try
{
if (!Directory.Exists(outputDir))
{
Directory.CreateDirectory(outputDir);
}
}
catch (Exception ex)
{
return OutputPathResolution.Failure(
$"无法创建输出目录: {outputDir}{ex.Message} | Cannot create output directory: {outputDir}, {ex.Message}");
}
var filePath = Path.Combine(outputDir, fileName);
if (File.Exists(filePath))
{
// 需求 7.5:不覆盖且文件已存在 → 错误 | Req 7.5: no overwrite and file exists → error
if (!AutoIncrementOnDuplicate)
{
return OutputPathResolution.Failure(
$"目标文件已存在且不允许覆盖: {filePath} | Target file already exists and overwrite is disabled: {filePath}");
}
// 需求 7.4/7.6:累加序号(最多至 9999),超上限 → 错误 | Req 7.4/7.6: increment (up to 9999); beyond limit → error
int counter = 1;
string newPath = null;
bool found = false;
for (; counter <= MaxDuplicateIncrement; counter++)
{
newPath = Path.Combine(outputDir, $"{baseName}({counter}){extension}");
if (!File.Exists(newPath))
{
found = true;
break;
}
}
if (!found)
{
return OutputPathResolution.Failure(
$"累加序号已达上限({MaxDuplicateIncrement}),无法生成唯一文件名: {baseName}{extension} | " +
$"Increment limit ({MaxDuplicateIncrement}) reached, cannot generate a unique file name: {baseName}{extension}");
}
filePath = newPath;
}
return OutputPathResolution.Success(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;
}
}
}