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