Compare commits
3
Commits
6a59552bc7
...
7a56eb98d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a56eb98d2 | ||
|
|
61d7293afb | ||
|
|
47b4d3837b |
@@ -1,4 +1,5 @@
|
||||
using Common;
|
||||
using Command;
|
||||
using ADP.ViewModels;
|
||||
using ADP.ViewModels.Dialogs;
|
||||
using ADP.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 ADP.Profiles;
|
||||
using Prism.Dialogs;
|
||||
using 加密狗;
|
||||
|
||||
namespace ADP
|
||||
@@ -60,6 +64,56 @@ namespace ADP
|
||||
}
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// 向命令库 CommandDialog 注入弹窗能力(命令库是纯 .NET 类库,不引用 WPF,这里通过委托实现依赖倒置)
|
||||
var dialogService = Container.Resolve<IDialogService>();
|
||||
CommandDialog.弹窗处理器 = (弹窗类型, 弹窗详细, 是否阻塞, 自动关闭秒数, ct) =>
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(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<GlobalInfo>();
|
||||
LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
|
||||
@@ -152,6 +206,7 @@ namespace ADP
|
||||
//注册服务
|
||||
containerRegistry.Register<IMonitorValueService, MonitorValueService>();
|
||||
containerRegistry.Register<ITestReportService, TestReportService>();
|
||||
containerRegistry.Register<ITestCheckRecordService, TestCheckRecordService>();
|
||||
}
|
||||
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
|
||||
protected override IModuleCatalog CreateModuleCatalog()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.ViewModelBase;
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace ADP.ViewModels.Dialogs
|
||||
{
|
||||
@@ -69,6 +71,11 @@ namespace ADP.ViewModels.Dialogs
|
||||
|
||||
public DialogCloseListener RequestClose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 自动关闭定时器:自动关闭秒数大于 0 时启动,到时自动关闭弹窗(同时解除阻塞等待)
|
||||
/// </summary>
|
||||
private DispatcherTimer _autoCloseTimer;
|
||||
|
||||
public MessageBoxViewModel(IContainerProvider containerProvider):base(containerProvider)
|
||||
{
|
||||
YesCommand = new DelegateCommand(OnYes);
|
||||
@@ -93,6 +100,7 @@ namespace ADP.ViewModels.Dialogs
|
||||
|
||||
public override void OnDialogClosed()
|
||||
{
|
||||
_autoCloseTimer?.Stop();
|
||||
_eventAggregator.GetEvent<OverlayEvent>().Publish(false);
|
||||
}
|
||||
|
||||
@@ -117,6 +125,21 @@ namespace ADP.ViewModels.Dialogs
|
||||
ShowNo = parameters.GetValue<bool>("ShowNo");
|
||||
ShowOk = parameters.GetValue<bool>("ShowOk");
|
||||
ShowCancel = parameters.GetValue<bool>("ShowCancel");
|
||||
|
||||
// 自动关闭:秒数大于 0 时启动定时器,到时自动关闭(供命令库 弹窗 命令的自动关闭参数使用)
|
||||
if (parameters.TryGetValue("AutoCloseSeconds", out double autoCloseSeconds) && autoCloseSeconds > 0)
|
||||
{
|
||||
_autoCloseTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(autoCloseSeconds)
|
||||
};
|
||||
_autoCloseTimer.Tick += (s, e) =>
|
||||
{
|
||||
_autoCloseTimer.Stop();
|
||||
RequestClose.Invoke(new DialogResult(ButtonResult.OK));
|
||||
};
|
||||
_autoCloseTimer.Start();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using Common.Attributes;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Command
|
||||
{
|
||||
/// <summary>
|
||||
/// 对话框命令:在测试流程中弹出提示窗口。
|
||||
/// 命令库为纯 .NET 类库,不直接引用 WPF 程序集;
|
||||
/// 实际的弹窗能力由宿主程序(ADP 外壳)在启动时注入到 <see cref="弹窗处理器"/> 委托中实现(依赖倒置)。
|
||||
/// </summary>
|
||||
[ADPCommand]
|
||||
public static class CommandDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// 弹窗类型
|
||||
/// </summary>
|
||||
public enum DialogType
|
||||
{
|
||||
/// <summary>
|
||||
/// 信息提示
|
||||
/// </summary>
|
||||
Info,
|
||||
/// <summary>
|
||||
/// 警告提示
|
||||
/// </summary>
|
||||
Warning,
|
||||
/// <summary>
|
||||
/// 错误提示
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗处理委托(参数依次为:弹窗类型、弹窗详细、是否阻塞、自动关闭秒数、取消令牌)。
|
||||
/// 由宿主程序启动时注入实现;未注入时弹窗命令降级为仅输出日志,不会抛异常。
|
||||
/// </summary>
|
||||
[Browsable(false)]
|
||||
public static Func<DialogType, string, bool, float, CancellationToken, Task> 弹窗处理器;
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗:在界面上显示一个提示对话框。
|
||||
/// </summary>
|
||||
/// <param name="弹窗类型">弹窗样式:Info 信息 / Warning 警告 / Error 错误</param>
|
||||
/// <param name="弹窗详细">弹窗中显示的详细内容</param>
|
||||
/// <param name="是否阻塞">true = 步骤暂停,等待用户关闭(或自动关闭)后才继续;false = 弹出后步骤立即继续</param>
|
||||
/// <param name="自动关闭秒数">大于 0 时,弹窗在指定秒数后自动关闭;小于等于 0 时不自动关闭,需用户手动关闭</param>
|
||||
/// <param name="ct">异步取消令牌</param>
|
||||
public static async Task 弹窗(DialogType 弹窗类型, string 弹窗详细, bool 是否阻塞, float 自动关闭秒数, CancellationToken ct)
|
||||
{
|
||||
var handler = 弹窗处理器;
|
||||
if (handler == null)
|
||||
{
|
||||
Console.WriteLine($"[弹窗命令] 宿主未注入弹窗处理器,跳过弹窗:{弹窗详细}");
|
||||
return;
|
||||
}
|
||||
await handler(弹窗类型, 弹窗详细, 是否阻塞, 自动关闭秒数, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 归一化变量取值:输入变量的值常以字符串形式保存(如 "10"),
|
||||
/// 若直接参与比较,NCalc 会按字符串逐位比较("10" > "5" 为假),
|
||||
/// 导致大于/小于判断失真。此处将可解析为数字/布尔的字符串转换为对应类型,
|
||||
/// 使比较按数值语义进行;无法解析的字符串保持原样。
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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<ITestReportService>();
|
||||
_testCheckRecordService = containerProvider.Resolve<ITestCheckRecordService>();
|
||||
|
||||
LoadedCommand = new AsyncDelegateCommand(OnLoad);
|
||||
QueryCommand = new AsyncDelegateCommand(OnQuery);
|
||||
ExportCommand = new AsyncDelegateCommand(OnExport);
|
||||
ExportReportCommand = new AsyncDelegateCommand(OnExportReportCommand);
|
||||
}
|
||||
|
||||
#region 命令处理
|
||||
@@ -183,6 +187,39 @@ namespace ExportModule.ViewModels
|
||||
await Task.Run(() => ExportToExcel(dialog.FileName, entities));
|
||||
ShowInfoMessageBox($"导出完成,共 {entities.Count} 条步骤记录,已保存至:{dialog.FileName}", () => { });
|
||||
}
|
||||
/// <summary>
|
||||
/// 导出测试报告:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
@@ -263,6 +300,134 @@ namespace ExportModule.ViewModels
|
||||
workbook.SaveAs(filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试报告导出:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细(含重复判断、判断时间、数据值)
|
||||
/// </summary>
|
||||
private static void ExportReportToExcel(string filePath, List<TestCheckRecordEntity> 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 生命周期
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
@@ -109,13 +110,20 @@
|
||||
Margin="4,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="7"
|
||||
Content="导出 Excel"
|
||||
Content="导出详细测试步骤"
|
||||
Command="{Binding ExportCommand}"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Padding="16,6"
|
||||
Margin="4,0"
|
||||
VerticalAlignment="Center"/>
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="8"
|
||||
Content="导出测试报告"
|
||||
Command="{Binding ExportReportCommand}"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Padding="16,6"
|
||||
Margin="4,0"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="9"
|
||||
Content="加载全部"
|
||||
Command="{Binding LoadedCommand}"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using SqlSugar;
|
||||
using System;
|
||||
|
||||
namespace Model.Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试项判断记录:运行过程中测试项(IsTestItem 子程序)范围内每一次 OKExpression 判断。
|
||||
/// 同一次运行的所有记录共享同一个 TestRoundId,导出测试报告时按此 Guid 查询。
|
||||
/// IsSummary=true 的行为该测试项单次执行的汇总(PASS/NG),IsSummary=false 的行为单次判断明细。
|
||||
/// </summary>
|
||||
public class TestCheckRecordEntity : BaseEntity
|
||||
{
|
||||
/// <summary>同一次运行的统一标识(StepRunning.TestRoundID)</summary>
|
||||
[SugarColumn(ColumnName = "TestRoundId", ColumnDescription = "运行轮次标识")]
|
||||
public Guid TestRoundId { get; set; }
|
||||
|
||||
/// <summary>作用域/台架名称</summary>
|
||||
[SugarColumn(ColumnName = "Scope", ColumnDescription = "台架名称")]
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>测试项名称(当前ADP文件路径)</summary>
|
||||
[SugarColumn(ColumnName = "FileName", ColumnDescription = "测试项名称")]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>测试项名称(IsTestItem 子程序步骤的名称)</summary>
|
||||
[SugarColumn(ColumnName = "TestItemName", Length = 200)]
|
||||
public string TestItemName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>发生判断的步骤名称</summary>
|
||||
[SugarColumn(ColumnName = "StepName", Length = 200)]
|
||||
public string StepName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>子程序嵌套深度(0=主程序)</summary>
|
||||
[SugarColumn(ColumnName = "Depth")]
|
||||
public int Depth { get; set; }
|
||||
|
||||
/// <summary>判断表达式</summary>
|
||||
[SugarColumn(ColumnName = "OKExpression", Length = 1000, IsNullable = true)]
|
||||
public string? OKExpression { get; set; }
|
||||
|
||||
/// <summary>判断结果(true=PASS / false=NG)</summary>
|
||||
[SugarColumn(ColumnName = "Pass")]
|
||||
public bool Pass { get; set; }
|
||||
|
||||
/// <summary>表达式变量的实际取值(如 "电压=12.5; 电流=3.2; ")</summary>
|
||||
[SugarColumn(ColumnName = "Values", Length = 2000, IsNullable = true)]
|
||||
public string? Values { get; set; }
|
||||
|
||||
/// <summary>是否为汇总行(每个测试项单次执行结束时写入一条)</summary>
|
||||
[SugarColumn(ColumnName = "IsSummary")]
|
||||
public bool IsSummary { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ namespace Model.Models
|
||||
|
||||
public bool IsUsed { get; set; } = true;
|
||||
|
||||
/// <summary>是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断)</summary>
|
||||
public bool IsTestItem { get; set; } = false;
|
||||
|
||||
public int Index { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using Model;
|
||||
using Model.Entity;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
public class TestCheckRecordService : BaseService<TestCheckRecordEntity>, ITestCheckRecordService
|
||||
{
|
||||
public TestCheckRecordService(SqlSugarRepository<TestCheckRecordEntity> repository) : base(repository)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序)
|
||||
/// </summary>
|
||||
public async Task<Result<List<TestCheckRecordEntity>>> GetByTestRoundIdAsync(Guid testRoundId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await _repository.Entities
|
||||
.Where(x => x.TestRoundId == testRoundId)
|
||||
.OrderBy(x => x.CreateTime)
|
||||
.ToListAsync();
|
||||
|
||||
return Result<List<TestCheckRecordEntity>>.Success(list);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<TestCheckRecordEntity>>.Error("根据 TestRoundId 查询测试项判断记录失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Model;
|
||||
using Model.Entity;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Interface
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试项判断记录服务:运行时记录测试项范围内每一次 OKExpression 判断,导出测试报告时查询。
|
||||
/// </summary>
|
||||
public interface ITestCheckRecordService : IBaseService<TestCheckRecordEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序)
|
||||
/// </summary>
|
||||
Task<Result<List<TestCheckRecordEntity>>> GetByTestRoundIdAsync(Guid testRoundId);
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ namespace TestingModule.ViewModels
|
||||
public ICommand SelectionChangedCommand { get;set; }
|
||||
public ICommand OpenSubProgramCommand { get; set; }
|
||||
public ICommand GoBackCommand { get; set; }
|
||||
public ICommand ToggleTestItemCommand { get; set; }
|
||||
#endregion
|
||||
|
||||
#region 子程序导航(主程序 / 错误程序各自独立)
|
||||
@@ -133,6 +134,7 @@ namespace TestingModule.ViewModels
|
||||
SelectionChangedCommand = new DelegateCommand<object>(SelectionChanged);
|
||||
OpenSubProgramCommand = new DelegateCommand(OpenSubProgram);
|
||||
GoBackCommand = new DelegateCommand(GoBack);
|
||||
ToggleTestItemCommand = new DelegateCommand(ToggleTestItem);
|
||||
SubscribeStepCollections();
|
||||
Program.PropertyChanged += Program_PropertyChanged;
|
||||
Admin = _globalInfo.IsAdmin;
|
||||
@@ -297,6 +299,25 @@ namespace TestingModule.ViewModels
|
||||
nav.CurrentProgram = SelectedStep.SubProgram;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换选中步骤的测试项标记(仅子程序可标记,运行时将记录其范围内所有 OKExpression 判断)
|
||||
/// </summary>
|
||||
private void ToggleTestItem()
|
||||
{
|
||||
if (!_globalInfo.IsAdmin) return;
|
||||
|
||||
var source = (SelectedItems != null && SelectedItems.Any())
|
||||
? SelectedItems
|
||||
: (SelectedStep != null ? new List<StepVM> { SelectedStep } : null);
|
||||
|
||||
if (source == null || !source.Any()) return;
|
||||
|
||||
foreach (var item in source.Where(x => x.StepType == "子程序" && x.SubProgram != null))
|
||||
{
|
||||
item.IsTestItem = !item.IsTestItem;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回上一级程序(操作当前激活的 Tab 的导航状态)
|
||||
/// </summary>
|
||||
|
||||
@@ -99,6 +99,10 @@
|
||||
<DataGridCheckBoxColumn Width="58"
|
||||
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="启用" />
|
||||
<DataGridCheckBoxColumn Width="58"
|
||||
Binding="{Binding IsTestItem}"
|
||||
Header="测试项"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn Binding="{Binding Index}"
|
||||
Header="序号"
|
||||
IsReadOnly="True" />
|
||||
@@ -142,6 +146,8 @@
|
||||
<Separator/>
|
||||
<MenuItem Header="打开子程序"
|
||||
Command="{Binding OpenSubProgramCommand}" />
|
||||
<MenuItem Header="标记/取消测试项"
|
||||
Command="{Binding ToggleTestItemCommand}" />
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
|
||||
@@ -224,6 +230,10 @@
|
||||
<DataGridCheckBoxColumn Width="58"
|
||||
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="启用" />
|
||||
<DataGridCheckBoxColumn Width="58"
|
||||
Binding="{Binding IsTestItem}"
|
||||
Header="测试项"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn Binding="{Binding Index}"
|
||||
Header="序号"
|
||||
IsReadOnly="True" />
|
||||
@@ -267,6 +277,8 @@
|
||||
<Separator/>
|
||||
<MenuItem Header="打开子程序"
|
||||
Command="{Binding OpenSubProgramCommand}" />
|
||||
<MenuItem Header="标记/取消测试项"
|
||||
Command="{Binding ToggleTestItemCommand}" />
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using UIShare.GlobalVariable;
|
||||
using static UIShare.UIViewModel.ParameterVM;
|
||||
@@ -29,6 +30,7 @@ namespace UIShare
|
||||
private IContainerProvider containerProvider;
|
||||
private IEventAggregator _eventAggregator;
|
||||
private ITestReportService _testReportService;
|
||||
private ITestCheckRecordService _testCheckRecordService;
|
||||
|
||||
private readonly Dictionary<Guid, ParameterVM> tmpParameters = [];
|
||||
|
||||
@@ -38,6 +40,9 @@ namespace UIShare
|
||||
|
||||
private readonly Stack<LoopContext> loopStack = new();
|
||||
|
||||
/// <summary>测试项上下文栈:进入 IsTestItem 子程序时压栈,栈非空期间每次 OKExpression 判断都归属栈顶测试项</summary>
|
||||
private readonly Stack<TestItemContext> testItemStack = new();
|
||||
|
||||
public CancellationTokenSource stepCTS = new();
|
||||
public CancellationTokenSource errorStepCTS = new();
|
||||
private bool SubSingleStep = false;
|
||||
@@ -46,13 +51,14 @@ namespace UIShare
|
||||
private volatile bool _disposed = false;
|
||||
|
||||
public Guid TestRoundID;
|
||||
public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService)
|
||||
public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService, ITestCheckRecordService testCheckRecordService)
|
||||
{
|
||||
_scopedContext = ScopedContext;
|
||||
_systemConfig = systemConfig;
|
||||
_eventAggregator = eventAggregator;
|
||||
_deviceManager= deviceManager;
|
||||
_testReportService = testReportService;
|
||||
_testCheckRecordService = testCheckRecordService;
|
||||
//_devices = containerProvider.Resolve<Devices>();
|
||||
}
|
||||
public async Task<bool> ExecuteErrorSteps(ProgramVM program, int depth = 0, CancellationToken cancellationToken = default)
|
||||
@@ -253,6 +259,7 @@ namespace UIShare
|
||||
loopStopwatchStack.Clear();
|
||||
ResetAllStepStatus(program.StepCollection);
|
||||
tmpParameters.Clear();
|
||||
testItemStack.Clear();
|
||||
TestRoundID = Guid.NewGuid();
|
||||
}
|
||||
int initialLoopStackCount = loopStack.Count;
|
||||
@@ -381,14 +388,24 @@ namespace UIShare
|
||||
SubProgram = step.SubProgram,
|
||||
StepName = step.Name
|
||||
});
|
||||
bool isTestItemStep = step.IsTestItem;
|
||||
if (isTestItemStep)
|
||||
{
|
||||
testItemStack.Push(new TestItemContext { Name = step.Name ?? "未命名测试项" });
|
||||
}
|
||||
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
||||
// 先评估本步骤自身(含自身 OKExpression 判断,归属本测试项),再写测试项汇总并弹栈
|
||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||
if (isTestItemStep)
|
||||
{
|
||||
await FinalizeTestItemAsync(depth);
|
||||
}
|
||||
// 发布退出子程序导航事件
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||
{
|
||||
Scope = _systemConfig.Title,
|
||||
Action = NavigateAction.Exit
|
||||
});
|
||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||
if (SubSingleStep)
|
||||
{
|
||||
SubSingleStep = false;
|
||||
@@ -812,12 +829,46 @@ namespace UIShare
|
||||
paraDic.TryAdd(item.Name, item.Value!);
|
||||
}
|
||||
}
|
||||
bool re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic);
|
||||
bool re;
|
||||
try
|
||||
{
|
||||
re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 表达式执行错误也视为 NG,并记录到测试项判断明细
|
||||
LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] OKExpression 执行异常: {ex.Message}", depth: depth);
|
||||
re = false;
|
||||
}
|
||||
step.Result = re ? 1 : 2;
|
||||
if (step.Result == 2)
|
||||
{
|
||||
LoggerHelper.WarnWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] NG:条件表达式验证失败", depth: depth);
|
||||
}
|
||||
// 测试项范围内:记录本次判断(重复判断逐次记录)
|
||||
if (testItemStack.Count > 0)
|
||||
{
|
||||
var ctx = testItemStack.Peek();
|
||||
bool isSelfCheck = ctx.Name == (step.Name ?? "未命名测试项") && step.SubProgram != null;
|
||||
if (!re) ctx.HasFailure = true;
|
||||
if (!isSelfCheck)
|
||||
{
|
||||
SaveCheckRecordAsync(new TestCheckRecordEntity
|
||||
{
|
||||
TestRoundId = TestRoundID,
|
||||
Scope = _systemConfig.Title,
|
||||
FileName = _systemConfig.CurrentADPFile ?? "",
|
||||
TestItemName = ctx.Name,
|
||||
StepName = step.Name ?? "",
|
||||
Depth = depth,
|
||||
OKExpression = step.OKExpression,
|
||||
Pass = re,
|
||||
Values = ExtractExpressionValues(step.OKExpression, paraDic),
|
||||
IsSummary = false,
|
||||
CreateTime = DateTime.Now
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -828,6 +879,90 @@ namespace UIShare
|
||||
}
|
||||
step.Result = 2;
|
||||
}
|
||||
|
||||
// 测试项范围内的步骤执行错误/表达式 NG 均计入当前测试项汇总(自身步骤的错误已在压栈期间计入)
|
||||
if (step.Result == 2 && testItemStack.Count > 0)
|
||||
{
|
||||
testItemStack.Peek().HasFailure = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试项执行结束:写入汇总记录(PASS/NG)并弹栈。
|
||||
/// 测试项内无任何判断时,结果由范围内步骤执行成败决定。
|
||||
/// </summary>
|
||||
private async Task FinalizeTestItemAsync(int depth)
|
||||
{
|
||||
if (testItemStack.Count == 0) return;
|
||||
var ctx = testItemStack.Pop();
|
||||
SaveCheckRecordAsync(new TestCheckRecordEntity
|
||||
{
|
||||
TestRoundId = TestRoundID,
|
||||
Scope = _systemConfig.Title,
|
||||
FileName = _systemConfig.CurrentADPFile ?? "",
|
||||
TestItemName = ctx.Name,
|
||||
StepName = ctx.Name,
|
||||
Depth = depth,
|
||||
OKExpression = null,
|
||||
Pass = !ctx.HasFailure,
|
||||
Values = null,
|
||||
IsSummary = true,
|
||||
CreateTime = DateTime.Now
|
||||
});
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存测试项判断记录(不阻塞执行主流程)
|
||||
/// </summary>
|
||||
private void SaveCheckRecordAsync(TestCheckRecordEntity entity)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _testCheckRecordService.InsertAsync(entity);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
LoggerHelper.Error($"保存测试项判断记录失败 [{entity.TestItemName}]: {result.Msg}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.Error($"保存测试项判断记录失败 [{entity.TestItemName}]: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从表达式中提取实际出现的变量并取其当前值,拼接为 "变量=值; " 格式(长名优先,避免子串误匹配)
|
||||
/// </summary>
|
||||
private static string? ExtractExpressionValues(string expression, Dictionary<string, object> paraDic)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var matchedSpans = new List<(int Start, int End)>();
|
||||
foreach (var name in paraDic.Keys.OrderByDescending(k => k.Length))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
foreach (Match m in Regex.Matches(expression, $@"\b{Regex.Escape(name)}\b"))
|
||||
{
|
||||
bool overlaps = matchedSpans.Any(s => m.Index < s.End && m.Index + m.Length > s.Start);
|
||||
if (!overlaps)
|
||||
{
|
||||
matchedSpans.Add((m.Index, m.Index + m.Length));
|
||||
sb.Append($"{name}={paraDic[name]}; ");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.Length > 0 ? sb.ToString() : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -861,6 +996,7 @@ namespace UIShare
|
||||
tmpParameters.Clear();
|
||||
loopStack.Clear();
|
||||
loopStopwatchStack.Clear();
|
||||
testItemStack.Clear();
|
||||
stepStopwatch.Stop();
|
||||
}
|
||||
|
||||
@@ -876,6 +1012,13 @@ namespace UIShare
|
||||
public StepVM? LoopStartStep { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>测试项执行上下文:记录测试项名称与范围内是否出现过失败</summary>
|
||||
private class TestItemContext
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool HasFailure { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace UIShare.UIViewModel
|
||||
NGGotoStepID = source.NGGotoStepID;
|
||||
Description = source.Description;
|
||||
IsUsed = source.IsUsed;
|
||||
IsTestItem = source.IsTestItem;
|
||||
|
||||
if (source.Method != null)
|
||||
{
|
||||
@@ -55,6 +56,15 @@ namespace UIShare.UIViewModel
|
||||
set => SetProperty(ref _isUsed, value);
|
||||
}
|
||||
|
||||
private bool _isTestItem = false;
|
||||
|
||||
/// <summary>是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断)</summary>
|
||||
public bool IsTestItem
|
||||
{
|
||||
get => _isTestItem;
|
||||
set => SetProperty(ref _isTestItem, value);
|
||||
}
|
||||
|
||||
private int _index;
|
||||
|
||||
public int Index
|
||||
|
||||
Reference in New Issue
Block a user