Files
ACP/ExportModule/ViewModels/ExportViewModel.cs
T
2026-08-30 15:57:28 +08:00

429 lines
18 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ClosedXML.Excel;
using Logger;
using Microsoft.Win32;
using Model;
using Model.Entity;
using Model.Models;
using Service.Interface;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using UIShare.ViewModelBase;
namespace ExportModule.ViewModels
{
public class ExportViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region 属性
public bool KeepAlive => true;
private ObservableCollection<TestReportModel> _testReportModelList = new();
public ObservableCollection<TestReportModel> TestReportModelList
{
get { return _testReportModelList; }
set { SetProperty(ref _testReportModelList, value); }
}
private DateTime? _startTime;
public DateTime? StartTime
{
get => _startTime;
set => SetProperty(ref _startTime, value);
}
private DateTime? _endTime;
public DateTime? EndTime
{
get => _endTime;
set => SetProperty(ref _endTime, value);
}
private string _selectedScope = "全部";
public string SelectedScope
{
get => _selectedScope;
set => SetProperty(ref _selectedScope, value);
}
public ObservableCollection<string> ScopeOptions { get; } = new()
{
"全部",
"TestCell1", "TestCell2", "TestCell3", "TestCell4",
"TestCell5", "TestCell6", "TestCell7", "TestCell8"
};
private string _statusMessage = "就绪";
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
private int _totalCount;
public int TotalCount
{
get => _totalCount;
set => SetProperty(ref _totalCount, value);
}
private TestReportModel? _selectedTestReport;
public TestReportModel? SelectedTestReport
{
get => _selectedTestReport;
set => SetProperty(ref _selectedTestReport, value);
}
#endregion
#region 命令
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 命令处理
/// <summary>
/// 加载全部(无筛选条件)
/// </summary>
private async Task OnLoad()
{
var result = await _testReportService.GetFilterList();
if (result.IsSuccess)
{
TestReportModelList = new ObservableCollection<TestReportModel>(result.Data ?? new List<TestReportModel>());
TotalCount = TestReportModelList.Count;
ShowInfoMessageBox($"加载完成,共 {TotalCount} 条记录", () => { });
}
else
{
TestReportModelList = new ObservableCollection<TestReportModel>();
TotalCount = 0;
ShowErrorMessageBox($"加载失败:{result.Msg}", () => { });
LoggerHelper.Error(result.Msg);
}
}
/// <summary>
/// 查询(根据开始日期、结束日期、台架名称筛选)
/// </summary>
private async Task OnQuery()
{
// 处理结束日期:若用户只选了日期没选时间,默认到当天末尾
DateTime? queryEnd = EndTime;
if (queryEnd.HasValue)
{
queryEnd = queryEnd.Value.Date.AddDays(1).AddSeconds(-1);
}
string? scope = SelectedScope == "全部" ? null : SelectedScope;
var result = await _testReportService.GetFilterList(StartTime, queryEnd, scope);
if (result.IsSuccess)
{
TestReportModelList = new ObservableCollection<TestReportModel>(result.Data ?? new List<TestReportModel>());
TotalCount = TestReportModelList.Count;
ShowInfoMessageBox($"查询完成,共 {TotalCount} 条记录", () => { });
}
else
{
TestReportModelList = new ObservableCollection<TestReportModel>();
TotalCount = 0;
ShowErrorMessageBox($"查询失败:{result.Msg}", () => { });
LoggerHelper.Error(result.Msg);
}
}
/// <summary>
/// 导出 Excel:根据用户在表格中选中的 TestReportModel.TestRoundId 查询并导出完整步骤数据
/// </summary>
private async Task OnExport()
{
if (SelectedTestReport == null)
{
ShowErrorMessageBox("请先在列表中选择一条测试记录。", () => { });
return;
}
StatusMessage = "正在查询导出数据...";
var entityResult = await _testReportService.GetByTestRoundIdAsync(SelectedTestReport.TestRoundId);
if (!entityResult.IsSuccess || entityResult.Data == null || entityResult.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 entities = entityResult.Data;
StatusMessage = $"正在导出 {entities.Count} 条步骤记录...";
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
#region 辅助方法
private static void ExportToExcel(string filePath, List<TestReportEntity> entities)
{
using var workbook = new XLWorkbook();
var ws = workbook.Worksheets.Add("测试报告");
// 表头
string[] headers = {
"Id", "CreateTime", "TestRoundId", "Scope", "FileName",
"StepIndex", "StepName", "StepType", "MethodName", "MethodFullName",
"Result", "RunTimeMs", "OutputValue", "Depth", "IsErrorStep", "LoopRemaining"
};
for (int c = 0; c < headers.Length; c++)
{
ws.Cell(1, c + 1).Value = headers[c];
}
// 标题行样式
var headerRange = ws.Range(1, 1, 1, headers.Length);
headerRange.Style.Font.Bold = true;
headerRange.Style.Fill.BackgroundColor = XLColor.FromArgb(0xECEFF4);
headerRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
headerRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
// 填充数据
for (int i = 0; i < entities.Count; i++)
{
var e = entities[i];
int row = i + 2;
ws.Cell(row, 1).Value = e.Id;
ws.Cell(row, 2).Value = e.CreateTime.ToString("yyyy-MM-dd HH:mm:ss");
ws.Cell(row, 3).Value = e.TestRoundId.ToString();
ws.Cell(row, 4).Value = e.Scope ?? "";
ws.Cell(row, 5).Value = e.FileName ?? "";
ws.Cell(row, 6).Value = e.StepIndex;
ws.Cell(row, 7).Value = e.StepName ?? "";
ws.Cell(row, 8).Value = e.StepType ?? "";
ws.Cell(row, 9).Value = e.MethodName ?? "";
ws.Cell(row, 10).Value = e.MethodFullName ?? "";
ws.Cell(row, 11).Value = e.Result;
ws.Cell(row, 12).Value = e.RunTimeMs;
ws.Cell(row, 13).Value = e.OutputValue ?? "";
ws.Cell(row, 14).Value = e.Depth;
ws.Cell(row, 15).Value = e.IsErrorStep ? "是" : "否";
ws.Cell(row, 16).Value = e.LoopRemaining;
// 数据行边框
var dataRange = ws.Range(row, 1, row, headers.Length);
dataRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
dataRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
}
ws.Columns().AdjustToContents();
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 生命周期
public override void OnNavigatedTo(NavigationContext navigationContext)
{
}
public void Dispose()
{
}
#endregion
}
}