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("未找到可导出的有效历史数据。");
}
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<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
&& 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<ExportCurveData> runs)
private static void BuildSummarySheet(
IXLWorksheet worksheet,
IReadOnlyList<ExportCurveData> 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<SeriesLayout>();
var perRunSeries = new List<PerRunSeriesLayout>();
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<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;
var charts = new List<ChartDefinition>();
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<ExportCurveData> 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<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 PerRunSeriesLayout(ExportCurveData Data, SeriesLayout Series);

View File

@@ -1663,19 +1663,21 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private bool TryResolveHistoryExportData(out IReadOnlyList<PersistedRunData> exportData)
{
var historyData = new List<PersistedRunData>();
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;