diff --git a/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/IWriter.cs b/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/IWriter.cs index 030dd45ce4a96417b16bb118879c294038f4cd99..107a313c8b5a83c31b44e6a60188921f548204ff 100644 --- a/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/IWriter.cs +++ b/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/IWriter.cs @@ -47,5 +47,10 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport /// 表格数据对象Key /// string TableKey { get; set; } + + /// + /// 单元格合并键 + /// + string MergeCellKey { get; set; } } } \ No newline at end of file diff --git a/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/TemplateExportHelper.cs b/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/TemplateExportHelper.cs index c8da92764cd56bd62c77893a6f626f148258b7f8..8dda7766a1d7135725537308bd2713f6b5d83415 100644 --- a/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/TemplateExportHelper.cs +++ b/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/TemplateExportHelper.cs @@ -174,7 +174,7 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport Data = data ?? throw new ArgumentException("数据不能为空!", nameof(data)); - using (Stream stream = new FileStream(TemplateFilePath, FileMode.Open)) + using (Stream stream = new FileStream(TemplateFilePath, FileMode.Open, FileAccess.Read)) { using (var excelPackage = new ExcelPackage(stream)) { @@ -277,6 +277,23 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport var isFirst = true; + //calculate stride + var minRowIndex = -1; + var maxRowIndex = -1; + foreach (var writer in tableGroup) + { + var address = new ExcelAddressBase(writer.TplAddress); + if (minRowIndex == -1 || minRowIndex > address.Start.Row) + { + minRowIndex = address.Start.Row; + } + if (maxRowIndex < address.End.Row) + { + maxRowIndex = address.End.Row; + } + } + var stride = maxRowIndex - minRowIndex + 1;//default to 1 + var mergeCellKeys = new Dictionary>(); foreach (var col in tableGroup) { var address = new ExcelAddressBase(col.TplAddress); @@ -285,6 +302,16 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport sheet.Cells[address.Start.Row, address.Start.Column].Value = string.Empty; continue; } + if (!col.MergeCellKey.IsNullOrWhiteSpace()) + { + if (!mergeCellKeys.ContainsKey(col.MergeCellKey)) + { + mergeCellKeys.Add(col.MergeCellKey, new List()); + } + var mergecelllist = mergeCellKeys[col.MergeCellKey]; + //mergecelllist.Add(address); + mergecelllist.Add(new ExcelAddressBase(col.TplAddress)); //avoid side effect of below codes + } //TODO:支持同一行多个表格 //行数大于1时需要插入行 if (isFirst && rowCount > 1) @@ -292,21 +319,25 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport startRow = address.Start.Row; //插入行 //插入的目标行号 - var targetRow = address.Start.Row + 1 + insertRows; + var targetRow = address.Start.Row + 1*stride + insertRows; //插入 - var numRowsToInsert = rowCount - 1; + var numRowsToInsert = (rowCount - 1); var refRow = address.Start.Row + insertRows; //sheet.InsertRow(targetRow, numRowsToInsert, refRow); - sheet.InsertRow(targetRow, numRowsToInsert); + sheet.InsertRow(targetRow, numRowsToInsert * stride); //EPPlus的问题。修复如果存在合并的单元格,但是在新插入的行无法生效的问题,具体见 https://stackoverflow.com/questions/31853046/epplus-copy-style-to-a-range/34299694#34299694 for (var i = 0; i < numRowsToInsert; i++) { - sheet.Cells[String.Format("{0}:{0}", refRow)].Copy(sheet.Cells[String.Format("{0}:{0}", targetRow + i)]); - //sheet.Row(refRow).StyleID = sheet.Row(targetRow + i).StyleID; + for(var j=0; j < stride; j++) + { + sheet.Cells[String.Format("{0}:{0}", refRow+j)].Copy( + sheet.Cells[String.Format("{0}:{0}", targetRow + i*stride+j)]); + //sheet.Row(refRow).StyleID = sheet.Row(targetRow + i).StyleID; + } } } - RenderTableCells(target, tbParameters, sheet, insertRows, tableKey, rowCount, col, address); + RenderTableCells(target, tbParameters, sheet, insertRows, tableKey, rowCount, col, address, stride); if (isFirst) { @@ -314,21 +345,118 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport } } + #region 合并单元格 + { + foreach (var mergeCellKey in mergeCellKeys.Keys) + { + foreach (var address in mergeCellKeys[mergeCellKey]) + { + object currentValue; + object prevValue = null; + var rangeStartRowIndex = -1; + var rangeEndRowIndex = -1; + //check the list of data for this column + for (var i = 0; i < rowCount; i++) + { + var cellFunc = CreateOrGetCellFuncByTableKey(target, tbParameters, tableKey, mergeCellKey); + var result = cellFunc.Invoke(i); + + var rowIndex = address.Start.Row + i * stride + insertRows; + currentValue = result; + if (currentValue.Equals(prevValue)) + { + //merge cell + if (rangeStartRowIndex == -1) + { + rangeStartRowIndex = rowIndex - 1; + } + rangeEndRowIndex = rowIndex; + } + else + { + //change of value, hence perform range merge + if (rangeStartRowIndex != -1) + { + var rangeAddress = new ExcelAddress( + rangeStartRowIndex, address.Start.Column, rangeEndRowIndex, address.Start.Column); + sheet.Cells[rangeAddress.Address].Merge = true; + rangeStartRowIndex = -1; + } + } + prevValue = currentValue; + } + if (rangeStartRowIndex != -1) + { + var rangeAddress = new ExcelAddress( + rangeStartRowIndex, address.Start.Column, rangeEndRowIndex, address.Start.Column); + sheet.Cells[rangeAddress.Address].Merge = true; + rangeStartRowIndex = -1; + } + } + } + } + + #endregion 合并单元格 + #region 更新单元格 var updateCellWriters = SheetWriters[sheetName].Where(p => p.WriterType == WriterTypes.Cell).Where(p => p.RowIndex > startRow); foreach (var item in updateCellWriters) { - item.RowIndex += rowCount - 1; + item.RowIndex += (rowCount - 1)*stride; } #endregion 更新单元格 //表格渲染完成后更新插入的行数 - insertRows += rowCount - 1; + insertRows += (rowCount - 1)*stride; } } + private Lambda CreateOrGetCellFuncByTableKey(Interpreter target, Parameter[] tbParameters, string tableKey, string cellKey) + { + //get expression + var expresson = "{{" + cellKey + "}}"; + string dataVar; + if (IsDictionaryType || IsExpandoObjectType) + { + dataVar = ($"\" + {tableKey}.Skip(index).First()"); + } + else if (IsJObjectType) + { + dataVar = $"\" + data[\"{tableKey}\"][index]"; + } + else + { + dataVar = $"\" + {tableKey}.Skip(index).First()."; + } + { + if (IsDynamicSupportTypes) + { + dataVar = dataVar.TrimEnd('.'); + expresson = expresson + .Replace("{{", dataVar + "[\"") + .Replace("}}", "\"] + \""); + } + else + { + expresson = expresson + .Replace("{{", dataVar) + .Replace("}}", " + \""); + } + + expresson = expresson.StartsWith("\"") + ? expresson.TrimStart('\"').TrimStart().TrimStart('+') + : "\"" + expresson; + expresson = expresson.EndsWith("\"") + ? expresson.TrimEnd('\"').TrimEnd().TrimEnd('+') + : expresson + "\""; + } + var cellFunc = CreateOrGetCellFunc(target, null, expresson, tbParameters); + + return cellFunc; + + } /// /// 重新设置行宽(适应图片) /// @@ -365,17 +493,22 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport /// /// /// - private void RenderTableCells(Interpreter target, Parameter[] tbParameters, ExcelWorksheet sheet, int insertRows, string tableKey, int rowCount, IWriter writer, ExcelAddressBase address) + /// + private void RenderTableCells( + Interpreter target, Parameter[] tbParameters, ExcelWorksheet sheet, int insertRows, + string tableKey, int rowCount, IWriter writer, ExcelAddressBase address, int stride=1) { var cellString = writer.CellString; + var tokens = cellString.Split('|'); if (cellString.Contains("{{Table>>")) //{{ Table >> BookInfo | RowNo}} - cellString = "{{" + cellString.Split('|')[1].Trim(); + cellString = "{{" + tokens[1].Trim(); else if (cellString.Contains(">>Table}}")) //{{Remark|>>Table}} - cellString = cellString.Split('|')[0].Trim() + "}}"; + cellString = tokens[0].Trim() + "}}"; - RenderTableCells(target, tbParameters, sheet, insertRows, tableKey, rowCount, cellString, address); + RenderTableCells(target, tbParameters, sheet, insertRows, tableKey, + rowCount, cellString, address, stride); } /// @@ -405,7 +538,7 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport /// /// /// - private void RenderCell(Interpreter target, ExcelWorksheet sheet, string expresson, string cellAddress, string dataVar = "\" + data.", Lambda cellFunc = null, Parameter[] parameters = null, params object[] invokeParams) + private object RenderCell(Interpreter target, ExcelWorksheet sheet, string expresson, string cellAddress, string dataVar = "\" + data.", Lambda cellFunc = null, Parameter[] parameters = null, params object[] invokeParams) { //处理单元格渲染管道 RenderCellPipeline(target, sheet, ref expresson, cellAddress, cellFunc, parameters, dataVar, invokeParams); @@ -443,6 +576,7 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport { sheet.Cells[cellAddress].Value = expresson; } + return sheet.Cells[cellAddress].Value; } /// @@ -456,7 +590,10 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport /// /// /// - private void RenderTableCells(Interpreter target, Parameter[] parameters, ExcelWorksheet sheet, int insertRows, string tableKey, int rowCount, string cellString, ExcelAddressBase address) + /// + private void RenderTableCells(Interpreter target, Parameter[] parameters, + ExcelWorksheet sheet, int insertRows, string tableKey, int rowCount, + string cellString, ExcelAddressBase address, int stride=1) { //var dataVar = !IsDynamicSupportTypes ? ("\" + data." + tableKey + "[index].") : ("\" + data[\"" + tableKey + "\"][index]"); string dataVar; @@ -474,13 +611,26 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport } //渲染一列单元格 + //var isFirstCell = true; + //object previousValue = null; for (var i = 0; i < rowCount; i++) { - var rowIndex = address.Start.Row + i + insertRows; + var rowIndex = address.Start.Row + i*stride + insertRows; var targetAddress = new ExcelAddress(rowIndex, address.Start.Column, rowIndex, address.Start.Column); //https://github.com/dotnetcore/Magicodes.IE/issues/155 sheet.Row(rowIndex).Height = sheet.Row(address.Start.Row).Height; - RenderCell(target, sheet, cellString, targetAddress.Address, dataVar, null, parameters, i); + var value = RenderCell(target, sheet, cellString, targetAddress.Address, dataVar, null, parameters, i); + //if (!mergeCellKey.IsNullOrWhiteSpace()) + //{ + // if (!isFirstCell) + // { + // //check if need merge with the cell in previous row (in case of stride > 1, + // //make sure the adjacent cells are merged in template) + + // } + // isFirstCell = false; + // previousValue = value; + //} } } @@ -669,10 +819,11 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport var rows = q.GroupBy(p => p.Rows); + //move isStartTable out of outer loop to cope with multiple line table + var isStartTable = false; + string tableKey = null; foreach (var rowGroups in rows) { - var isStartTable = false; - string tableKey = null; foreach (var cell in rowGroups) { var cellString = cell.Value.ToString(); @@ -682,6 +833,20 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport //{{ Table >> BookInfo | RowNo}} tableKey = Regex.Split(cellString, "{{Table>>")[1].Split('|')[0].Trim(); } + var mergeCellKey = string.Empty; + if (cellString.ToLower().Contains("|merge")) + { + var tokens = Regex.Split(cellString, + "Merge", RegexOptions.IgnoreCase); + var tokens1 = tokens[1].Split('}'); + var tokens2 = tokens1[0].Trim('?', '&').Split('='); + mergeCellKey = tokens2[1].Split('|')[0].Trim('}',' '); + + //remove merge + cellString = Regex.Replace(cellString, "\\|Merge.*\\|", e => { return "|"; }, RegexOptions.IgnoreCase); + cellString = Regex.Replace(cellString, "\\|Merge.*}}", e => { return "}}"; }, RegexOptions.IgnoreCase); + + } writers.Add(new Writer { @@ -690,7 +855,8 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport CellString = cellString, WriterType = isStartTable ? WriterTypes.Table : WriterTypes.Cell, RowIndex = cell.Start.Row, - ColIndex = cell.Start.Column + ColIndex = cell.Start.Column, + MergeCellKey = mergeCellKey }); if (isStartTable && cellString.Contains(">>Table}}")) diff --git a/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/Writer.cs b/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/Writer.cs index 118d55c31a2608ad24e9c74b80e8060b504add42..ac7cfc31d9450c856cd775fc27e0e9410a27d810 100644 --- a/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/Writer.cs +++ b/src/Magicodes.ExporterAndImporter.Excel/Utility/TemplateExport/Writer.cs @@ -52,5 +52,10 @@ namespace Magicodes.ExporterAndImporter.Excel.Utility.TemplateExport /// 列号 /// public int ColIndex { get; set; } + + /// + /// 单元格合并索引属性:相邻行此属性同值时,当前单元格与上一行同列单元格合并 + /// + public string MergeCellKey { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/src/Magicodes.ExporterAndImporter.Tests/ExcelTemplateExporter2_Tests.cs b/src/Magicodes.ExporterAndImporter.Tests/ExcelTemplateExporter2_Tests.cs new file mode 100644 index 0000000000000000000000000000000000000000..2dbac12606e7a42b811100de1cd54ea85e3c0c9c --- /dev/null +++ b/src/Magicodes.ExporterAndImporter.Tests/ExcelTemplateExporter2_Tests.cs @@ -0,0 +1,127 @@ +using Magicodes.ExporterAndImporter.Core; +using Magicodes.ExporterAndImporter.Excel; +using Magicodes.ExporterAndImporter.Tests.Models.Export; +using Magicodes.ExporterAndImporter.Tests.Models.Export.ExportByTemplate_Test1; +using Newtonsoft.Json.Linq; +using OfficeOpenXml; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Magicodes.ExporterAndImporter.Tests +{ + public class ExcelTemplateExporter2_Tests : TestBase + { + #region 模板导出 + + [Fact(DisplayName = "Excel跨行模板导出")] + public async Task ExportByTemplateMultiRows_Test() + { + //模板路径 + var tplPath = Path.Combine(Directory.GetCurrentDirectory(), "TestFiles", "ExportTemplates", + "template-multirows.xlsx"); + //创建Excel导出对象 + IExportFileByTemplate exporter = new ExcelExporter(); + //导出路径 + var filePath = Path.Combine(Directory.GetCurrentDirectory(), nameof(ExportByTemplateMultiRows_Test) + ".xlsx"); + if (File.Exists(filePath)) File.Delete(filePath); + //根据模板导出 + await exporter.ExportByTemplate(filePath, + new TextbookOrderInfo("湖南心莱信息科技有限公司", "湖南长沙岳麓区", "雪雁", "1367197xxxx", null, + DateTime.Now.ToLongDateString(), "https://docs.microsoft.com/en-us/media/microsoft-logo-dark.png", + new List() + { + new BookInfo(1, "0000000001", "《XX从入门到放弃》", null, "机械工业出版社", "3.14", 100, "备注") + { + Cover = Path.Combine("TestFiles", "ExporterTest.png") + }, + new BookInfo(2, "0000000001", "《XX从入门到放弃》", null, "机械工业出版社", "3.14", 100, "备注") + { + Cover = "https://docs.microsoft.com/en-us/media/microsoft-logo-dark.png" + }, + new BookInfo(3, "0000000002", "《XX从入门到放弃》", "张三", "机械工业出版社", "3.14", 100, null), + new BookInfo(4, null, "《XX从入门到放弃》", "张三", "机械工业出版社", "3.14", 100, "备注") + { + Cover = Path.Combine("TestFiles", "issue131.png") + } + }), + tplPath); + + using (var pck = new ExcelPackage(new FileInfo(filePath))) + { + //检查转换结果 + var sheet = pck.Workbook.Worksheets.First(); + //确保所有的转换均已完成 + sheet.Cells[sheet.Dimension.Address].Any(p => p.Text.Contains("{{")).ShouldBeFalse(); + //检查图片 + sheet.Drawings.Count.ShouldBe(4); + + sheet.Cells[sheet.Dimension.Address].Any(p => p.Text.Contains("图")).ShouldBeTrue(); + //检查合计是否正确 + + sheet.Cells["H15"].Formula.ShouldBe("=SUM(G4,G6,G8,G10)"); + sheet.Cells["H16"].Formula.ShouldBe("=AVERAGE(G4,G6,G8,G10)"); + } + } + + + [Fact(DisplayName = "Excel合并单元格模板导出")] + public async Task ExportByTemplateMergeCells_Test() + { + //模板路径 + var tplPath = Path.Combine(Directory.GetCurrentDirectory(), "TestFiles", "ExportTemplates", + "template-mergecells.xlsx"); + //创建Excel导出对象 + IExportFileByTemplate exporter = new ExcelExporter(); + //导出路径 + var filePath = Path.Combine(Directory.GetCurrentDirectory(), nameof(ExportByTemplateMergeCells_Test) + ".xlsx"); + if (File.Exists(filePath)) File.Delete(filePath); + //根据模板导出 + await exporter.ExportByTemplate(filePath, + new TextbookOrderInfo("湖南心莱信息科技有限公司", "湖南长沙岳麓区", "雪雁", "1367197xxxx", null, + DateTime.Now.ToLongDateString(), "https://docs.microsoft.com/en-us/media/microsoft-logo-dark.png", + new List() + { + new BookInfo(1, "0000000001", "《XX从入门到放弃》", null, "机械工业出版社", "3.14", 100, "备注") + { + Cover = Path.Combine("TestFiles", "ExporterTest.png") + }, + new BookInfo(2, "0000000001", "《XX从入门到放弃》", null, "机械工业出版社", "3.14", 100, "备注") + { + Cover = "https://docs.microsoft.com/en-us/media/microsoft-logo-dark.png" + }, + new BookInfo(3, "0000000002", "《XX从入门到放弃》", "张三", "机械工业出版社", "3.14", 100, null), + new BookInfo(4, null, "《XX从入门到放弃》", "张三", "机械工业出版社", "3.14", 100, "备注") + { + Cover = Path.Combine("TestFiles", "issue131.png") + } + }), + tplPath); + + using (var pck = new ExcelPackage(new FileInfo(filePath))) + { + //检查转换结果 + var sheet = pck.Workbook.Worksheets.First(); + //确保所有的转换均已完成 + sheet.Cells[sheet.Dimension.Address].Any(p => p.Text.Contains("{{")).ShouldBeFalse(); + //检查图片 + sheet.Drawings.Count.ShouldBe(4); + + sheet.Cells[sheet.Dimension.Address].Any(p => p.Text.Contains("图")).ShouldBeTrue(); + //检查合计是否正确 + + sheet.Cells["H11"].Formula.ShouldBe("=SUM(G4:G6,G4)"); + sheet.Cells["H12"].Formula.ShouldBe("=AVERAGE(G4:G6)"); + } + } + + #endregion 模板导出 + + } +} diff --git a/src/Magicodes.ExporterAndImporter.Tests/Magicodes.ExporterAndImporter.Tests.csproj b/src/Magicodes.ExporterAndImporter.Tests/Magicodes.ExporterAndImporter.Tests.csproj index 632cf4aac503784b0c592bd8d386b1ac757f53b1..831c9b241d7db3618b8a2286f7765e470705abf1 100644 --- a/src/Magicodes.ExporterAndImporter.Tests/Magicodes.ExporterAndImporter.Tests.csproj +++ b/src/Magicodes.ExporterAndImporter.Tests/Magicodes.ExporterAndImporter.Tests.csproj @@ -84,6 +84,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/src/Magicodes.ExporterAndImporter.Tests/TestFiles/ExportTemplates/template-mergecells.xlsx b/src/Magicodes.ExporterAndImporter.Tests/TestFiles/ExportTemplates/template-mergecells.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..58e73308ee950e890e6801f510c800aa31be0a4a Binary files /dev/null and b/src/Magicodes.ExporterAndImporter.Tests/TestFiles/ExportTemplates/template-mergecells.xlsx differ diff --git a/src/Magicodes.ExporterAndImporter.Tests/TestFiles/ExportTemplates/template-multirows.xlsx b/src/Magicodes.ExporterAndImporter.Tests/TestFiles/ExportTemplates/template-multirows.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e22dc639a028d2de8fb3e8b25b1ad8beb6300fd1 Binary files /dev/null and b/src/Magicodes.ExporterAndImporter.Tests/TestFiles/ExportTemplates/template-multirows.xlsx differ