Files
FootwearTest-20260602/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Services/SlipExcelExportService.cs
2026-06-08 11:56:58 +08:00

420 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
using Serilog;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using A = DocumentFormat.OpenXml.Drawing;
using C = DocumentFormat.OpenXml.Drawing.Charts;
using Xdr = DocumentFormat.OpenXml.Drawing.Spreadsheet;
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
{
public sealed class SlipExcelExportService
{
private const string InfoSheetName = "试验信息";
private const string ResultSheetName = "结果汇总";
private const string DataSheetName = "实时数据";
private const int StandardTrialCount = 3;
public string Export(SlipReportExport report, string? exportDirectory = null)
{
var directory = string.IsNullOrWhiteSpace(exportDirectory)
? GetDefaultExportDirectory()
: exportDirectory;
Directory.CreateDirectory(directory);
var safeNumber = MakeSafeFileName(string.IsNullOrWhiteSpace(report.TestNumber)
? DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)
: report.TestNumber);
var path = Path.Combine(directory, $"{safeNumber}_防滑性能测试报告.xlsx");
Log.Information(
"开始导出防滑性能 ExcelTestNumber={TestNumber}, ResultCount={ResultCount}, PointCount={PointCount}, Path={Path}",
report.TestNumber,
report.Results.Count,
report.Points.Count,
path);
using var document = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook);
var workbookPart = document.AddWorkbookPart();
workbookPart.Workbook = new Workbook();
var sheets = workbookPart.Workbook.AppendChild(new Sheets());
AddInfoSheet(workbookPart, sheets, 1, report);
AddResultSheet(workbookPart, sheets, 2, report.Results);
AddDataSheet(workbookPart, sheets, 3, report.Points);
workbookPart.Workbook.Save();
Log.Information("防滑性能 Excel 导出完成Path={Path}", path);
return path;
}
public static string GetDefaultExportDirectory()
{
var legacyDirectory = Path.Combine(@"D:\文件保存", DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture));
if (Directory.Exists(@"D:\"))
{
return legacyDirectory;
}
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
"FootwearSlipResistance",
DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture));
}
private static void AddInfoSheet(WorkbookPart workbookPart, Sheets sheets, uint sheetId, SlipReportExport report)
{
var rows = new List<IReadOnlyList<object?>>
{
new object?[] { "字段", "内容" },
new object?[] { "试验编号", report.TestNumber },
new object?[] { "报告名称", report.ReportName },
new object?[] { "操作人员", report.OperatorName },
new object?[] { "测试方法", report.MethodName },
new object?[] { "样品特征", report.SampleFeature },
new object?[] { "鞋码区间/目标负荷", $"{report.ShoeSize} / {report.TargetLoad}" },
new object?[] { "实际负荷", report.ActualLoad },
new object?[] { "润滑介质", report.Lubricant },
new object?[] { "测试模式", report.TestMode },
new object?[] { "地面材质", report.Surface },
new object?[] { "测试速度", report.TestSpeed },
new object?[] { "标准依据", report.StandardReference },
new object?[] { "导出时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) }
};
AddWorksheet(workbookPart, sheets, sheetId, InfoSheetName, rows, "A", "B");
}
private static void AddResultSheet(WorkbookPart workbookPart, Sheets sheets, uint sheetId, IReadOnlyList<TestSample> results)
{
var rows = new List<IReadOnlyList<object?>>
{
new object?[] { "序号", "时间", "静摩擦系数", "动摩擦系数", "判定" }
};
foreach (var result in results)
{
rows.Add(new object?[]
{
result.Index,
result.Time,
result.StaticCoefficient,
result.DynamicCoefficient,
result.Verdict
});
}
var latest = results.Take(StandardTrialCount).ToList();
if (latest.Count == StandardTrialCount)
{
rows.Add(new object?[] { string.Empty, string.Empty, string.Empty, string.Empty, string.Empty });
rows.Add(new object?[]
{
"近3次平均",
string.Empty,
latest.Average(result => result.StaticCoefficientValue).ToString("F2", CultureInfo.InvariantCulture),
latest.Average(result => result.DynamicCoefficientValue).ToString("F2", CultureInfo.InvariantCulture),
"GB/T 3903.6-2024 8.2"
});
}
AddWorksheet(workbookPart, sheets, sheetId, ResultSheetName, rows, "A", "E");
}
private static void AddDataSheet(WorkbookPart workbookPart, Sheets sheets, uint sheetId, IReadOnlyList<SlipDataPoint> points)
{
var rows = new List<IReadOnlyList<object?>>
{
new object?[] { "时间(s)", "垂直压力(N)", "水平拉力(N)", "距离(mm)", "摩擦系数" }
};
foreach (var point in points)
{
rows.Add(new object?[]
{
point.TimeSeconds,
point.VerticalLoadN,
point.HorizontalFrictionN,
point.DisplacementMm,
point.FrictionCoefficient
});
}
var worksheetPart = AddWorksheet(workbookPart, sheets, sheetId, DataSheetName, rows, "A", "E");
if (points.Count > 0)
{
AddChart(worksheetPart, points.Count + 1);
}
}
private static WorksheetPart AddWorksheet(
WorkbookPart workbookPart,
Sheets sheets,
uint sheetId,
string sheetName,
IReadOnlyList<IReadOnlyList<object?>> rows,
string firstColumn,
string lastColumn)
{
var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
var sheetData = new SheetData();
var worksheet = new Worksheet();
worksheet.Append(CreateColumns(firstColumn, lastColumn));
worksheet.Append(sheetData);
worksheetPart.Worksheet = worksheet;
for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++)
{
var row = new Row { RowIndex = (uint)(rowIndex + 1) };
for (var columnIndex = 0; columnIndex < rows[rowIndex].Count; columnIndex++)
{
row.Append(CreateCell(columnIndex + 1, rowIndex + 1, rows[rowIndex][columnIndex]));
}
sheetData.Append(row);
}
var relationshipId = workbookPart.GetIdOfPart(worksheetPart);
sheets.Append(new Sheet
{
Id = relationshipId,
SheetId = sheetId,
Name = sheetName
});
worksheetPart.Worksheet.Save();
return worksheetPart;
}
private static Columns CreateColumns(string firstColumn, string lastColumn)
{
var first = ColumnNameToIndex(firstColumn);
var last = ColumnNameToIndex(lastColumn);
var columns = new Columns();
for (var index = first; index <= last; index++)
{
columns.Append(new Column
{
Min = (uint)index,
Max = (uint)index,
Width = index == 1 ? 15 : 18,
CustomWidth = true
});
}
return columns;
}
private static Cell CreateCell(int columnIndex, int rowIndex, object? value)
{
var cell = new Cell { CellReference = $"{GetColumnName(columnIndex)}{rowIndex}" };
switch (value)
{
case null:
cell.DataType = CellValues.String;
cell.CellValue = new CellValue(string.Empty);
break;
case int intValue:
cell.CellValue = new CellValue(intValue);
break;
case double doubleValue:
cell.CellValue = new CellValue(doubleValue.ToString("0.#####", CultureInfo.InvariantCulture));
break;
default:
cell.DataType = CellValues.String;
cell.CellValue = new CellValue(Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty);
break;
}
return cell;
}
private static void AddChart(WorksheetPart worksheetPart, int lastRow)
{
var drawingsPart = worksheetPart.AddNewPart<DrawingsPart>();
var drawingRelationshipId = worksheetPart.GetIdOfPart(drawingsPart);
worksheetPart.Worksheet.Append(new DocumentFormat.OpenXml.Spreadsheet.Drawing { Id = drawingRelationshipId });
drawingsPart.WorksheetDrawing = new Xdr.WorksheetDrawing();
AddChartPart(
drawingsPart,
CreateScreenshotStyleChartSpace(lastRow),
2U,
"防滑性能测试曲线",
1,
39);
drawingsPart.WorksheetDrawing.Save();
worksheetPart.Worksheet.Save();
}
private static void AddChartPart(
DrawingsPart drawingsPart,
C.ChartSpace chartSpace,
uint drawingId,
string name,
int fromRow,
int toRow)
{
var chartPart = drawingsPart.AddNewPart<ChartPart>();
var chartRelationshipId = drawingsPart.GetIdOfPart(chartPart);
chartPart.ChartSpace = chartSpace;
chartPart.ChartSpace.Save();
var graphicFrame = new Xdr.GraphicFrame(
new Xdr.NonVisualGraphicFrameProperties(
new Xdr.NonVisualDrawingProperties { Id = drawingId, Name = name },
new Xdr.NonVisualGraphicFrameDrawingProperties()),
new Xdr.Transform(
new A.Offset { X = 0L, Y = 0L },
new A.Extents { Cx = 0L, Cy = 0L }),
new A.Graphic(
new A.GraphicData(
new C.ChartReference { Id = chartRelationshipId })
{ Uri = "http://schemas.openxmlformats.org/drawingml/2006/chart" }));
drawingsPart.WorksheetDrawing.Append(new Xdr.TwoCellAnchor(
new Xdr.FromMarker(
new Xdr.ColumnId("6"),
new Xdr.ColumnOffset("0"),
new Xdr.RowId(fromRow.ToString(CultureInfo.InvariantCulture)),
new Xdr.RowOffset("0")),
new Xdr.ToMarker(
new Xdr.ColumnId("15"),
new Xdr.ColumnOffset("0"),
new Xdr.RowId(toRow.ToString(CultureInfo.InvariantCulture)),
new Xdr.RowOffset("0")),
graphicFrame,
new Xdr.ClientData()));
}
private static C.ChartSpace CreateScreenshotStyleChartSpace(int lastRow)
{
const uint xAxisId = 48650112U;
const uint forceAndDistanceAxisId = 48672768U;
const uint coefficientAxisId = 48689152U;
var forceChart = new C.ScatterChart(
new C.ScatterStyle { Val = C.ScatterStyleValues.LineMarker },
CreateSeries(0, 2, lastRow),
CreateSeries(1, 3, lastRow),
new C.AxisId { Val = xAxisId },
new C.AxisId { Val = forceAndDistanceAxisId });
var coefficientChart = new C.ScatterChart(
new C.ScatterStyle { Val = C.ScatterStyleValues.LineMarker },
CreateSeries(2, 5, lastRow),
new C.AxisId { Val = xAxisId },
new C.AxisId { Val = coefficientAxisId });
var distanceChart = new C.ScatterChart(
new C.ScatterStyle { Val = C.ScatterStyleValues.LineMarker },
CreateSeries(3, 4, lastRow),
new C.AxisId { Val = xAxisId },
new C.AxisId { Val = forceAndDistanceAxisId });
var chart = new C.Chart(
CreateTitle("防滑性能测试曲线"),
new C.PlotArea(
new C.Layout(),
forceChart,
coefficientChart,
distanceChart,
CreateValueAxis(xAxisId, forceAndDistanceAxisId, C.AxisPositionValues.Bottom, "时间(s)"),
CreateValueAxis(forceAndDistanceAxisId, xAxisId, C.AxisPositionValues.Left, "压力(N)/距离(mm)"),
CreateValueAxis(coefficientAxisId, xAxisId, C.AxisPositionValues.Right, "摩擦系数")),
new C.Legend(
new C.LegendPosition { Val = C.LegendPositionValues.Bottom },
new C.Layout()),
new C.PlotVisibleOnly { Val = true });
return new C.ChartSpace(
new C.EditingLanguage { Val = "zh-CN" },
chart);
}
private static C.ScatterChartSeries CreateSeries(uint index, int yColumnIndex, int lastRow)
{
var sheet = EscapeSheetName(DataSheetName);
var xFormula = $"'{sheet}'!$A$2:$A${lastRow}";
var yColumn = GetColumnName(yColumnIndex);
var yFormula = $"'{sheet}'!${yColumn}$2:${yColumn}${lastRow}";
var titleFormula = $"'{sheet}'!${yColumn}$1";
return new C.ScatterChartSeries(
new C.Index { Val = index },
new C.Order { Val = index },
new C.SeriesText(new C.StringReference(new C.Formula(titleFormula))),
new C.XValues(new C.NumberReference(new C.Formula(xFormula))),
new C.YValues(new C.NumberReference(new C.Formula(yFormula))),
new C.Smooth { Val = false });
}
private static C.ValueAxis CreateValueAxis(uint axisId, uint crossingAxisId, C.AxisPositionValues position, string title) =>
new(
new C.AxisId { Val = axisId },
new C.Scaling(new C.Orientation { Val = C.OrientationValues.MinMax }),
new C.AxisPosition { Val = position },
new C.MajorGridlines(),
CreateTitle(title),
new C.NumberingFormat { FormatCode = "0.00", SourceLinked = false },
new C.TickLabelPosition { Val = C.TickLabelPositionValues.NextTo },
new C.CrossingAxis { Val = crossingAxisId },
new C.Crosses { Val = C.CrossesValues.AutoZero });
private static C.Title CreateTitle(string text) =>
new(
new C.ChartText(
new C.RichText(
new A.BodyProperties(),
new A.ListStyle(),
new A.Paragraph(
new A.Run(
new A.RunProperties { Language = "zh-CN", FontSize = 1100 },
new A.Text(text))))),
new C.Layout(),
new C.Overlay { Val = false });
private static string MakeSafeFileName(string value)
{
foreach (var invalidChar in Path.GetInvalidFileNameChars())
{
value = value.Replace(invalidChar, '_');
}
return value;
}
private static string EscapeSheetName(string value) => value.Replace("'", "''");
private static string GetColumnName(int columnIndex)
{
var dividend = columnIndex;
var columnName = string.Empty;
while (dividend > 0)
{
var modulo = (dividend - 1) % 26;
columnName = Convert.ToChar('A' + modulo) + columnName;
dividend = (dividend - modulo) / 26;
}
return columnName;
}
private static int ColumnNameToIndex(string columnName)
{
var sum = 0;
foreach (var character in columnName)
{
sum *= 26;
sum += character - 'A' + 1;
}
return sum;
}
}
}