使用springboot+easyexcel实现导出excel并合并指定单元格

1:准备一个单元格合并策略类代码:

java 复制代码
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.List;

public class ExcelMergeHandler implements CellWriteHandler {

    // 要合并的列索引数组
    private final int[] mergeColumnIndex;
    // 合并开始的行索引
    private final int mergeRowIndex;

    /**
     * 构造函数
     *
     * @param mergeRowIndex     合并开始的行索引
     * @param mergeColumnIndex  要合并的列索引数组
     */
    public ExcelMergeHandler(int mergeRowIndex, int[] mergeColumnIndex) {
        this.mergeRowIndex = mergeRowIndex;
        this.mergeColumnIndex = mergeColumnIndex;
    }

    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
        // 单元格创建前的处理(这里不需要处理)
    }

    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        // 单元格创建后的处理(这里不需要处理)
    }

    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        // 当前行索引
        int curRowIndex = cell.getRowIndex();
        // 当前列索引
        int curColIndex = cell.getColumnIndex();

        // 如果当前行大于合并开始行且当前列在需要合并的列中
        if (curRowIndex > mergeRowIndex && isMergeColumn(curColIndex)) {
            // 进行合并操作
            mergeWithPrevRow(writeSheetHolder, cell, curRowIndex, curColIndex);
        }
    }

    /**
     * 检查当前列是否在需要合并的列中
     *
     * @param curColIndex 当前列索引
     * @return 如果是需要合并的列返回true,否则返回false
     */
    private boolean isMergeColumn(int curColIndex) {
        for (int columnIndex : mergeColumnIndex) {
            if (curColIndex == columnIndex) {
                return true;
            }
        }
        return false;
    }

    /**
     * 当前单元格向上合并
     *
     * @param writeSheetHolder 当前工作表持有者
     * @param cell             当前单元格
     * @param curRowIndex      当前行索引
     * @param curColIndex      当前列索引
     */
    private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int curRowIndex, int curColIndex) {
        // 获取当前单元格的数据
        Object curData = getCellData(cell);
        // 获取前一个单元格的数据
        Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
        Object preData = getCellData(preCell);

        // 判断当前单元格和前一个单元格的数据以及主键是否相同
        if (curData.equals(preData) && isSamePrimaryKey(cell, curRowIndex)) {
            // 获取工作表
            Sheet sheet = writeSheetHolder.getSheet();
            // 合并单元格
            mergeCells(sheet, curRowIndex, curColIndex);
        }
    }

    /**
     * 获取单元格的数据
     *
     * @param cell 单元格
     * @return 单元格数据
     */
    private Object getCellData(Cell cell) {
        return cell.getCellType() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
    }

    /**
     * 判断当前单元格和前一个单元格的主键是否相同
     *
     * @param cell         当前单元格
     * @param curRowIndex  当前行索引
     * @return 如果主键相同返回true,否则返回false
     */
    private boolean isSamePrimaryKey(Cell cell, int curRowIndex) {
        String currentPrimaryKey = cell.getRow().getCell(0).getStringCellValue();
        String previousPrimaryKey = cell.getSheet().getRow(curRowIndex - 1).getCell(0).getStringCellValue();
        return currentPrimaryKey.equals(previousPrimaryKey);
    }

    /**
     * 合并单元格
     *
     * @param sheet        工作表
     * @param curRowIndex  当前行索引
     * @param curColIndex  当前列索引
     */
    private void mergeCells(Sheet sheet, int curRowIndex, int curColIndex) {
        // 获取已合并的区域
        List<CellRangeAddress> mergeRegions = sheet.getMergedRegions();
        boolean isMerged = false;

        // 检查前一个单元格是否已经被合并
        for (int i = 0; i < mergeRegions.size() && !isMerged; i++) {
            CellRangeAddress cellRangeAddr = mergeRegions.get(i);
            if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
                sheet.removeMergedRegion(i);
                cellRangeAddr.setLastRow(curRowIndex);
                sheet.addMergedRegion(cellRangeAddr);
                isMerged = true;
            }
        }

        // 如果前一个单元格未被合并,则新增合并区域
        if (!isMerged) {
            CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
            sheet.addMergedRegion(cellRangeAddress);
        }
    }
}

2:业务类实现代码:

java 复制代码
    //从第2行开始合并
    private static final int mergeRowIndex=1;
    //需要合并的单元格列
    private static final int[] mergeCols={0,1,2,3,4,5,6,7,8,9,10,11,12};

    @Override
    public void exportBillOfQuantity(BillOfQuantityExportDto exportDto, HttpServletResponse response) {
        // 设置响应参数
        setResponseHeader(response, "export-bill-quantities.xlsx_");

        // 整理需要导出的数据
        List<ExportBillQuantitiesVo> exportBillQuantitiesVoList = getExportBillQuantitiesVoList(exportDto);

        try {
            // 获取模板文件输入流
            InputStream templateStream = new ClassPathResource(GlobalConstants.TEMPLATE_PATH + File.separator + GlobalConstants.EXPORT_BILL_QUANTITIES_FILE_NAME).getInputStream();

            // 使用EasyExcel写入数据到HttpServletResponse
            EasyExcel.write(response.getOutputStream())
                    .registerWriteHandler(setStyle())
                    .registerWriteHandler(new ExcelMergeHandler(mergeRowIndex, mergeCols))
                    .withTemplate(templateStream)
                    .sheet()
                    .doWrite(exportBillQuantitiesVoList);
        } catch (IOException e) {
            log.error("export productCoreParamList data is filed", e);
            throw new BusinessException(StatusCode.FAILED);
        }
    }

    // 设置响应头通用方法
    private void setResponseHeader(HttpServletResponse response, String fileName) {
        try {
            String fileNameStr = fileName + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xls";
            String encodedFileName = URLEncoder.encode(fileNameStr, StandardCharsets.UTF_8.toString());
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + encodedFileName);
        } catch (Exception e) {
            log.error("set response header error", e);
            throw new BusinessException(StatusCode.SYSTEM_ERROR);
        }
    }

    // 设置导出excel文件部分内容样式
    private HorizontalCellStyleStrategy setStyle() {
        // 定义样式:自动换行
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        contentWriteCellStyle.setWrapped(true); // 关键:开启自动换行
        WriteFont writeFont = new WriteFont();
        writeFont.setFontName("Microsoft YaHei"); // 字体
        writeFont.setFontHeightInPoints((short) 12); // 字体大小
        contentWriteCellStyle.setWriteFont(writeFont);

        // 注册样式策略(全局生效)
        HorizontalCellStyleStrategy styleStrategy = new HorizontalCellStyleStrategy(
                null, // 头样式(默认)
                contentWriteCellStyle // 内容样式(自动换行)
        );
        return styleStrategy;
    }
相关推荐
海兰34 分钟前
【web应用】Excel 项目数据自动化分析系统(AI 驱动分析)详细设计与部署指南(附源代码)
前端·人工智能·自动化·excel
fox_lht38 分钟前
15.3.改进我们之前的输入、输出项目
开发语言·后端·学习·rust
大鸡腿同学1 小时前
用 AI 肝了一个星期的智能客服助手,看看怎么个事
后端
IT_陈寒1 小时前
Python的os.path.join居然能这么坑?
前端·人工智能·后端
张忠琳1 小时前
【Go 1.26.4】Golang Channel 深度解析
开发语言·后端·golang
Rain5092 小时前
2.1 Nest.js 项目初始化与模块化架构
开发语言·前端·javascript·后端·架构·数据分析·node.js
cjp5602 小时前
009. ASP.NET WEB API 用户关联esp32设备
前端·后端·asp.net
贺国亚2 小时前
Text-to-SQL与Analytics-Agent
后端
一只叫煤球的猫2 小时前
ThreadForge 源码解读二:一个 Task 从 submit 到完成,内部到底发生了什么?
java·后端·面试