420 lines
17 KiB
C#
420 lines
17 KiB
C#
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(
|
||
"开始导出防滑性能 Excel:TestNumber={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;
|
||
}
|
||
}
|
||
}
|