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> { 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 results) { var rows = new List> { 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 points) { var rows = new List> { 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> rows, string firstColumn, string lastColumn) { var worksheetPart = workbookPart.AddNewPart(); 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(); 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(); 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; } } }