This commit is contained in:
GukSang.Jin
2026-04-28 14:22:31 +08:00
parent 7c0d894907
commit 16dcf95b9a
2 changed files with 92 additions and 103 deletions

View File

@@ -47,6 +47,7 @@ public sealed class RunExportService
throw new InvalidOperationException("未找到可导出的有效历史数据。"); throw new InvalidOperationException("未找到可导出的有效历史数据。");
} }
var exportStatistics = BuildExportStatistics(runs.Count, exportRuns);
var outputDirectory = Path.GetDirectoryName(outputPath); var outputDirectory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrWhiteSpace(outputDirectory)) if (!string.IsNullOrWhiteSpace(outputDirectory))
{ {
@@ -56,13 +57,17 @@ public sealed class RunExportService
WorksheetChartLayout chartLayout; WorksheetChartLayout chartLayout;
using (var workbook = new XLWorkbook()) using (var workbook = new XLWorkbook())
{ {
BuildSummarySheet(workbook.Worksheets.Add("报表汇总"), exportRuns); BuildSummarySheet(workbook.Worksheets.Add("报表汇总"), exportRuns, exportStatistics);
BuildRawDataSheet(workbook.Worksheets.Add("原始数据"), exportRuns); BuildRawDataSheet(workbook.Worksheets.Add("原始数据"), exportRuns);
chartLayout = BuildChartSheet(workbook.Worksheets.Add("曲线图"), exportRuns); chartLayout = BuildChartSheet(workbook.Worksheets.Add("曲线图"), exportRuns);
workbook.SaveAs(outputPath); workbook.SaveAs(outputPath);
} }
if (chartLayout.Charts.Count > 0)
{
InsertNativeCharts(outputPath, chartLayout); InsertNativeCharts(outputPath, chartLayout);
}
return outputPath; return outputPath;
} }
@@ -96,7 +101,17 @@ public sealed class RunExportService
.ToArray(); .ToArray();
} }
private static bool IsValidSample(RawSampleRecord sample) private static ExportReportStatistics BuildExportStatistics(int sourceRunCount, IReadOnlyList<ExportCurveData> 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 return sample.SampleIndex > 0
&& IsFinite(sample.DisplacementMm) && IsFinite(sample.DisplacementMm)
@@ -109,7 +124,10 @@ public sealed class RunExportService
return !double.IsNaN(value) && !double.IsInfinity(value); return !double.IsNaN(value) && !double.IsInfinity(value);
} }
private static void BuildSummarySheet(IXLWorksheet worksheet, IReadOnlyList<ExportCurveData> runs) private static void BuildSummarySheet(
IXLWorksheet worksheet,
IReadOnlyList<ExportCurveData> runs,
ExportReportStatistics exportStatistics)
{ {
ApplyDefaultWorksheetStyle(worksheet); ApplyDefaultWorksheetStyle(worksheet);
@@ -122,11 +140,19 @@ public sealed class RunExportService
worksheet.Cell("A3").Value = "导出时间"; worksheet.Cell("A3").Value = "导出时间";
worksheet.Cell("B3").Value = DateTime.Now; worksheet.Cell("B3").Value = DateTime.Now;
worksheet.Cell("B3").Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss"; worksheet.Cell("B3").Style.DateFormat.Format = "yyyy-mm-dd hh:mm:ss";
worksheet.Cell("A4").Value = "有效历史数量"; worksheet.Cell("A4").Value = "历史记录总数";
worksheet.Cell("B4").Value = runs.Count; worksheet.Cell("B4").Value = exportStatistics.SourceRunCount;
worksheet.Cell("A5").Value = "汇总说明"; worksheet.Cell("A5").Value = "有效导出记录数";
worksheet.Cell("B5").Value = "本表仅统计含有效采样点的试验记录,包含结果汇总、标准差统计和原始力值统计。"; worksheet.Cell("B5").Value = exportStatistics.ExportRunCount;
worksheet.Range("B5:F5").Merge(); 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); BuildStatisticsPanel(worksheet, runs);
@@ -309,14 +335,14 @@ public sealed class RunExportService
worksheet.Cell("A1").Style.Font.FontColor = XLColor.FromHtml("#21313D"); worksheet.Cell("A1").Style.Font.FontColor = XLColor.FromHtml("#21313D");
worksheet.Cell("A3").Value = "图表说明"; worksheet.Cell("A3").Value = "图表说明";
worksheet.Cell("B3").Value = "包含历史总览曲线、静动摩擦趋势图,以及每轮有效数据的单独曲线图。"; worksheet.Cell("B3").Value = "每条含至少 2 个有效采样点的历史试验记录对应一个独立曲线图,横轴按真实位移数值比例绘制。";
worksheet.Range("B3:H3").Merge(); worksheet.Range("B3:H3").Merge();
worksheet.Cell("A5").Value = "曲线清单"; worksheet.Cell("A5").Value = "曲线清单";
worksheet.Cell("A5").Style.Font.Bold = true; worksheet.Cell("A5").Style.Font.Bold = true;
worksheet.Cell("A5").Style.Font.FontSize = 12; worksheet.Cell("A5").Style.Font.FontSize = 12;
WriteHeaderRow(worksheet, 6, ["曲线标识", "图例标签", "颜色", "有效采样点数"]); WriteHeaderRow(worksheet, 6, ["曲线标识", "图例标签", "颜色", "有效采样点数", "曲线状态"]);
for (var index = 0; index < runs.Count; index++) 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, 2).Value = BuildLegendLabel(runs[index]);
worksheet.Cell(row, 3).Value = runs[index].CurveColorHex; worksheet.Cell(row, 3).Value = runs[index].CurveColorHex;
worksheet.Cell(row, 4).Value = runs[index].ValidSamples.Count; 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.Fill.BackgroundColor = ToXlColor(runs[index].CurveColorHex);
worksheet.Cell(row, 3).Style.Font.FontColor = XLColor.White; worksheet.Cell(row, 3).Style.Font.FontColor = XLColor.White;
} }
ApplyChartColumnWidths(worksheet); ApplyChartColumnWidths(worksheet);
var chartLayout = BuildChartDataArea(worksheet, runs); 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); worksheet.SheetView.FreezeRows(1);
return chartLayout; return chartLayout;
} }
@@ -371,8 +400,9 @@ public sealed class RunExportService
{ {
var row = 4 + index; var row = 4 + index;
worksheet.Cell(row, 8).Value = rows[index].Label; 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.Bold = true;
worksheet.Cell(row, 8).Style.Font.FontColor = XLColor.White;
worksheet.Cell(row, 9).Value = rows[index].Value; worksheet.Cell(row, 9).Value = rows[index].Value;
worksheet.Cell(row, 9).Style.Border.BottomBorder = XLBorderStyleValues.Thin; worksheet.Cell(row, 9).Style.Border.BottomBorder = XLBorderStyleValues.Thin;
worksheet.Cell(row, 9).Style.Border.BottomBorderColor = XLColor.FromHtml("#D3DDE5"); worksheet.Cell(row, 9).Style.Border.BottomBorderColor = XLColor.FromHtml("#D3DDE5");
@@ -384,7 +414,6 @@ public sealed class RunExportService
const int startColumn = 30; const int startColumn = 30;
const int startRow = 1; const int startRow = 1;
var seriesLayouts = new List<SeriesLayout>();
var perRunSeries = new List<PerRunSeriesLayout>(); var perRunSeries = new List<PerRunSeriesLayout>();
var currentColumn = startColumn; var currentColumn = startColumn;
var sheetName = worksheet.Name; var sheetName = worksheet.Name;
@@ -420,74 +449,14 @@ public sealed class RunExportService
BuildRangeReference(sheetName, startDataRow, yColumn, endDataRow, yColumn), BuildRangeReference(sheetName, startDataRow, yColumn, endDataRow, yColumn),
item.CurveColorHex); item.CurveColorHex);
seriesLayouts.Add(layout); if (IsChartableRun(item))
{
perRunSeries.Add(new PerRunSeriesLayout(item, layout)); 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(); var charts = new List<ChartDefinition>();
worksheet.Column(staticColumn).Hide(); var perRunStartRow = Math.Max(10, runs.Count + 8);
worksheet.Column(kineticColumn).Hide();
var trendEndRow = Math.Max(trendDataStartRow, trendDataStartRow + runs.Count - 1);
var charts = new List<ChartDefinition>
{
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;
const int chartHeight = 11; const int chartHeight = 11;
const int chartWidth = 9; const int chartWidth = 9;
const int rowGap = 2; const int rowGap = 2;
@@ -515,6 +484,17 @@ public sealed class RunExportService
return new WorksheetChartLayout(worksheet.Name, charts); return new WorksheetChartLayout(worksheet.Name, charts);
} }
private static bool IsChartableRun(ExportCurveData item)
{
return item.ValidSamples.Count >= 2;
}
private static int CalculateChartSheetLastRow(IReadOnlyList<ExportCurveData> runs)
{
var chartCount = runs.Count(IsChartableRun);
return Math.Max(24, runs.Count + 22 + ((chartCount + 1) / 2) * 13);
}
private static void ApplyDefaultWorksheetStyle(IXLWorksheet worksheet) private static void ApplyDefaultWorksheetStyle(IXLWorksheet worksheet)
{ {
worksheet.Style.Font.FontName = "Microsoft YaHei UI"; worksheet.Style.Font.FontName = "Microsoft YaHei UI";
@@ -530,9 +510,10 @@ public sealed class RunExportService
var cell = worksheet.Cell(row, index + 1); var cell = worksheet.Cell(row, index + 1);
cell.Value = headers[index]; cell.Value = headers[index];
cell.Style.Font.Bold = true; 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.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, 2 => 28,
3 => 12, 3 => 12,
4 => 12, 4 => 12,
5 => 22,
_ => 14 _ => 14
}; };
} }
@@ -681,7 +663,7 @@ public sealed class RunExportService
private static void BuildChartPart(ChartPart chartPart, ChartDefinition chartDefinition) private static void BuildChartPart(ChartPart chartPart, ChartDefinition chartDefinition)
{ {
const uint categoryAxisId = 48650112U; const uint xAxisId = 48650112U;
const uint valueAxisId = 48672768U; const uint valueAxisId = 48672768U;
var chartSpace = new C.ChartSpace(); var chartSpace = new C.ChartSpace();
@@ -694,8 +676,8 @@ public sealed class RunExportService
var plotArea = new C.PlotArea(); var plotArea = new C.PlotArea();
plotArea.Append(new C.Layout()); plotArea.Append(new C.Layout());
var lineChart = new C.LineChart( var scatterChart = new C.ScatterChart(
new C.Grouping { Val = C.GroupingValues.Standard }, new C.ScatterStyle { Val = C.ScatterStyleValues.Line },
new C.VaryColors { Val = false }); new C.VaryColors { Val = false });
for (var index = 0; index < chartDefinition.Series.Count; index++) for (var index = 0; index < chartDefinition.Series.Count; index++)
@@ -710,25 +692,25 @@ public sealed class RunExportService
Width = 19050 Width = 19050
}); });
var lineSeries = new C.LineChartSeries( var scatterSeries = new C.ScatterChartSeries(
new C.Index { Val = (uint)index }, new C.Index { Val = (uint)index },
new C.Order { Val = (uint)index }, new C.Order { Val = (uint)index },
new C.SeriesText(new C.StringReference(new C.Formula(series.TitleReference))), new C.SeriesText(new C.StringReference(new C.Formula(series.TitleReference))),
chartShape, chartShape,
new C.Marker(new C.Symbol { Val = C.MarkerStyleValues.None }), new C.Marker(new C.Symbol { Val = C.MarkerStyleValues.None }),
new C.CategoryAxisData(new C.NumberReference(new C.Formula(series.XValuesReference))), new C.XValues(new C.NumberReference(new C.Formula(series.XValuesReference))),
new C.Values(new C.NumberReference(new C.Formula(series.YValuesReference))), new C.YValues(new C.NumberReference(new C.Formula(series.YValuesReference))),
new C.Smooth { Val = false }); new C.Smooth { Val = false });
lineChart.Append(lineSeries); scatterChart.Append(scatterSeries);
} }
lineChart.Append(new C.AxisId { Val = categoryAxisId }); scatterChart.Append(new C.AxisId { Val = xAxisId });
lineChart.Append(new C.AxisId { Val = valueAxisId }); scatterChart.Append(new C.AxisId { Val = valueAxisId });
plotArea.Append(lineChart); plotArea.Append(scatterChart);
var categoryAxis = new C.CategoryAxis( var xAxis = new C.ValueAxis(
new C.AxisId { Val = categoryAxisId }, new C.AxisId { Val = xAxisId },
new C.Scaling(new C.Orientation { Val = C.OrientationValues.MinMax }), new C.Scaling(new C.Orientation { Val = C.OrientationValues.MinMax }),
new C.Delete { Val = false }, new C.Delete { Val = false },
new C.AxisPosition { Val = C.AxisPositionValues.Bottom }, new C.AxisPosition { Val = C.AxisPositionValues.Bottom },
@@ -737,9 +719,7 @@ public sealed class RunExportService
new C.TickLabelPosition { Val = C.TickLabelPositionValues.NextTo }, new C.TickLabelPosition { Val = C.TickLabelPositionValues.NextTo },
new C.CrossingAxis { Val = valueAxisId }, new C.CrossingAxis { Val = valueAxisId },
new C.Crosses { Val = C.CrossesValues.AutoZero }, new C.Crosses { Val = C.CrossesValues.AutoZero },
new C.AutoLabeled { Val = true }, new C.CrossBetween { Val = C.CrossBetweenValues.Between });
new C.LabelAlignment { Val = C.LabelAlignmentValues.Center },
new C.LabelOffset { Val = 100 });
var valueAxis = new C.ValueAxis( var valueAxis = new C.ValueAxis(
new C.AxisId { Val = valueAxisId }, new C.AxisId { Val = valueAxisId },
@@ -750,11 +730,11 @@ public sealed class RunExportService
new C.NumberingFormat { FormatCode = "0.000", SourceLinked = true }, new C.NumberingFormat { FormatCode = "0.000", SourceLinked = true },
CreateAxisTitle(chartDefinition.YAxisTitle), CreateAxisTitle(chartDefinition.YAxisTitle),
new C.TickLabelPosition { Val = C.TickLabelPositionValues.NextTo }, 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.Crosses { Val = C.CrossesValues.AutoZero },
new C.CrossBetween { Val = C.CrossBetweenValues.Between }); new C.CrossBetween { Val = C.CrossBetweenValues.Between });
plotArea.Append(categoryAxis); plotArea.Append(xAxis);
plotArea.Append(valueAxis); plotArea.Append(valueAxis);
chart.Append(plotArea); chart.Append(plotArea);
@@ -965,6 +945,13 @@ public sealed class RunExportService
private sealed record ExportCurveData(PersistedRunData Data, IReadOnlyList<RawSampleRecord> ValidSamples, int Sequence, string CurveColorHex); private sealed record ExportCurveData(PersistedRunData Data, IReadOnlyList<RawSampleRecord> 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 SeriesLayout(string TitleReference, string XValuesReference, string YValuesReference, string ColorHex);
private sealed record PerRunSeriesLayout(ExportCurveData Data, SeriesLayout Series); private sealed record PerRunSeriesLayout(ExportCurveData Data, SeriesLayout Series);

View File

@@ -1663,19 +1663,21 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private bool TryResolveHistoryExportData(out IReadOnlyList<PersistedRunData> exportData) private bool TryResolveHistoryExportData(out IReadOnlyList<PersistedRunData> exportData)
{ {
var historyData = new List<PersistedRunData>(); var historyData = new List<PersistedRunData>();
var hasExportableSamples = false;
foreach (var run in RunHistory.OrderBy(item => item.CompletedAt)) foreach (var run in RunHistory.OrderBy(item => item.CompletedAt))
{ {
var data = LoadRunData(run); var data = LoadRunData(run);
if (data is not null && data.Samples.Count > 0) if (data is not null)
{ {
historyData.Add(data); historyData.Add(data);
hasExportableSamples |= data.Samples.Any(RunExportService.IsValidSample);
} }
} }
exportData = historyData; exportData = historyData;
if (historyData.Count == 0) if (!hasExportableSamples)
{ {
AddWarningEvent("未找到可导出的历史采样数据。"); AddWarningEvent("未找到可导出的有效历史采样数据。");
return false; return false;
} }
@@ -1710,7 +1712,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable
} }
var workbookPath = _runExportService.ExportHistoricalComparisonReport(exportData, saveFileDialog.FileName); 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); _dataRepository.UpdateExportPaths(item.Run.RunId, null, workbookPath);
item.Run.ReportExportPath = workbookPath; item.Run.ReportExportPath = workbookPath;