【EasyExcel】EasyExcel合并指定列单元格导出详解设置导出样式

【EasyExcel】EasyExcel合并指定列单元格导出&设置导出样式

需求分析

  1. 需求背景

    • 许多报表需要对相同数据的单元格进行合并,以提高数据的可读性和美观性。例如,在销售报表中,将相同客户的订单合并在一起。
    • 同时,报表中的标题和内容部分通常需要不同的样式,以便于区分和阅读。
  2. 确定需求

    • 合并单元格的列索引,例如合并第1、2列中的相同数据。
    • 合并操作开始的行索引,通常为数据行的起始行。
    • 标题行和内容行需要不同的样式,如字体、大小、对齐方式等。
  3. 示例

    • 将图一表格导出为图二形式
    • 图一

    image-20240515175949924

    • 图二

    image-20240515175718363

实现步骤

一、映射实体

上图Excel导出文件的映射类如下:

package com.shy.server.business.finance.reimburse.excel;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;

import java.math.BigDecimal;

@Data
@HeadRowHeight(60)
@ContentRowHeight(60)
public class ExpenseUserReimburseDetailToExcel {

    @ExcelProperty(value = {"报销明细表(报销人)", "月份"})
    private String month;

    @ExcelProperty(value = {"报销明细表(报销人)", "报销人"})
    @ColumnWidth(45)
    private String userName;

    @ExcelProperty(value = {"报销明细表(报销人)", "归属项目名称"})
    @ColumnWidth(15)
    private String projectName;

    @ExcelProperty(value = {"报销明细表(报销人)", "归属项目编码"})
    @ColumnWidth(15)
    private String projectCode;

    @ExcelProperty(value = {"报销明细表(报销人)", "差旅费/元"})
    private BigDecimal travel;

    @ExcelProperty(value = {"报销明细表(报销人)", "办公费/元"})
    private BigDecimal office;

    @ExcelProperty(value = {"报销明细表(报销人)", "招待费/元"})
    private BigDecimal entertainment;

    @ExcelProperty(value = {"报销明细表(报销人)", "交通费/元"})
    private BigDecimal transportation;

    @ExcelProperty(value = {"报销明细表(报销人)", "培训费/元"})
    private BigDecimal training;

    @ExcelProperty(value = {"报销明细表(报销人)", "会务费/元"})
    private BigDecimal business;

    @ExcelProperty(value = {"报销明细表(报销人)", "维修费/元"})
    private BigDecimal maintenance;

    @ExcelProperty(value = {"报销明细表(报销人)", "快递费/元"})
    private BigDecimal courier;

    @ExcelProperty(value = {"报销明细表(报销人)", "设备采购费/元"})
    private BigDecimal equipmentProcurement;

    @ExcelProperty(value = {"报销明细表(报销人)", "餐饮费/元"})
    private BigDecimal meals;

    @ExcelProperty(value = {"报销明细表(报销人)", "参展费/元"})
    private BigDecimal exhibition;

    @ExcelProperty(value = {"报销明细表(报销人)", "资质报销费/元"})
    private BigDecimal qualification;

    @ExcelProperty(value = {"报销明细表(报销人)", "考试报名费/元"})
    private BigDecimal exam;

    @ExcelProperty(value = {"报销明细表(报销人)", "投标报名费/元"})
    private BigDecimal bid;

    @ExcelProperty(value = {"报销明细表(报销人)", "其他/元"})
    private BigDecimal other;

    @ExcelProperty(value = {"报销明细表(报销人)", "总计"})
    private BigDecimal total;

}

二、自定义单元格合并处理器
  1. 介绍CellWriteHandler接口

    • CellWriteHandler接口用于在单元格创建和处理的各个阶段执行自定义逻辑。
    • 需要实现的三个主要方法:beforeCellCreateafterCellCreateafterCellDispose
  2. 创建ExcelMergeHandler类并实现CellWriteHandler接口

package com.shy.framework.excel.core.handler;


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.getCellTypeEnum() == 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);
       }
   }
}

三、设置导出样式
  1. 创建ExcelStyleHandler类
    • 介绍ExcelStyleHandler类,用于设置Excel文件的单元格样式。
  2. 实现getHeadStyle方法
    • 设置标题样式,包括字体、边框、对齐方式等。
  3. 实现getContentStyle方法
    • 设置内容样式,包括字体、边框、对齐方式等。
  4. 实现createBaseStyle方法
    • 提取公共样式设置,减少代码重复,提高可维护性。
java复制代码public class ExcelStyleHandler {
    
    /**
     * 创建标题样式
     * @return WriteCellStyle 标题样式
     */
    public static WriteCellStyle getHeadStyle() {
        WriteCellStyle headWriteCellStyle = createBaseStyle();
        
        // 设置标题字体
        WriteFont headWriteFont = new WriteFont();
        headWriteFont.setFontName("宋体");
        headWriteFont.setFontHeightInPoints((short) 14);
        headWriteFont.setBold(true);
        headWriteCellStyle.setWriteFont(headWriteFont);

        return headWriteCellStyle;
    }

    /**
     * 创建内容样式
     * @return WriteCellStyle 内容样式
     */
    public static WriteCellStyle getContentStyle() {
        WriteCellStyle contentWriteCellStyle = createBaseStyle();
        
        // 设置内容字体
        WriteFont contentWriteFont = new WriteFont();
        contentWriteFont.setFontHeightInPoints((short) 12);
        contentWriteFont.setFontName("宋体");
        contentWriteCellStyle.setWriteFont(contentWriteFont);

        return contentWriteCellStyle;
    }

    /**
     * 创建基础样式
     * @return WriteCellStyle 基础样式
     */
    private static WriteCellStyle createBaseStyle() {
        WriteCellStyle writeCellStyle = new WriteCellStyle();

        // 设置边框
        writeCellStyle.setBorderBottom(BorderStyle.THIN);
        writeCellStyle.setBottomBorderColor((short) 0);
        writeCellStyle.setBorderLeft(BorderStyle.THIN);
        writeCellStyle.setLeftBorderColor((short) 0);
        writeCellStyle.setBorderRight(BorderStyle.THIN);
        writeCellStyle.setRightBorderColor((short) 0);
        writeCellStyle.setBorderTop(BorderStyle.THIN);
        writeCellStyle.setTopBorderColor((short) 0);

        // 设置对齐方式
        writeCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        writeCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);

        // 设置其他样式属性
        writeCellStyle.setWrapped(true);
        writeCellStyle.setShrinkToFit(true);

        return writeCellStyle;
    }
}
四、创建ExcelUtils工具类

主要用于存放各类Excel操作工具

package com.shy.framework.excel.core.util;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.shy.framework.excel.core.annotations.ExcelSelected;
import com.shy.framework.excel.core.handler.ExcelMergeHandler;
import com.shy.framework.excel.core.handler.ExcelStyleHandler;
import com.shy.framework.excel.core.selected.CustomSheetWriteHandler;
import com.shy.framework.excel.core.selected.ExcelSelectedResolve;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.*;

/**
 * Excel 工具类
 */
@Slf4j
public class ExcelUtils {

    /**
     * 将列表写入Excel 并合并指定列、行
     *
     * @param response  响应
     * @param filename  文件名
     * @param sheetName Excel sheet 名
     * @param head      Excel head 头
     * @param data      数据列表哦
     * @param <T>       泛型,保证 head 和 data 类型的一致性
     * @param mergeRowIndex 合并开始行(从0开始)
     * @param mergeCols 需要合并的列
     * @throws IOException 写入失败的情况
     */
    public static <T> void mergeWrite(HttpServletResponse response, String filename, String sheetName,
                           Class<T> head, List<T> data, int mergeRowIndex, int[] mergeCols) throws IOException {
        EasyExcel.write(response.getOutputStream(), head)
                .autoCloseStream(Boolean.FALSE)
                // 自动合并列单元格
                .registerWriteHandler(new ExcelMergeHandler(mergeRowIndex, mergeCols))
                // 表格样式
                .registerWriteHandler(new HorizontalCellStyleStrategy(ExcelStyleHandler.getHeadStyle(), ExcelStyleHandler.getContentStyle()))
                .sheet(sheetName).doWrite(data);
        // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
    }

    public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
        return EasyExcel.read(file.getInputStream(), head, null)
                .autoCloseStream(false)  // 不要自动关闭,交给 Servlet 自己处理
                .doReadAllSync();
    }

}

五、调用工具类导出Excel文件

以下一个实际业务中导出Excel文件的接口

    /**
     * 报销明细数据导出
     */
    @GetMapping("/exportReimburseDetailExcel")
    @ApiOperationSupport(order = 4)
    @ApiOperation(value = "报销明细表导出", notes = "报销明细表导出")
    public void exportReimburseDetailExcel(HttpServletResponse response, @Valid ReimburseStatisticsDTO param) throws IOException{
        List<ReimburseDetailVO> result = personExpenseService.exportReimburseDetailExcel(param);
        // 需要合并的列
        int[] cols = {0, 1, 2, 3};
        // 从第二行后开始合并
        int row = 2;
        // 导出报销人维度报销明细表
        ExcelUtils.mergeWrite(response, "报销明细表-报销人.xlsx", "报销明细表",
                ExpenseUserReimburseDetailToExcel.class,
                ReimburseStatisticsConvert.INSTANCE.expenseUserDetailDoToExcel(result),
                row, cols);
    }

Over!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/632773.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL中order by排序时,数据存在null,排序在最前面

order by排序是最常用的功能&#xff0c;但是排序有时会遇到数据为空null的情况&#xff0c;这样排序就会乱了&#xff0c;这里以MySQL为例&#xff0c;记录我遇到的问题和解决思路。 sql 排序为 null 值问题&#xff1a; 排序时我们用 receive_date(一个统计的时间&#xff…

物业水电抄表系统的全面解析

1.系统概述 物业水电抄表系统是现代物业管理中的重要组成部分&#xff0c;它通过自动化的方式&#xff0c;实时监控和记录居民或企业的水电使用情况&#xff0c;极大地提高了工作效率&#xff0c;降低了人工抄表的错误率。该系统通常包括数据采集、数据传输、数据分析和数据展…

python创建新环境并安装pytorch

python创建新环境并安装pytorch 一、创建新环境1、准备工作2、创建虚拟环境并命名3、激活虚拟环境 二、安装pytorch1、pytorch官网2、选择与你的系统相对应的版本3、安装成功 一、创建新环境 1、准备工作 本次创建的环境是在anaconda环境下&#xff0c;否则需要在纯净环境下创…

centOS忘记密码的处理办法

1、开机后在出现内核选项时&#xff0c;按 e&#xff1b; 2、在Linux 开头的这行&#xff0c;输入 rd.break 如下图&#xff1b; 3、然后&#xff0c;执行&#xff1a;CtrlX&#xff1b; 4、进入之后是 switch_root:/#输入 mount -o rw,remount /sysroot 以读写方式重新挂载 /s…

初讲树,二叉数(搜索二叉树,实现的方法<链式,顺序>)

目录 1.树的概念及其结构 1.1树的概念 1.2树相关的概念 1.3树的表示 2.二叉树概念及其结构 2.1概念 2.2现实中的二叉树 2.3特殊的二叉树 2.4二叉树的性质 2.5二叉树存储结构 2.5.1链式存储 2.5.2顺序存储 3.搜索二叉树 1.树的概念及其结构 1.1树的概念 树是一种非…

从零入门激光SLAM(十六)——卡尔曼滤波基础

一、卡尔曼滤波简介KF 卡尔曼滤波器&#xff08;Kalman Filter&#xff09;是一种用于估计动态系统状态的递归算法。它通过结合系统的动态模型和噪声观测数据&#xff0c;提供对系统状态的最优估计。卡尔曼滤波器广泛应用于信号处理、控制系统、导航、计算机视觉等领域。 卡尔…

无人机超强教程!无人机图像拼接、航拍植被动态定量化研究、激光雷达地形测量与河网水系提取

查看原文>>>无人机生态环境监测、图像处理与GIS数据分析综合实践技术应用 目录 一、无人机航拍基本流程、航线规划与飞行实践 二、无人机图像拼接软件的学习与操作实践 三、无人机图像拼接典型案例详解 四、无人机图像拼接数据在GIS中的处理与分析 五、无人机图…

Leaflet【二】图层绘制——UI图层【点线面】 矢量图层【img、svg】

layer图层 在leaflet当中使用图层比OL当中简便一点&#xff0c;我们创建的layer图层可以直接通过 addTo 方法加到地图上&#xff0c;不需要通过layer、source再去做一些区分&#xff0c; 图标 Icon 创建Marker时提供的一个Icon 详细配置–>go // 导入一张图片作为图标imp…

python放烟花的代码

以下是一个简单的Python烟花大会的代码示例。这个代码使用Python的turtle模块来绘制烟花&#xff0c;并使用随机函数来生成烟花的路径和颜色。 python import turtle import random # 设置画布和画笔 canvas turtle.Screen() canvas.bgcolor("black") pen turtle.…

光伏电站设备数据采集

随着全球对可再生能源的关注度日益提升&#xff0c;光伏电站作为绿色能源的重要组成部分&#xff0c;其运营效率和稳定性显得尤为重要。在光伏电站的日常管理中&#xff0c;设备数据采集是一项至关重要的工作&#xff0c;它直接关系到电站的运行状态、故障预警以及能源产出的优…

人工智能创新领衔,Android系统如虎添翼:2024 Google I/O 大会深度解析

人工智能创新领衔&#xff0c;Android系统如虎添翼&#xff1a;2024 Google I/O 大会深度解析 2024年5月14日举行的Google I/O大会&#xff0c;犹如一场精彩的科技盛宴&#xff0c;吸引了全球的目光。大会上&#xff0c;谷歌发布了一系列重磅产品和技术更新&#xff0c;展现了…

揭秘!国产电路仿真软件新星闪耀,让电路设计更智能!

在数字化时代&#xff0c;电路设计与仿真软件的重要性日益凸显。随着科技的飞速发展&#xff0c;国产电路仿真软件也逐渐崭露头角&#xff0c;成为行业内的佼佼者。今天&#xff0c;我们就来揭秘这些国产电路仿真软件的新星&#xff0c;看看它们是如何让电路设计变得更加智能、…

上位机图像处理和嵌入式模块部署(树莓派4b的低成本方案)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们说过树莓派4b的替代版本和提高版本&#xff0c;其实还有一种方案&#xff0c;那就是树莓派4b的超低版本方案。国内开发板soc这块&#xff…

如何看固态硬盘是否支持trim功能?固态硬盘开启trim数据还能恢复吗

随着科技的飞速发展&#xff0c;固态硬盘&#xff08;SSD&#xff09;已成为电脑存储的主流选择。相较于传统的机械硬盘&#xff0c;固态硬盘以其高速读写和优秀的耐用性赢得了广泛好评。而在固态硬盘的众多功能中&#xff0c;TRIM功能尤为关键&#xff0c;它能有效提升固态硬盘…

机器人工具箱学习(三)

一、动力学方程 机器人的动力学公式描述如下&#xff1a; 式中&#xff0c; τ \boldsymbol{\tau} τ表示关节驱动力矩矢量&#xff1b; q , q ˙ , q \boldsymbol{q} ,\; \dot{\boldsymbol { q }} ,\; \ddot{\boldsymbol { q }} q,q˙​,q​分别为广义的关节位置、速度和加速…

Python代码:十二、格式化输出(2)

1、描述 牛牛、牛妹和牛可乐都是Nowcoder的用户&#xff0c;某天Nowcoder的管理员希望将他们的用户名以某种格式进行显示&#xff0c; 现在给定他们三个当中的某一个名字name&#xff0c;请分别按全小写、全大写和首字母大写的方式对name进行格式化输出&#xff08;注&#x…

关于毫、微、纳、皮

千分之一称为“毫”(m)&#xff0c;即10^(-3) “毫”的千分之一称为“微”( μ)&#xff0c;即10^(-6) “微”的千分之一称为“纳”( n)&#xff0c;即10^(-9) “纳”的千分之一称为“皮”( p)&#xff0c;即10^(-12) 另外&#xff1a; 千倍为“千”(K) 千倍的千倍称为“…

Echarts仪表盘实现半球带圆点

效果图&#xff1a; 代码如下&#xff1a; <template><div><!-- 图表 --><div class"echart-box" id"main"></div></div> </template> <script setup> import * as echarts from "echarts"; …

CSP认证刷题笔记(3)最大矩形(13年CSP认证第三题)

文章目录 题目描述基本思路求解代码 题目描述 在横轴上放了n个相邻的矩形&#xff0c;每个矩形的宽度是1&#xff0c;而第i&#xff08;1≤i≤n&#xff09;个矩形的高度是 hi。这n个矩形构成了一个直方图。例如&#xff0c;下图中六个矩形的高度就分别是3,1,6,5,2,3。 请找出…

【面试干货】一个数组的倒序

【面试干货】一个数组的倒序 1、实现思想2、代码实现 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 1、实现思想 创建一个新的数组&#xff0c;然后将原数组的元素按相反的顺序复制到新数组中。 2、代码实现 package csdn;public class…