Files
XplorePlane/XP.ReportEngine/ViewModels/ReportDemoViewModel.cs
T

693 lines
27 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;
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
/// 演示如何使用 IReportService 生成 PDF 报告
/// Demonstrates how to use IReportService to generate PDF reports
/// </summary>
public class ReportDemoViewModel : BindableBase
{
private readonly IReportService _reportService;
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 string _lastOutputDir;
private bool _isGenerating;
private bool _formatPdf = true;
private bool _formatExcel;
private bool _formatCsv;
private bool _formatTxt;
#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();
}
}
}
/// <summary>
/// 是否生成 PDF 格式 | Whether to generate PDF format
/// </summary>
public bool FormatPdf
{
get => _formatPdf;
set => SetProperty(ref _formatPdf, value);
}
/// <summary>
/// 是否生成 Excel 格式 | Whether to generate Excel format
/// </summary>
public bool FormatExcel
{
get => _formatExcel;
set => SetProperty(ref _formatExcel, value);
}
/// <summary>
/// 是否生成 CSV 格式 | Whether to generate CSV format
/// </summary>
public bool FormatCsv
{
get => _formatCsv;
set => SetProperty(ref _formatCsv, value);
}
/// <summary>
/// 是否生成 TXT 格式 | Whether to generate TXT format
/// </summary>
public bool FormatTxt
{
get => _formatTxt;
set => SetProperty(ref _formatTxt, value);
}
#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; }
/// <summary>
/// 打开输出目录命令 | Open output folder command
/// </summary>
public DelegateCommand OpenOutputFolderCommand { get; }
#endregion
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public ReportDemoViewModel(
IReportService reportService,
IPdfViewerService pdfViewerService,
IPdfPrintService pdfPrintService,
ILoggerService logger,
ReportIdGenerator reportIdGenerator,
ReportConfig reportConfig)
{
_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));
_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));
OpenOutputFolderCommand = new DelegateCommand(OpenOutputFolder, () => !string.IsNullOrEmpty(_lastOutputDir));
}
/// <summary>
/// 生成报告(带进度条,支持多格式)| Generate report (with progress window, multi-format)
/// </summary>
private async Task GenerateReportAsync()
{
IsGenerating = true;
StatusMessage = "正在生成报告...";
try
{
// 收集用户勾选的输出格式 | Collect selected output formats
var selectedFormats = new List<ReportOutputFormat>();
if (FormatPdf) selectedFormats.Add(ReportOutputFormat.Pdf);
if (FormatExcel) selectedFormats.Add(ReportOutputFormat.Excel);
if (FormatCsv) selectedFormats.Add(ReportOutputFormat.Csv);
if (FormatTxt) selectedFormats.Add(ReportOutputFormat.Txt);
if (selectedFormats.Count == 0)
{
MessageBox.Show("请至少选择一种输出格式。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
StatusMessage = "未选择输出格式";
IsGenerating = false;
return;
}
// 构建文件名占位符参数 | 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"
};
// 显示即将生成的格式列表 | Show formats to be generated
var formatNames = string.Join(", ", selectedFormats);
var defaultOutputDir = _reportConfig.OutputDirectory;
var result = MessageBox.Show(
$"将生成以下格式的报告:{formatNames}\r\n\r\n输出目录:{defaultOutputDir}\r\n\r\n是否继续?",
"报告生成确认",
MessageBoxButton.OKCancel,
MessageBoxImage.Question);
if (result == MessageBoxResult.Cancel)
{
StatusMessage = "已取消生成";
IsGenerating = false;
return;
}
// 创建进度条窗口 | 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: Build report request
progressWindow.UpdateProgress("正在组装报告数据...", 30);
await Task.Delay(200);
var request = new ReportRequest
{
ProcessorOutputs = processorOutputs,
Metadata = new ReportMetadata
{
ReportId = fileNameParams["ReportId"],
InspectionDate = DateTime.Now,
SampleName = ProductName,
OperatorName = OperatorName,
Description = Description
},
FileNameParameters = fileNameParams,
Formats = selectedFormats
};
// 注入工件整体图片 | Inject workpiece overview image
var workpieceImagePath = System.IO.Path.Combine(MockImageDirectory, "OverView.png");
if (File.Exists(workpieceImagePath))
{
request.AdditionalImages["workpieceImage"] = new ImageData
{
SourceType = ImageSourceType.FilePath,
FilePath = workpieceImagePath
};
}
// 步骤 3:调用报告服务生成(在后台线程执行,避免阻塞 UI)
// Step 3: Call report service (on background thread to avoid blocking UI)
progressWindow.UpdateProgress($"正在生成报告({formatNames}...", 60);
await Task.Delay(200);
var genResult = await Task.Run(() => _reportService.GenerateAsync(request));
// 步骤 4:处理结果 | Step 4: Handle result
progressWindow.UpdateProgress("正在完成...", 95);
await Task.Delay(200);
if (genResult.IsSuccess)
{
_lastOutputPath = genResult.OutputFilePath;
_lastOutputDir = Path.GetDirectoryName(genResult.OutputFilePath);
// 构建状态信息:列出所有生成的文件 | Build status: list all generated files
if (genResult.OutputFilePaths != null && genResult.OutputFilePaths.Count > 0)
{
var paths = new List<string>();
foreach (var output in genResult.OutputFilePaths)
{
paths.Add($"{output.Format}: {Path.GetFileName(output.FilePath)}");
}
StatusMessage = $"生成成功 ({genResult.OutputFilePaths.Count} 个文件){string.Join(" | ", paths)}";
}
else
{
StatusMessage = $"报告生成成功:{genResult.OutputFilePath}";
}
_logger.Info("报告生成成功:{Path} | Report generated successfully: {Path}", genResult.OutputFilePath);
progressWindow.UpdateProgress("报告生成完成!", 100);
await Task.Delay(500);
// 根据配置自动打开 PDF 阅读器(仅当生成了 PDF 时)
// Auto-open PDF viewer based on config (only when PDF was generated)
if (_reportConfig.AutoOpenAfterGenerate && FormatPdf && !string.IsNullOrEmpty(genResult.OutputFilePath))
{
try
{
_pdfViewerService.OpenViewer(genResult.OutputFilePath);
}
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();
OpenOutputFolderCommand.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);
}
}
/// <summary>
/// 打开输出目录 | Open output folder in Explorer
/// </summary>
private void OpenOutputFolder()
{
if (string.IsNullOrEmpty(_lastOutputDir) || !Directory.Exists(_lastOutputDir))
{
MessageBox.Show("输出目录不存在,请先生成报告。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
System.Diagnostics.Process.Start("explorer.exe", _lastOutputDir);
}
catch (Exception ex)
{
_logger.Error(ex, "打开输出目录失败 | Failed to open output folder: {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:\XPData\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()
{
// 生成 64 组 BGA 球模拟数据 | Generate 64 BGA ball mock data
var bgaBalls = new List<Dictionary<string, object>>();
var random = new Random(42); // 固定种子确保可重复 | Fixed seed for reproducibility
var voidLimit = 0.05;
for (int i = 1; i <= 64; i++)
{
// 大部分合格,少数不合格 | Most pass, a few fail
double voidRate;
if (i == 12 || i == 27 || i == 41 || i == 58)
{
// 这几个球不合格 | These balls fail
voidRate = Math.Round(0.05 + random.NextDouble() * 0.03, 4);
}
else
{
voidRate = Math.Round(random.NextDouble() * 0.045, 4);
}
var area = Math.Round(190.0 + random.NextDouble() * 15.0, 1);
var classification = voidRate > voidLimit ? "Fail" : "Pass";
bgaBalls.Add(new Dictionary<string, object>
{
["Index"] = i,
["VoidRate"] = voidRate,
["Area"] = area,
["Classification"] = classification
});
}
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"] = bgaBalls
},
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
};
}
#endregion
}
}