目录
前言
在使用若依框架进行前后端分离开发时,常常需要处理Excel导出的需求。其中一个常见的需求是在导出Excel时能够合并某些具有相同值的单元格,以达到更好的视觉效果。
实现效果
思路
- 在若依框架中为 Excel 注解添加 isMerge() 方法,此方法用于标识该字段是否需要在导出时与其他具有相同值的单元格进行合并。
- 当导出Excel时,系统会检查那些标记了 isMerge() 为 true 的字段。
- 获取这些字段在Excel表格中的列位置。
- 对整个Sheet进行逐行遍历,比较当前行和下一行相应列的值是否相同。
- 如果值不同,则记录当前行作为合并区间的结束行。
- 完成所有行的遍历后,根据记录的信息执行单元格合并操作。
实现
1.对Excel注解添加isMerge方法
package com.ruoyi.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.math.BigDecimal;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import com.ruoyi.common.utils.poi.ExcelHandlerAdapter;
/**
* 自定义导出Excel数据注解
*
* @author zhiheng
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel {
// ...其他属性...
/**
* 是否合并单元格
* @return
*/
public boolean isMerge() default false;
// ...其他方法...
}
2.修改Excel工具类
修改ExcelUtil工具类,以便识别上述注解并在导出时合并单元格。
2.1 新增属性
在ExcelUtil类中添加mergeFields和dataRowNum属性,用于记录需要合并的列以及数据开始的行号。
public class ExcelUtil<T> {
// ...其他属性...
/**
* 需要合并的列信息。
*/
private List<Object[]> mergeFields = new ArrayList<>(30);
/**
* 数据从第几行开始。
*/
private int dataRowNum;
// ...其他方法...
}
2.2 新增方法
在ExcelUtil类中添加一个新的方法mergedCell(),用于执行单元格合并操作。
public class ExcelUtil<T> {
// ...其他方法...
/**
* 合并单元格的方法。此方法用于在一个Excel工作表(sheet)中根据指定规则合并重复值所在的行。
*/
public void mergedCell() {
// 获取工作表中的最后一行编号
int lastRowNum = sheet.getLastRowNum();
// 如果需要合并的字段列表为空或者表格只有一行,则无需执行合并操作
if (this.mergeFields.isEmpty() || lastRowNum == 0) return;
// 获取一个映射,其中包含需要被合并的列索引及其初始行号(通常为该行+1)
Map<Integer, Integer> mergeFileIndex = getMergeFileIndex();
// 创建一个新的映射来存储每个需要合并的列的范围信息
Map<Integer, List<String>> mergeMap = new HashMap<>();
// 初始化合并映射,为每个需要合并的列创建一个空的List来保存其合并范围
for (Map.Entry<Integer, Integer> entry : mergeFileIndex.entrySet()) {
mergeMap.put(entry.getKey(), new ArrayList<>());
}
// 遍历从数据开始行(dataRowNum)到最后一行的所有行
for (int i = dataRowNum; i <= lastRowNum; i++) {
// 获取当前行
Row thisRow = sheet.getRow(i);
// 获取下一行,如果存在的话;否则设置为null
Row nextRow = i < lastRowNum ? sheet.getRow(i + 1) : null;
// 对于每个需要合并的列
for (Map.Entry<Integer, Integer> entry : mergeFileIndex.entrySet()) {
// 获取当前行的单元格值
String thisValue = getCellValue(thisRow, entry.getKey());
// 获取下一行同一列的单元格值,如果存在的话
String nextValue = nextRow != null ? getCellValue(nextRow, entry.getKey()) : null;
// 检查当前行与下一行的值是否不同
if (!Objects.equals(thisValue, nextValue)) {
// 如果不同,并且该列的起始行号不是当前行号
if (entry.getValue() != i) {
// 则记录这个列的合并范围
mergeMap.get(entry.getKey()).add(entry.getValue() + "-" + i);
}
// 更新该列的起始行号为下一行
entry.setValue(i + 1);
}
}
}
// 遍历合并映射,将每个需要合并的列的范围应用到工作表上
for (Map.Entry<Integer, List<String>> entry : mergeMap.entrySet()) {
// 遍历每个范围
for (String range : entry.getValue()) {
// 将范围字符串分割成起始行和结束行
String[] split = range.split("-");
// 创建一个CellRangeAddress对象来表示合并区域
// 参数分别为:起始行,结束行,起始列,结束列
sheet.addMergedRegion(new CellRangeAddress(Integer.parseInt(split[0]), Integer.parseInt(split[1]), entry.getKey(), entry.getKey()));
}
}
}
// ...其他方法...
}
2.3 获取需要合并的字段所在列
添加一个辅助方法getMergeFileIndex()来获取需要合并的字段所在的列索引。
public class ExcelUtil<T> {
// ...其他方法...
/**
* 根据提供的合并字段获取一个映射,该映射包含了需要合并的列索引以及这些列的初始行号。
* @return 包含列索引和初始行号的映射。
*/
public Map<Integer, Integer> getMergeFileIndex() {
// 创建一个集合用来存储需要合并的字段名称
Set<String> mergeFieldSet = new HashSet<String>();
// 获取标题行,通常是数据行前的一行
Row titleRow = this.sheet.getRow(dataRowNum - 1);
// 遍历所有需要合并的字段配置
for (Object[] mergeField : this.mergeFields) {
// 从配置数组中取出Excel注解对象
Excel attr = (Excel) mergeField[1];
// 添加字段名到集合中
mergeFieldSet.add(attr.name());
}
// 定义一个映射来存放列的位置(索引)与其数据的初始行号
Map<Integer, Integer> mergeFieldIndexSet = new HashMap<>();
// 遍历标题行中的每一个单元格
for (int i = 0; i < titleRow.getPhysicalNumberOfCells(); i++) {
// 获取当前列的单元格
Cell cell = titleRow.getCell(i);
// 如果单元格为空则跳过
if (StringUtils.isNull(cell)) continue;
// 获取当前单元格的值(即标题)
String headerName = String.valueOf(this.getCellValue(titleRow, i));
// 如果标题是需要被合并的字段之一,则将其索引及初始行号加入到映射中
if (mergeFieldSet.contains(headerName)) {
mergeFieldIndexSet.put(i, this.dataRowNum);
}
}
// 返回最终的映射结果
return mergeFieldIndexSet;
}
// ...其他方法...
}
2.4 调整writeSheet()方法
最后,需要在writeSheet()方法中添加逻辑,以便在填充完数据后调用mergedCell()方法。
public class ExcelUtil<T> {
// ...其他方法...
/**
* 写入Excel表格数据到指定的工作表中。
*/
public void writeSheet() {
// 计算需要创建的工作表数量。这里的计算考虑到了如果数据量不足以填满一个完整的工作表,
// 也需要至少有一个工作表来放置数据。
int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
// 遍历每个需要创建的工作表
for (int index = 0; index < sheetNo; index++) {
// 创建或切换到相应的工作表
createSheet(sheetNo, index);
// 在当前工作表中创建新行,并设置数据行的起始行号
Row row = sheet.createRow(rownum);
dataRowNum = rownum + 1;
// 初始化列索引变量
int column = 0;
// 写入各个字段的列头名称
for (Object[] os : fields) {
// 从配置数组中取出Excel注解对象
Excel excel = (Excel) os[1];
// 创建列头单元格
// this.createCell(excel, row, column++); // 此行为注释掉的旧代码
this.createHeadCell(excel, row, column++);
// 如果当前字段需要合并,则将其添加到合并字段列表中
if (excel.isMerge()) {
mergeFields.add(os);
}
}
// 如果当前操作类型为导出,则填充Excel数据、添加统计行并合并单元格
if (Type.EXPORT.equals(type)) {
fillExcelData(index, row);
addStatisticsRow();
mergedCell();
}
}
}
// ...其他方法...
}
使用示例
在实体类中使用自定义的Excel注解,并设置isMerge属性为true。
public class MyEntity {
/** 包裹号 */
@Excel(name = "包裹号",isMerge = true)
private String packageNumber;
// ...其他字段和方法...
}
通过以上步骤,就可以在若依框架中实现Excel导出时的单元格合并功能了。希望这篇文章能帮助你更好地完成项目需求!