导出多行表头以及合并单元格的Excel

场景:

导出excel,excel的表头有多行,滚动excel表头固定,而且表格的数据行需要根据指定的某几列判断当内容相同时进行单元格跨行合并。涉及到表头跨列合并,单元格跨行合并。

具体表格的样式:

不合并单元格内容时导出的excel数据如下,列1,列2,列3部分行的值是一样的,图中标颜色的3、4行以及6、7、8行


最终需要导出的excel样式如下,前三列值一样时跨行合并单元格。图中第3、4行合并,第6、7、8行合并,第9行因为列3的值和前一行数据值不同不合并

代码实现

1、定义导出excel的模板DTO
java 复制代码
import java.io.Serializable;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;

@Data
public class ExcelExportDTO implements Serializable {
    private static final long serialVersionUID = 1L;
    @ColumnWidth(20)
    @ExcelProperty({"基本信息", "列1"})
    private String columnStr1;
    @ColumnWidth(30)
    @ExcelProperty({"基本信息", "列2"})
    private String columnStr2;
    @ColumnWidth(18)
    @ExcelProperty({"基本信息", "列3"})
    private String columnStr3;
    @ColumnWidth(36)
    @ExcelProperty({"其他信息", "列4"})
    private String columnStr4;
    @ColumnWidth(28)
    @ExcelProperty({"其他信息", "列5"})
    private String columnStr5;
    @ColumnWidth(28)
    @ExcelProperty({"其他信息", "列6"})
    private String columnStr6;
}

这里定义导出模板DTO的时候使用@ExcelProperty的数组格式,第一个值相同的会自动跨列合并

2、查询组装DTO列表作为导出数据,并调用导出方法
java 复制代码
	public void export(QueryRequest request, HttpServletResponse response) {
        List<ExcelExportDTO> exportList = new ArrayList<>();// TODO 这里调用自己的业务查询逻辑组装数据,注意如果需要合并将数据按顺序排列
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            // 组装需要跨行合并单元格的数据范围
            List<PrecomputedMergeHandler.MergeRange> mergeRanges = buildMergeRange(exportList, 2);
            // 写入 excel
            fileBizService.writeAndMergeColumnExcel(exportList, outputStream, ExcelExportDTO.class, 0, 2, new int[]{0, 1, 2}, mergeRanges);
            // 流转换
            byte[] buffer = outputStream.toByteArray();
            String fileName = "多行表头及合并单元格内容excel导出" + RandomUtil.randomNumbers(8) + ".xlsx";
            // 将流写出到 response
            fileBizService.writeToResponse(buffer, fileName, response);
        } catch (Exception e) {
            log.error("excel导出失败", e);
            throw new Exception("excel导出失败");
        }
    }


	/**
	 * 组装需要跨行合并单元格的数据范围
	 * 
	 * @param exportList excel导出的数据集合
	 * @param headerRows excel表头行数
	 * @return
	 */
	private List<PrecomputedMergeHandler.MergeRange> buildMergeRange(List<ExcelExportDTO> exportList, int headerRows) {
        List<PrecomputedMergeHandler.MergeRange> mergeRanges = new ArrayList<>();
        int startIndex = 0; // 数据行索引(0-based)
        for (int i = 0; i < exportList.size(); i++) {
            boolean isLast = (i == exportList.size() - 1);
            boolean groupEnd = isLast || !isSameGroup(exportList.get(i), exportList.get(i + 1));
            if (groupEnd) {
                if (i > startIndex) {
                    int startRow = headerRows + startIndex;
                    int endRow = headerRows + i;
                    mergeRanges.add(new PrecomputedMergeHandler.MergeRange(startRow, endRow));
                }
                startIndex = i + 1;
            }
        }
        return mergeRanges;
    }



	/**
	 * 是否相同的组
	 * 用来判断单元格是否需要合并,我这里的场景时当列1+列2+列3的内容相同时需要合并,所以就判断这三列的值
	 *
	 * @param a
	 * @param b
	 * @return
	 */
	private boolean isSameGroup(ExcelExportDTO a, ExcelExportDTO b) {
        return Objects.equals(a.getColumnStr1(), b.getColumnStr1())
                && Objects.equals(a.getColumnStr2(), b.getColumnStr2())
                && Objects.equals(a.getColumnStr3(), b.getColumnStr3());
    }
3、实现文件导出
java 复制代码
public interface FileBizService {

    /**
     * 导出Excel并处理合并单元格
     *
     * @param exportList           导出数据
     * @param outputStream         文件流
     * @param clazz                导出数据对象Class
     * @param freezeHeaderColSplit 冻结表头列数  从第一列开始
     * @param freezeHeaderRowSplit 冻结表头行数   冻结几行表头就传几 示例:冻结单行传1,冻结两行传2,不冻结传0
     * @param mergeColumnIndex     需要合并单元格的列索引,从0开始
     * @param mergeRanges          需要合并单元格的数据范围
     * @param <T>
     */
    <T> void writeAndMergeColumnExcel(List<T> exportList, ByteArrayOutputStream outputStream, Class<T> clazz, int freezeHeaderColSplit, int freezeHeaderRowSplit, int[] mergeColumnIndex, List<PrecomputedMergeHandler.MergeRange> mergeRanges);
}

import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;

/*
*	导出excel实现类
*/
@Service
public class FileBizServiceImpl implements FileBizService, InitializingBean {
		/**
     * 导出Excel并处理合并单元格
     *
     * @param exportList           导出数据
     * @param outputStream         文件流
     * @param clazz                导出数据对象Class
     * @param freezeHeaderColSplit 冻结表头列数  从第一列开始
     * @param freezeHeaderRowSplit 冻结表头行数   冻结几行表头就传几 示例:冻结单行传1,冻结两行传2,不冻结传0
     * @param mergeColumnIndex     需要合并单元格的列索引,从0开始
     * @param mergeRanges          需要合并单元格的数据范围
     * @param <T>
     */
		@Override
    public <T> void writeAndMergeColumnExcel(List<T> exportList, ByteArrayOutputStream outputStream, Class<T> clazz, int freezeHeaderColSplit, int freezeHeaderRowSplit, int[] mergeColumnIndex, List<PrecomputedMergeHandler.MergeRange> mergeRanges) {
        // 头的策略
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
        // 设置表头居中对齐
        headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        // 颜色
        headWriteCellStyle.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex());
        WriteFont headWriteFont = new WriteFont();
        headWriteFont.setFontHeightInPoints((short) 15);
        // 字体
        headWriteCellStyle.setWriteFont(headWriteFont);
        headWriteCellStyle.setWrapped(true);
        // 内容的策略
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        contentWriteCellStyle.setWrapped(true);
        // 水平居中
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        // 垂直居中
        contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);

        EasyExcel.write(outputStream, clazz)
                .excelType(ExcelTypeEnum.XLSX)
                // 设置表格样式策略
                .registerWriteHandler(horizontalCellStyleStrategy)
                // 合并单元格处理器
                .registerWriteHandler(new PrecomputedMergeHandler(mergeColumnIndex, mergeRanges))
                // 冻结表头处理器
                .registerWriteHandler(new DynamicFreezeHeadWriteHandler(freezeHeaderColSplit, freezeHeaderRowSplit))
                .sheet("Sheet1")
                .doWrite(exportList);
    }
}
4、合并单元格处理器
java 复制代码
import java.util.List;

import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;

public class PrecomputedMergeHandler implements SheetWriteHandler {

    private final int[] mergeColumns;
    private final List<MergeRange> mergeRanges;

    public PrecomputedMergeHandler(int[] mergeColumns, List<MergeRange> mergeRanges) {
        this.mergeColumns = mergeColumns;
        this.mergeRanges = mergeRanges;
    }

    @Override
    public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
    }

    @Override
    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
        Sheet sheet = writeSheetHolder.getSheet();
        for (MergeRange range : mergeRanges) {
            if (range.startRow < range.endRow) {  // 只合并多行区域
                for (int col : mergeColumns) {
                    CellRangeAddress region = new CellRangeAddress(range.startRow, range.endRow, col, col);
                    sheet.addMergedRegion(region);
                }
            }
        }
    }

    public static class MergeRange {
        int startRow; // 绝对行号(包含表头)
        int endRow;   // 绝对行号

        public MergeRange(int startRow, int endRow) {
            this.startRow = startRow;
            this.endRow = endRow;
        }
    }
}
5、冻结表头处理器
java 复制代码
public class DynamicFreezeHeadWriteHandler implements SheetWriteHandler {

    private final int colSplit;  // 冻结列数
    private final int rowSplit;  // 冻结行数

    public DynamicFreezeHeadWriteHandler(int colSplit, int rowSplit) {
        this.colSplit = colSplit;
        this.rowSplit = rowSplit;
    }

    @Override
    public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
    }

    @Override
    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
        // 冻结表头
        writeSheetHolder.getSheet().createFreezePane(colSplit, rowSplit, 0, 2);
    }
}