diff --git a/ACP/App.xaml.cs b/ACP/App.xaml.cs index 97e8863..24fceaf 100644 --- a/ACP/App.xaml.cs +++ b/ACP/App.xaml.cs @@ -1,4 +1,5 @@ using Common; +using Command; using ACP.ViewModels; using ACP.ViewModels.Dialogs; using ACP.Views; @@ -12,6 +13,8 @@ using Service.Interface; using System.Configuration; using System.Data; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using UIShare.PubEvent; using static System.Runtime.InteropServices.JavaScript.JSType; @@ -23,6 +26,7 @@ using DeviceCommand.Base; using AutoMapper; using Microsoft.Extensions.Logging.Abstractions; using ACP.Profiles; +using Prism.Dialogs; using UIShare.Helpers; namespace ACP @@ -60,6 +64,56 @@ namespace ACP } protected override void OnInitialized() { + // 向命令库 CommandDialog 注入弹窗能力(命令库是纯 .NET 类库,不引用 WPF,这里通过委托实现依赖倒置) + var dialogService = Container.Resolve(); + CommandDialog.弹窗处理器 = (弹窗类型, 弹窗详细, 是否阻塞, 自动关闭秒数, ct) => + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + CancellationTokenRegistration registration = default; + if (是否阻塞) + { + registration = ct.Register(() => tcs.TrySetCanceled(ct)); + } + // 命令可能在后台线程执行,统一切回 UI 线程弹窗 + Application.Current.Dispatcher.Invoke(() => + { + var param = new DialogParameters + { + { "Title", 弹窗类型 switch + { + CommandDialog.DialogType.Warning => "警告", + CommandDialog.DialogType.Error => "错误", + _ => "信息提示" + } }, + { "Message", 弹窗详细 ?? string.Empty }, + { "Icon", 弹窗类型 switch + { + CommandDialog.DialogType.Warning => "warn", + CommandDialog.DialogType.Error => "error", + _ => "info" + } }, + { "ShowOk", true }, + { "AutoCloseSeconds", (double)自动关闭秒数 } + }; + if (是否阻塞) + { + // 阻塞:用户手动关闭或自动关闭后才继续执行步骤 + dialogService.ShowDialog("MessageBox", param, _ => + { + registration.Dispose(); + tcs.TrySetResult(true); + }); + } + else + { + // 非阻塞:弹出后步骤立即继续(不等关闭回调) + dialogService.Show("MessageBox", param, _ => { }); + tcs.TrySetResult(true); + } + }); + return tcs.Task; + }; + // 配置全局日志分发器:按 CurrentScope 路由到对应 LogArea var globalInfo = Container.Resolve(); LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo); @@ -126,6 +180,7 @@ namespace ACP //注册服务 containerRegistry.Register(); containerRegistry.Register(); + containerRegistry.Register(); } //指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中) protected override IModuleCatalog CreateModuleCatalog() diff --git a/Common/Tool/ExpressionEvaluator.cs b/Common/Tool/ExpressionEvaluator.cs index 7b0bf17..6ec9efa 100644 --- a/Common/Tool/ExpressionEvaluator.cs +++ b/Common/Tool/ExpressionEvaluator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -23,7 +24,7 @@ namespace Common.Tools { foreach (var kvp in processedVariables) { - expr.Parameters[kvp.Key] = kvp.Value; + expr.Parameters[kvp.Key] = NormalizeValue(kvp.Value); } } @@ -95,6 +96,29 @@ namespace Common.Tools return (processedExpression, newVariables); } + /// + /// 归一化变量取值:输入变量的值常以字符串形式保存(如 "10"), + /// 若直接参与比较,NCalc 会按字符串逐位比较("10" > "5" 为假), + /// 导致大于/小于判断失真。此处将可解析为数字/布尔的字符串转换为对应类型, + /// 使比较按数值语义进行;无法解析的字符串保持原样。 + /// + private static object? NormalizeValue(object? value) + { + if (value is string s) + { + if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) + || double.TryParse(s, out d)) + { + return d; + } + if (bool.TryParse(s, out var b)) + { + return b; + } + } + return value; + } + // 检查字符串是否包含中文字符 private static bool ContainsChinese(string text) { diff --git a/ExportModule/ViewModels/ExportViewModel.cs b/ExportModule/ViewModels/ExportViewModel.cs index 730beba..cfe8fe5 100644 --- a/ExportModule/ViewModels/ExportViewModel.cs +++ b/ExportModule/ViewModels/ExportViewModel.cs @@ -82,19 +82,23 @@ namespace ExportModule.ViewModels public ICommand LoadedCommand { get; } public ICommand QueryCommand { get; } public ICommand ExportCommand { get; } + public ICommand ExportReportCommand { get; } #endregion #region 私有字段 private readonly ITestReportService _testReportService; + private readonly ITestCheckRecordService _testCheckRecordService; #endregion public ExportViewModel(IContainerProvider containerProvider) : base(containerProvider) { _testReportService = containerProvider.Resolve(); + _testCheckRecordService = containerProvider.Resolve(); LoadedCommand = new AsyncDelegateCommand(OnLoad); QueryCommand = new AsyncDelegateCommand(OnQuery); ExportCommand = new AsyncDelegateCommand(OnExport); + ExportReportCommand = new AsyncDelegateCommand(OnExportReportCommand); } #region 命令处理 @@ -184,6 +188,40 @@ namespace ExportModule.ViewModels ShowInfoMessageBox($"导出完成,共 {entities.Count} 条步骤记录,已保存至:{dialog.FileName}", () => { }); } + /// + /// 导出测试报告:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细 + /// + private async Task OnExportReportCommand() + { + if (SelectedTestReport == null) + { + ShowErrorMessageBox("请先在列表中选择一条测试记录。", () => { }); + return; + } + + StatusMessage = "正在查询测试项判断记录..."; + var checkResult = await _testCheckRecordService.GetByTestRoundIdAsync(SelectedTestReport.TestRoundId); + if (!checkResult.IsSuccess || checkResult.Data == null || checkResult.Data.Count == 0) + { + ShowErrorMessageBox("未找到该测试记录的测试项判断数据(需将子程序标记为测试项后运行)。", () => { }); + return; + } + + var dialog = new SaveFileDialog + { + Filter = "Excel 工作簿 (*.xlsx)|*.xlsx|所有文件 (*.*)|*.*", + DefaultExt = ".xlsx", + FileName = $"测试报告_{SelectedTestReport.Scope}_{SelectedTestReport.StartTime:yyyyMMdd_HHmmss}.xlsx" + }; + + if (dialog.ShowDialog() != true) return; + + var records = checkResult.Data; + StatusMessage = $"正在导出 {records.Count} 条测试项判断记录..."; + await Task.Run(() => ExportReportToExcel(dialog.FileName, records, SelectedTestReport)); + ShowInfoMessageBox($"导出完成,已保存至:{dialog.FileName}", () => { }); + } + #endregion #region 辅助方法 @@ -245,6 +283,134 @@ namespace ExportModule.ViewModels workbook.SaveAs(filePath); } + /// + /// 测试报告导出:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细(含重复判断、判断时间、数据值) + /// + private static void ExportReportToExcel(string filePath, List records, TestReportModel report) + { + using var workbook = new XLWorkbook(); + var ws = workbook.Worksheets.Add("测试报告"); + const int colCount = 6; + + var passColor = XLColor.FromArgb(0xB2, 0xFF, 0xB2); // 通过 - 浅绿(与现有导出配色一致) + var ngColor = XLColor.FromArgb(0xFF, 0xB2, 0xB2); // 失败 - 浅红 + var headerColor = XLColor.FromArgb(0xEC, 0xEF, 0xF4); + + int row = 1; + + // ===== 标题与信息行 ===== + ws.Range(row, 1, row, colCount).Merge(); + ws.Cell(row, 1).Value = "测 试 报 告"; + ws.Range(row, 1, row, colCount).Style.Font.Bold = true; + ws.Range(row, 1, row, colCount).Style.Font.FontSize = 16; + ws.Range(row, 1, row, colCount).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + row++; + ws.Cell(row, 1).Value = $"台架:{report.Scope ?? ""}"; + ws.Cell(row, 3).Value = $"测试文件:{report.FileName ?? ""}"; + row++; + ws.Cell(row, 1).Value = $"开始时间:{report.StartTime:yyyy-MM-dd HH:mm:ss}"; + ws.Cell(row, 3).Value = $"结束时间:{report.EndTime:yyyy-MM-dd HH:mm:ss}"; + row += 2; + + // 按测试项分组(保持首次出现顺序) + var groups = records.GroupBy(r => r.TestItemName).ToList(); + + // ===== 上层:测试项汇总 ===== + ws.Cell(row, 1).Value = "【测试项汇总】"; + ws.Cell(row, 1).Style.Font.Bold = true; + row++; + + string[] summaryHeaders = { "序号", "测试项", "判定结果", "执行次数", "判定次数", "NG次数" }; + for (int c = 0; c < summaryHeaders.Length; c++) + ws.Cell(row, c + 1).Value = summaryHeaders[c]; + var headerRange = ws.Range(row, 1, row, colCount); + headerRange.Style.Font.Bold = true; + headerRange.Style.Fill.BackgroundColor = headerColor; + headerRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + headerRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + row++; + + int seq = 1; + foreach (var group in groups) + { + var summaries = group.Where(x => x.IsSummary).ToList(); + var details = group.Where(x => !x.IsSummary).ToList(); + bool overallPass = summaries.All(s => s.Pass) && details.All(d => d.Pass); + + ws.Cell(row, 1).Value = seq++; + ws.Cell(row, 2).Value = group.Key; + ws.Cell(row, 3).Value = overallPass ? "PASS" : "NG"; + ws.Cell(row, 4).Value = summaries.Count; + ws.Cell(row, 5).Value = details.Count; + ws.Cell(row, 6).Value = details.Count(d => !d.Pass); + + var dataRange = ws.Range(row, 1, row, colCount); + dataRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + dataRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + ws.Cell(row, 3).Style.Font.Bold = true; + ws.Cell(row, 3).Style.Fill.BackgroundColor = overallPass ? passColor : ngColor; + row++; + } + row++; + + // ===== 下层:判断明细 ===== + ws.Cell(row, 1).Value = "【判断明细】"; + ws.Cell(row, 1).Style.Font.Bold = true; + row++; + + string[] detailHeaders = { "序号", "判定时间", "步骤名称", "OKExpression", "判定结果", "数据值" }; + foreach (var group in groups) + { + var details = group.Where(x => !x.IsSummary).ToList(); + bool overallPass = group.Where(x => x.IsSummary).All(s => s.Pass) && details.All(d => d.Pass); + + // 测试项块标题行(合并单元格) + ws.Range(row, 1, row, colCount).Merge(); + ws.Cell(row, 1).Value = $"■ 测试项:{group.Key}  总体结果:{(overallPass ? "PASS" : "NG")}"; + ws.Cell(row, 1).Style.Font.Bold = true; + ws.Range(row, 1, row, colCount).Style.Fill.BackgroundColor = overallPass ? passColor : ngColor; + row++; + + for (int c = 0; c < detailHeaders.Length; c++) + ws.Cell(row, c + 1).Value = detailHeaders[c]; + var detailHeaderRange = ws.Range(row, 1, row, colCount); + detailHeaderRange.Style.Font.Bold = true; + detailHeaderRange.Style.Fill.BackgroundColor = headerColor; + detailHeaderRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + detailHeaderRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + row++; + + if (details.Count == 0) + { + ws.Range(row, 1, row, colCount).Merge(); + ws.Cell(row, 1).Value = "(无 OKExpression 判断,结果由步骤执行成败决定)"; + row++; + } + + int detailSeq = 1; + foreach (var d in details) + { + ws.Cell(row, 1).Value = detailSeq++; + ws.Cell(row, 2).Value = d.CreateTime.ToString("yyyy-MM-dd HH:mm:ss.fff"); + ws.Cell(row, 3).Value = d.StepName ?? ""; + ws.Cell(row, 4).Value = d.OKExpression ?? ""; + ws.Cell(row, 5).Value = d.Pass ? "PASS" : "NG"; + ws.Cell(row, 6).Value = d.Values ?? ""; + + var detailRange = ws.Range(row, 1, row, colCount); + detailRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; + detailRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; + ws.Cell(row, 5).Style.Font.Bold = true; + ws.Cell(row, 5).Style.Fill.BackgroundColor = d.Pass ? passColor : ngColor; + row++; + } + row++; + } + + ws.Columns().AdjustToContents(); + workbook.SaveAs(filePath); + } + #endregion #region 生命周期 diff --git a/ExportModule/Views/ExportView.xaml b/ExportModule/Views/ExportView.xaml index ce5ac3a..7c38cd5 100644 --- a/ExportModule/Views/ExportView.xaml +++ b/ExportModule/Views/ExportView.xaml @@ -60,7 +60,7 @@ - + @@ -116,12 +116,19 @@ Margin="4,0" VerticalAlignment="Center"/>