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 _testReportModelList = new(); public ObservableCollection 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 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(); _testCheckRecordService = containerProvider.Resolve(); LoadedCommand = new AsyncDelegateCommand(OnLoad); QueryCommand = new AsyncDelegateCommand(OnQuery); ExportCommand = new AsyncDelegateCommand(OnExport); ExportReportCommand = new AsyncDelegateCommand(OnExportReportCommand); } #region 命令处理 /// /// 加载全部(无筛选条件) /// private async Task OnLoad() { var result = await _testReportService.GetFilterList(); if (result.IsSuccess) { TestReportModelList = new ObservableCollection(result.Data ?? new List()); TotalCount = TestReportModelList.Count; ShowInfoMessageBox($"加载完成,共 {TotalCount} 条记录", () => { }); } else { TestReportModelList = new ObservableCollection(); TotalCount = 0; ShowErrorMessageBox($"加载失败:{result.Msg}", () => { }); LoggerHelper.Error(result.Msg); } } /// /// 查询(根据开始日期、结束日期、台架名称筛选) /// 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(result.Data ?? new List()); TotalCount = TestReportModelList.Count; ShowInfoMessageBox($"查询完成,共 {TotalCount} 条记录", () => { }); } else { TestReportModelList = new ObservableCollection(); TotalCount = 0; ShowErrorMessageBox($"查询失败:{result.Msg}", () => { }); LoggerHelper.Error(result.Msg); } } /// /// 导出 Excel:根据用户在表格中选中的 TestReportModel.TestRoundId 查询并导出完整步骤数据 /// 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}", () => { }); } /// /// 导出测试报告:上层为各测试项(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 辅助方法 private static void ExportToExcel(string filePath, List 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); } /// /// 测试报告导出:上层为各测试项(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 生命周期 public override void OnNavigatedTo(NavigationContext navigationContext) { } public void Dispose() { } #endregion } }