From 16dcf95b9a57e5d04c75ea6a6e9c9c008abf3fb1 Mon Sep 17 00:00:00 2001 From: "GukSang.Jin" Date: Tue, 28 Apr 2026 14:22:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- COFTester/Services/RunExportService.cs | 185 ++++++++++++------------- COFTester/ViewModels/MainViewModel.cs | 10 +- 2 files changed, 92 insertions(+), 103 deletions(-) diff --git a/COFTester/Services/RunExportService.cs b/COFTester/Services/RunExportService.cs index d36a030..79699a8 100644 --- a/COFTester/Services/RunExportService.cs +++ b/COFTester/Services/RunExportService.cs @@ -47,6 +47,7 @@ public sealed class RunExportService throw new InvalidOperationException("未找到可导出的有效历史数据。"); } + var exportStatistics = BuildExportStatistics(runs.Count, exportRuns); var outputDirectory = Path.GetDirectoryName(outputPath); if (!string.IsNullOrWhiteSpace(outputDirectory)) { @@ -56,13 +57,17 @@ public sealed class RunExportService WorksheetChartLayout chartLayout; using (var workbook = new XLWorkbook()) { - BuildSummarySheet(workbook.Worksheets.Add("报表汇总"), exportRuns); + BuildSummarySheet(workbook.Worksheets.Add("报表汇总"), exportRuns, exportStatistics); BuildRawDataSheet(workbook.Worksheets.Add("原始数据"), exportRuns); chartLayout = BuildChartSheet(workbook.Worksheets.Add("曲线图"), exportRuns); workbook.SaveAs(outputPath); } - InsertNativeCharts(outputPath, chartLayout); + if (chartLayout.Charts.Count > 0) + { + InsertNativeCharts(outputPath, chartLayout); + } + return outputPath; } @@ -96,7 +101,17 @@ public sealed class RunExportService .ToArray(); } - private static bool IsValidSample(RawSampleRecord sample) + private static ExportReportStatistics BuildExportStatistics(int sourceRunCount, IReadOnlyList exportRuns) + { + return new ExportReportStatistics( + sourceRunCount, + exportRuns.Count, + exportRuns.Count(IsChartableRun), + Math.Max(0, sourceRunCount - exportRuns.Count), + exportRuns.Sum(item => item.ValidSamples.Count)); + } + + internal static bool IsValidSample(RawSampleRecord sample) { return sample.SampleIndex > 0 && IsFinite(sample.DisplacementMm) @@ -109,7 +124,10 @@ public sealed class RunExportService return !double.IsNaN(value) && !double.IsInfinity(value); } - private static void BuildSummarySheet(IXLWorksheet worksheet, IReadOnlyList runs) + private static void BuildSummarySheet( + IXLWorksheet worksheet, + IReadOnlyList runs, + ExportReportStatistics exportStatistics) { ApplyDefaultWorksheetStyle(worksheet); @@ -122,11 +140,19 @@ public sealed class RunExportService worksheet.Cell("A3").Value = "导出时间"; worksheet.Cell("B3").Value = DateTime.Now; worksheet.Cell("B3").Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss"; - worksheet.Cell("A4").Value = "有效历史数量"; - worksheet.Cell("B4").Value = runs.Count; - worksheet.Cell("A5").Value = "汇总说明"; - worksheet.Cell("B5").Value = "本表仅统计含有效采样点的试验记录,包含结果汇总、标准差统计和原始力值统计。"; - worksheet.Range("B5:F5").Merge(); + worksheet.Cell("A4").Value = "历史记录总数"; + worksheet.Cell("B4").Value = exportStatistics.SourceRunCount; + worksheet.Cell("A5").Value = "有效导出记录数"; + worksheet.Cell("B5").Value = exportStatistics.ExportRunCount; + worksheet.Cell("A6").Value = "曲线图数量"; + worksheet.Cell("B6").Value = exportStatistics.ChartRunCount; + worksheet.Cell("A7").Value = "排除记录数"; + worksheet.Cell("B7").Value = exportStatistics.ExcludedRunCount; + worksheet.Cell("A8").Value = "有效采样点总数"; + worksheet.Cell("B8").Value = exportStatistics.ValidSampleCount; + worksheet.Cell("A9").Value = "汇总说明"; + worksheet.Cell("B9").Value = "本表仅统计含有效采样点的试验记录;单点记录保留数据但不生成曲线图。"; + worksheet.Range("B9:F9").Merge(); BuildStatisticsPanel(worksheet, runs); @@ -309,14 +335,14 @@ public sealed class RunExportService worksheet.Cell("A1").Style.Font.FontColor = XLColor.FromHtml("#21313D"); worksheet.Cell("A3").Value = "图表说明"; - worksheet.Cell("B3").Value = "包含历史总览曲线、静动摩擦趋势图,以及每轮有效数据的单独曲线图。"; + worksheet.Cell("B3").Value = "每条含至少 2 个有效采样点的历史试验记录对应一个独立曲线图,横轴按真实位移数值比例绘制。"; worksheet.Range("B3:H3").Merge(); worksheet.Cell("A5").Value = "曲线清单"; worksheet.Cell("A5").Style.Font.Bold = true; worksheet.Cell("A5").Style.Font.FontSize = 12; - WriteHeaderRow(worksheet, 6, ["曲线标识", "图例标签", "颜色", "有效采样点数"]); + WriteHeaderRow(worksheet, 6, ["曲线标识", "图例标签", "颜色", "有效采样点数", "曲线状态"]); for (var index = 0; index < runs.Count; index++) { @@ -325,13 +351,16 @@ public sealed class RunExportService worksheet.Cell(row, 2).Value = BuildLegendLabel(runs[index]); worksheet.Cell(row, 3).Value = runs[index].CurveColorHex; worksheet.Cell(row, 4).Value = runs[index].ValidSamples.Count; + worksheet.Cell(row, 5).Value = IsChartableRun(runs[index]) + ? "已绘制" + : "采样点不足,未绘制曲线"; worksheet.Cell(row, 3).Style.Fill.BackgroundColor = ToXlColor(runs[index].CurveColorHex); worksheet.Cell(row, 3).Style.Font.FontColor = XLColor.White; } ApplyChartColumnWidths(worksheet); var chartLayout = BuildChartDataArea(worksheet, runs); - ApplyPageLayout(worksheet, Math.Max(40, 25 + ((runs.Count + 1) / 2) * 14), "$A$1:$T$"); + ApplyPageLayout(worksheet, CalculateChartSheetLastRow(runs), "$A$1:$T$"); worksheet.SheetView.FreezeRows(1); return chartLayout; } @@ -371,8 +400,9 @@ public sealed class RunExportService { var row = 4 + index; worksheet.Cell(row, 8).Value = rows[index].Label; - worksheet.Cell(row, 8).Style.Fill.BackgroundColor = XLColor.FromHtml("#EEF3F7"); + worksheet.Cell(row, 8).Style.Fill.BackgroundColor = XLColor.FromHtml("#2F4A5C"); worksheet.Cell(row, 8).Style.Font.Bold = true; + worksheet.Cell(row, 8).Style.Font.FontColor = XLColor.White; worksheet.Cell(row, 9).Value = rows[index].Value; worksheet.Cell(row, 9).Style.Border.BottomBorder = XLBorderStyleValues.Thin; worksheet.Cell(row, 9).Style.Border.BottomBorderColor = XLColor.FromHtml("#D3DDE5"); @@ -384,7 +414,6 @@ public sealed class RunExportService const int startColumn = 30; const int startRow = 1; - var seriesLayouts = new List(); var perRunSeries = new List(); var currentColumn = startColumn; var sheetName = worksheet.Name; @@ -420,74 +449,14 @@ public sealed class RunExportService BuildRangeReference(sheetName, startDataRow, yColumn, endDataRow, yColumn), item.CurveColorHex); - seriesLayouts.Add(layout); - perRunSeries.Add(new PerRunSeriesLayout(item, layout)); + if (IsChartableRun(item)) + { + perRunSeries.Add(new PerRunSeriesLayout(item, layout)); + } } - var trendHeaderRow = startRow; - var trendDataStartRow = startRow + 1; - var runIndexColumn = currentColumn; - var staticColumn = runIndexColumn + 1; - var kineticColumn = runIndexColumn + 2; - - worksheet.Cell(trendHeaderRow, runIndexColumn).Value = "试验轮次"; - worksheet.Cell(trendHeaderRow, staticColumn).Value = "静摩擦系数 μs"; - worksheet.Cell(trendHeaderRow, kineticColumn).Value = "动摩擦系数 μk"; - - for (var index = 0; index < runs.Count; index++) - { - var row = trendDataStartRow + index; - worksheet.Cell(row, runIndexColumn).Value = runs[index].Data.Run.RunIndex; - worksheet.Cell(row, staticColumn).Value = runs[index].Data.Run.StaticCoefficient; - worksheet.Cell(row, kineticColumn).Value = runs[index].Data.Run.KineticCoefficient; - } - - worksheet.Column(runIndexColumn).Hide(); - worksheet.Column(staticColumn).Hide(); - worksheet.Column(kineticColumn).Hide(); - - var trendEndRow = Math.Max(trendDataStartRow, trendDataStartRow + runs.Count - 1); - var charts = new List - { - new( - "历史曲线总览", - "位移 (mm)", - "力值 (N)", - true, - seriesLayouts, - new ChartAnchor(0, 20, 11, 36), - "历史曲线总览"), - new( - "静摩擦系数趋势", - "试验轮次", - "μs", - true, - [ - new SeriesLayout( - BuildCellReference(sheetName, trendHeaderRow, staticColumn), - BuildRangeReference(sheetName, trendDataStartRow, runIndexColumn, trendEndRow, runIndexColumn), - BuildRangeReference(sheetName, trendDataStartRow, staticColumn, trendEndRow, staticColumn), - "#5E7DA0") - ], - new ChartAnchor(12, 20, 19, 28), - "静摩擦趋势图"), - new( - "动摩擦系数趋势", - "试验轮次", - "μk", - true, - [ - new SeriesLayout( - BuildCellReference(sheetName, trendHeaderRow, kineticColumn), - BuildRangeReference(sheetName, trendDataStartRow, runIndexColumn, trendEndRow, runIndexColumn), - BuildRangeReference(sheetName, trendDataStartRow, kineticColumn, trendEndRow, kineticColumn), - "#00839A") - ], - new ChartAnchor(12, 29, 19, 37), - "动摩擦趋势图") - }; - - const int perRunStartRow = 39; + var charts = new List(); + var perRunStartRow = Math.Max(10, runs.Count + 8); const int chartHeight = 11; const int chartWidth = 9; const int rowGap = 2; @@ -515,6 +484,17 @@ public sealed class RunExportService return new WorksheetChartLayout(worksheet.Name, charts); } + private static bool IsChartableRun(ExportCurveData item) + { + return item.ValidSamples.Count >= 2; + } + + private static int CalculateChartSheetLastRow(IReadOnlyList runs) + { + var chartCount = runs.Count(IsChartableRun); + return Math.Max(24, runs.Count + 22 + ((chartCount + 1) / 2) * 13); + } + private static void ApplyDefaultWorksheetStyle(IXLWorksheet worksheet) { worksheet.Style.Font.FontName = "Microsoft YaHei UI"; @@ -530,9 +510,10 @@ public sealed class RunExportService var cell = worksheet.Cell(row, index + 1); cell.Value = headers[index]; cell.Style.Font.Bold = true; - cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#E1EAF1"); + cell.Style.Font.FontColor = XLColor.White; + cell.Style.Fill.BackgroundColor = XLColor.FromHtml("#2F4A5C"); cell.Style.Border.BottomBorder = XLBorderStyleValues.Thin; - cell.Style.Border.BottomBorderColor = XLColor.FromHtml("#B7C4CE"); + cell.Style.Border.BottomBorderColor = XLColor.FromHtml("#1F3342"); } } @@ -595,6 +576,7 @@ public sealed class RunExportService 2 => 28, 3 => 12, 4 => 12, + 5 => 22, _ => 14 }; } @@ -681,7 +663,7 @@ public sealed class RunExportService private static void BuildChartPart(ChartPart chartPart, ChartDefinition chartDefinition) { - const uint categoryAxisId = 48650112U; + const uint xAxisId = 48650112U; const uint valueAxisId = 48672768U; var chartSpace = new C.ChartSpace(); @@ -694,8 +676,8 @@ public sealed class RunExportService var plotArea = new C.PlotArea(); plotArea.Append(new C.Layout()); - var lineChart = new C.LineChart( - new C.Grouping { Val = C.GroupingValues.Standard }, + var scatterChart = new C.ScatterChart( + new C.ScatterStyle { Val = C.ScatterStyleValues.Line }, new C.VaryColors { Val = false }); for (var index = 0; index < chartDefinition.Series.Count; index++) @@ -710,25 +692,25 @@ public sealed class RunExportService Width = 19050 }); - var lineSeries = new C.LineChartSeries( + var scatterSeries = new C.ScatterChartSeries( new C.Index { Val = (uint)index }, new C.Order { Val = (uint)index }, new C.SeriesText(new C.StringReference(new C.Formula(series.TitleReference))), chartShape, new C.Marker(new C.Symbol { Val = C.MarkerStyleValues.None }), - new C.CategoryAxisData(new C.NumberReference(new C.Formula(series.XValuesReference))), - new C.Values(new C.NumberReference(new C.Formula(series.YValuesReference))), + new C.XValues(new C.NumberReference(new C.Formula(series.XValuesReference))), + new C.YValues(new C.NumberReference(new C.Formula(series.YValuesReference))), new C.Smooth { Val = false }); - lineChart.Append(lineSeries); + scatterChart.Append(scatterSeries); } - lineChart.Append(new C.AxisId { Val = categoryAxisId }); - lineChart.Append(new C.AxisId { Val = valueAxisId }); - plotArea.Append(lineChart); + scatterChart.Append(new C.AxisId { Val = xAxisId }); + scatterChart.Append(new C.AxisId { Val = valueAxisId }); + plotArea.Append(scatterChart); - var categoryAxis = new C.CategoryAxis( - new C.AxisId { Val = categoryAxisId }, + var xAxis = new C.ValueAxis( + new C.AxisId { Val = xAxisId }, new C.Scaling(new C.Orientation { Val = C.OrientationValues.MinMax }), new C.Delete { Val = false }, new C.AxisPosition { Val = C.AxisPositionValues.Bottom }, @@ -737,9 +719,7 @@ public sealed class RunExportService new C.TickLabelPosition { Val = C.TickLabelPositionValues.NextTo }, new C.CrossingAxis { Val = valueAxisId }, new C.Crosses { Val = C.CrossesValues.AutoZero }, - new C.AutoLabeled { Val = true }, - new C.LabelAlignment { Val = C.LabelAlignmentValues.Center }, - new C.LabelOffset { Val = 100 }); + new C.CrossBetween { Val = C.CrossBetweenValues.Between }); var valueAxis = new C.ValueAxis( new C.AxisId { Val = valueAxisId }, @@ -750,11 +730,11 @@ public sealed class RunExportService new C.NumberingFormat { FormatCode = "0.000", SourceLinked = true }, CreateAxisTitle(chartDefinition.YAxisTitle), new C.TickLabelPosition { Val = C.TickLabelPositionValues.NextTo }, - new C.CrossingAxis { Val = categoryAxisId }, + new C.CrossingAxis { Val = xAxisId }, new C.Crosses { Val = C.CrossesValues.AutoZero }, new C.CrossBetween { Val = C.CrossBetweenValues.Between }); - plotArea.Append(categoryAxis); + plotArea.Append(xAxis); plotArea.Append(valueAxis); chart.Append(plotArea); @@ -965,6 +945,13 @@ public sealed class RunExportService private sealed record ExportCurveData(PersistedRunData Data, IReadOnlyList ValidSamples, int Sequence, string CurveColorHex); + private sealed record ExportReportStatistics( + int SourceRunCount, + int ExportRunCount, + int ChartRunCount, + int ExcludedRunCount, + int ValidSampleCount); + private sealed record SeriesLayout(string TitleReference, string XValuesReference, string YValuesReference, string ColorHex); private sealed record PerRunSeriesLayout(ExportCurveData Data, SeriesLayout Series); diff --git a/COFTester/ViewModels/MainViewModel.cs b/COFTester/ViewModels/MainViewModel.cs index a3a0fbb..449a8c5 100644 --- a/COFTester/ViewModels/MainViewModel.cs +++ b/COFTester/ViewModels/MainViewModel.cs @@ -1663,19 +1663,21 @@ public sealed class MainViewModel : ObservableObject, IDisposable private bool TryResolveHistoryExportData(out IReadOnlyList exportData) { var historyData = new List(); + var hasExportableSamples = false; foreach (var run in RunHistory.OrderBy(item => item.CompletedAt)) { var data = LoadRunData(run); - if (data is not null && data.Samples.Count > 0) + if (data is not null) { historyData.Add(data); + hasExportableSamples |= data.Samples.Any(RunExportService.IsValidSample); } } exportData = historyData; - if (historyData.Count == 0) + if (!hasExportableSamples) { - AddWarningEvent("未找到可导出的历史采样数据。"); + AddWarningEvent("未找到可导出的有效历史采样数据。"); return false; } @@ -1710,7 +1712,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable } var workbookPath = _runExportService.ExportHistoricalComparisonReport(exportData, saveFileDialog.FileName); - foreach (var item in exportData) + foreach (var item in exportData.Where(data => data.Samples.Any(RunExportService.IsValidSample))) { _dataRepository.UpdateExportPaths(item.Run.RunId, null, workbookPath); item.Run.ReportExportPath = workbookPath;