EasyExcel复杂表头导出(一对多)升级版

news2025/2/24 23:28:02

一、前言

        在之前写的 EasyExcel复杂表头导出(一对多)的博客的结尾,受限于当时的能力和精力,留下一些问题及展望。现在写下此博客,目的就是解决之前遗留的问题。

        背景介绍,见上述链接指向的博客,这里主要通过自定义拦截器的形式来完美解决。

二、导出功能的实现

2.1 Entity 对象

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 com.alibaba.excel.annotation.write.style.HeadStyle;
import com.alibaba.excel.converters.string.StringImageConverter;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.net.URL;

@Data
@EqualsAndHashCode
@HeadRowHeight(30)
@ContentRowHeight(80)
@ColumnWidth(15)
@HeadStyle(fillForegroundColor = 44)
@NoArgsConstructor
@AllArgsConstructor
class Customer {

    @ExcelProperty({"客户编号"})
    private String userCode;

    @ExcelProperty({"客户名称"})
    private String userName;

    @ColumnWidth(25)
    @ExcelProperty({"客户所在地址"})
    private String address;

    @ExcelProperty({"联系人信息", "联系人姓名"})
    private String personName;

    @ExcelProperty({"联系人信息", "联系电话"})
    private String telephone;

    @ExcelProperty({"图片"})
    private URL picture;

    /**
     * 你也可以通过字符串的形式来保存图片,具体说明见注意事项3.1
     */
    //@ExcelProperty(converter = StringImageConverter.class, value = {"本地图片"})
    //private String localPic;
}

2.2 Controller 层

@PostMapping("/exportExcel")
@ApiOperation("导出Excel")
public void exportExcel(HttpServletResponse response) throws Exception {
	// 查询需要导出的数据
	List result = getData();

	// 1设置表头样式
	WriteCellStyle headStyle = new WriteCellStyle();
	// 1.1设置表头数据居中
	headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);

	// 2设置表格内容样式
	WriteCellStyle bodyStyle = new WriteCellStyle();
	// 2.1设置表格内容水平居中
	bodyStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
	// 2.2设置表格内容垂直居中
	bodyStyle.setVerticalAlignment(VerticalAlignment.CENTER);

	// 3设置表格sheet样式
	WriteSheet sheet = EasyExcel.writerSheet("客户信息").head(Customer.class).sheetNo(1).build();
	// 4拿到表格处理对象
	ExcelWriter writer = EasyExcel.write(response.getOutputStream()).needHead(true).excelType(ExcelTypeEnum.XLSX)
            // 设置需要待合并的行和列。参数1:数值数组,指定需要合并的列;参数2:数值,指定从第几行开始合并
			.registerWriteHandler(new ExcelMergeCellHandler(new int[]{0, 1, 2, 5}, 0))
            // 设置单元格的风格样式
			.registerWriteHandler(new HorizontalCellStyleStrategy(headStyle, bodyStyle))
			.build();

	// 5写入excel数据
	writer.write(result, sheet);
	// 6通知浏览器以附件的形式下载处理,设置返回头要注意文件名有中文
	response.setHeader("Content-disposition", "attachment;filename=" + new String("客户信息表".getBytes("gb2312"), "ISO8859-1") + ".xlsx");
	response.setContentType("multipart/form-data");
	response.setCharacterEncoding("utf-8");
	writer.finish();
}

2.3 自定义拦截器(ExcelMergeCellHandler)

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 lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.List;

/**
 * @author DaHuaJia
 * @Description 自定义单元格合并处理Handler类
 * @Date 2022-08-18 19:25:58
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExcelMergeCellHandler implements CellWriteHandler {
    // 需要合并的列,从0开始算
    private int[] mergeColIndex;
    // 从指定的行开始合并,从0开始算
    private int mergeRowIndex;

    /**
     * 在单元格上的所有操作完成后调用,遍历每一个单元格,判断是否需要向上合并
     */
    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        // 获取当前单元格行下标
        int currRowIndex = cell.getRowIndex();
        // 获取当前单元格列下标
        int currColIndex = cell.getColumnIndex();
        // 判断是否大于指定行下标,如果大于则判断列是否也在指定的需要的合并单元列集合中
        if (currRowIndex > mergeRowIndex) {
            for (int i = 0; i < mergeColIndex.length; i++) {
                if (currColIndex == mergeColIndex[i]) {
                    /**
                     * 获取列表数据的唯一标识。不同集合的数据即使数值相同也不合并
                     * 注意:我这里的唯一标识为客户编号(Customer.userCode),在第一列,即下标为0。大家需要结合业务逻辑来做修改
                     */
                    // 获取当前单元格所在的行数据的唯一标识
                    Object currCode = cell.getRow().getCell(0).getStringCellValue();
                    // 获取当前单元格的正上方的单元格所在的行数据的唯一标识
                    Object preCode = cell.getSheet().getRow(currRowIndex - 1).getCell(0).getStringCellValue();
                    // 判断两条数据的是否是同一集合,只有同一集合的数据才能合并单元格
                    if(preCode.equals(currCode)){
                        // 如果都符合条件,则向上合并单元格
                        mergeWithPrevRow(writeSheetHolder, cell, currRowIndex, currColIndex);
                        break;
                    }
                }
            }
        }
    }

    /**
     * 当前单元格向上合并
     *
     * @param writeSheetHolder 表格处理句柄
     * @param cell             当前单元格
     * @param currRowIndex     当前行
     * @param currColIndex     当前列
     */
    private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int currRowIndex, int currColIndex) {
        // 获取当前单元格数值
        Object currData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
        // 获取当前单元格正上方的单元格对象
        Cell preCell = cell.getSheet().getRow(currRowIndex - 1).getCell(currColIndex);
        // 获取当前单元格正上方的单元格的数值
        Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();

        // 将当前单元格数值与其正上方单元格的数值比较
        if (preData.equals(currData)) {
            Sheet sheet = writeSheetHolder.getSheet();
            List<CellRangeAddress> mergeRegions = sheet.getMergedRegions();
            // 当前单元格的正上方单元格是否是已合并单元格
            boolean isMerged = false;
            for (int i = 0; i < mergeRegions.size() && !isMerged; i++) {
                CellRangeAddress address = mergeRegions.get(i);
                // 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
                if (address.isInRange(currRowIndex - 1, currColIndex)) {
                    sheet.removeMergedRegion(i);
                    address.setLastRow(currRowIndex);
                    sheet.addMergedRegion(address);
                    isMerged = true;
                }
            }
            // 若上一个单元格未被合并,则新增合并单元
            if (!isMerged) {
                CellRangeAddress cellRangeAddress = new CellRangeAddress(currRowIndex - 1, currRowIndex, currColIndex, currColIndex);
                sheet.addMergedRegion(cellRangeAddress);
            }
        }
    }
}

2.4 getDate方法(用于模拟service层拿到的数据)

public static List<Customer> getData() throws Exception {
	List<Customer> data = new ArrayList<>();

	Customer customer = new Customer("JiangXi", "江西电信公司", "江西省南昌市东湖区", "张三", "12345678910", new URL("https://m.360buyimg.com/babel/jfs/t1/221733/11/14107/61280/62fde84dE467522ce/79bbd42aa93f5a83.jpg"));
	data.add(customer);
	Customer customer2 = new Customer("JiangXi", "江西电信公司", "江西省南昌市东湖区", "李四", "15848563521", new URL("https://m.360buyimg.com/babel/jfs/t1/221733/11/14107/61280/62fde84dE467522ce/79bbd42aa93f5a83.jpg"));
	data.add(customer2);

	Customer customer3 = new Customer("GuangDong", "广东电信公司", "广东省广州市花都区", "小明", "15847953624", new URL("https://m.360buyimg.com/babel/jfs/t1/215924/36/19623/23344/62baa985E4df523c6/4893237860b306d6.jpg"));
	data.add(customer3);
	Customer customer4 = new Customer("GuangDong", "广东电信公司", "广东省广州市天河区", "小红", "16849531548", new URL("https://m.360buyimg.com/babel/jfs/t1/189640/15/26493/35837/62baa97eE6abda209/461f91e682d0e81a.jpg"));
	data.add(customer4);
	Customer customer5 = new Customer("GuangDong", "广东电信公司", "广东省广州市天河区", "小华", "16985632481", new URL("https://m.360buyimg.com/babel/jfs/t1/189640/15/26493/35837/62baa97eE6abda209/461f91e682d0e81a.jpg"));
	data.add(customer5);

	Customer customer6 = new Customer("BeiJing", "北京电信公司", "北京市东城区", "姜维", "16598645874", new URL("https://m.360buyimg.com/babel/jfs/t1/31481/11/16081/24873/62baa97dE6f3991d0/94ae13d66b9bbfdd.jpg"));
	data.add(customer6);

	return data;
}

2.5 效果

 三、注意事项

3.1 图片导出问题

对于图片的导出,其字段可以有多种数据类型,官网就介绍了5种(File、InputStream、String、byte[]、URL、WriteCellData<Void>)。这里简要介绍一下String 和 URL。

1、String 类型

/**
*  如果图片地址通过String类型保存,则需要加一个自带的类型转换器(StringImageConverter)
*/
@ExcelProperty(converter = StringImageConverter.class, value = {"本地图片"})
private String localPic;

2、URL类型 

@ExcelProperty({"网络图片"})
private URL picture;

        经过测试发现,String类型只能保存本地图片地址,如果保存网络图片地址,则会导致图片无法下载。原因则是EasyExcel会把“//” 转换成 “\”,导致地址错误。

        因此,可以约定String类型用于保存本地图片地址,URL类型用于保存网络图片地址。

3.2 图片单元格合并问题

        图片类型单元格无法做到相同的图片合并单元格,主要是因为无法通过单元格对象拿到图片的序列化值。

3.3 表格样式

        表格的样式既可以表格样式类(例如:WriteCellStyle)来设置,也可以通过注解(例如:@HeadStyle)来设置,两者互补,不冲突。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue3 route和router的区别以及如何传参数接受参数,如何像vue2一样使用$route和$router详解

vue3 route和router的区别以及如何传参数接受参数&#xff0c;如何像vue2一样使用$route和$router详解 因为我们在 setup 里面没有访问 this&#xff0c;所以我们不能再直接访问 this.$router 或 this.$route。作为替代&#xff0c;我们使用 useRouter 和useRoute函数,或者 Vu…

HTML小游戏21 —— html5版暴打皮卡丘游戏(附完整源码)

&#x1f482; 网站推荐:【神级源码资源网】【摸鱼小游戏】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 想寻找共同学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】本节教程我会带大家使用 HTML…

微信小程序--基础内容(详解)(一)

一、常用标签 1、view 标签 view 标签是一个块级元素&#xff0c;类似于 div&#xff08;小程序里面没有div标签&#xff09;&#xff0c;里面可以放任何内容或者插值表达式&#xff0c;如下所示&#xff1a; <view>这是view标签<view> <view>{{num}}<…

谷歌新版本跨域错误深度剖析与解决:request client is not a secure context and the resource is in more-private address

快速解决&#xff1a; 最近在测试http服务时&#xff0c;谷歌浏览器报了以下错误 “The request client is not a secure context and the resource is in more-private address space ‘local’”. 从报错信息来看&#xff0c;“不安全的请求方请求了更私有的本地资源” 对于…

API接口开发其实特简单,Python Flask Web 框架教程来了

大家好&#xff0c;日常工作中&#xff0c;无论你是数据工程师、数据挖掘工程师&#xff0c;甚至数据分析人员&#xff0c;都不可避免的与他人进行数据交互&#xff0c;API接口提供数据是最常见的形式。 今天我给大家分享 Python Flask Web 框架教程&#xff0c;共计10个部分&…

Vue面试题你知道多少

✅作者简介&#xff1a;大家好我是hacker707,大家可以叫我hacker&#xff0c;新星计划第三季python赛道Top1&#x1f3c6; &#x1f4c3;个人主页&#xff1a;hacker707的csdn博客 &#x1f525;系列专栏&#xff1a;带你玩转Vue &#x1f4ac;推荐一款模拟面试、刷题神器&…

前端401错误 解决方法:响应拦截器

目录 1.该问题出现的原因 2.处理401问题的解决方案原理 3.使用响应拦截器解决问题 1.该问题出现的原因 在前后端分离项目中&#xff0c;最常见的是前端点击登录后&#xff0c;后端返回token字符串&#xff0c;这个token可以看作是一个“令牌”&#xff0c;就比如你去酒店办理…

Vue实战【调整Vue-element-admin中的菜单栏,并添加顶部模块菜单栏】

目录&#x1f31f;前言&#x1f31f;小伙伴们先看&#x1f31f;实现思路&#x1f31f;具体代码&#x1f31f;最后&#x1f31f;前言 因为最近在整合公司的项目&#xff0c;需要把所有系统里的功能集成到一个项目里&#xff0c;这样就导致菜单栏目录会特别的多&#xff0c;不便…

【JavaScript】手撕前端面试题:事件委托 | 判断URL是否合法 | 全排列

&#x1f5a5;️ NodeJS专栏&#xff1a;Node.js从入门到精通 &#x1f5a5;️ 博主的前端之路&#xff08;源创征文一等奖作品&#xff09;&#xff1a;前端之行&#xff0c;任重道远&#xff08;来自大三学长的万字自述&#xff09; &#x1f5a5;️ TypeScript知识总结&…

【Axure】Axure RP 9下载、安装、授权、汉化

目录一、Axure RP 9 下载二、Axure RP 9 安装三、Axure RP 9 授权四、Axure RP 9 汉化一、Axure RP 9 下载 1、Axure RP 9 下载地址&#xff1a;https://www.axure.com/release-history/rp9 2、其他版本下载地址 ①登录axure官网:https://www.axure.com/ ②拉到最下面找到相关…

很好看的爱心表白代码(动态)

分享几个好看的爱心表白代码❤️爱心代码❤️&#xff08;C语言&#xff09;❤️流动爱心❤️&#xff08;htmlcssjs&#xff09;❤️线条爱心❤️&#xff08;htmlcssjs&#xff09;❤️biu表白爱心❤️&#xff08;htmlcssjs&#xff09;❤️matlab爱心函数❤️&#xff08;需…

Vue3+TS+Vite 入门指南

最近尝试上手 Vue3TSVite&#xff0c;对比起 Vue2 有些不适应&#xff0c;但还是真香~ 上手前先说下 Vue3 的一些变化吧~ Vue3 的变化 Vue3 带来的变化主要有以下几个方面&#xff1a; 使用层面 对比起 Vue2 启动速度快很多&#xff0c;新项目从 1s 升级到不到 500msvite.co…

Element-UI新手学习记录(一)

Layout 布局 通过基础的 24 分栏&#xff0c;迅速简便地创建布局。 span的作用 一行默认24个span&#xff0c;属性放在el-col中决定此元素占据多少span gutter属性 放在el-row中&#xff0c;给各个块之前设置间隔&#xff0c;但是是割的代码块的宽度。 offset属性 放在el…

小程序页面之间数据传递的四种方法

近期再使用小程序开发的时候遇到小程序页面和页面之间的数据传递问题。总结一下大致有以下几种方式实现页面数据传递。 最常见的就是路由传参&#xff0c;使用场景主要是页面汇总的少量数据的传递。以下都以Tarovue示例&#xff0c;原生、react或者uniapp同理&#xff0c;替换…

Pinia(二)了解和使用Store

Store Store 是保存状态(state)和业务逻辑的实体, store 不应该与我们的组件绑定. 换句话说, store 就是全局状态.store 有三个关键概念, 分别是 state, getters 和 actions, 这与 Vue 组件中的 data, computed 和 methods 是相对应的概念. 定义 store 通过 defineStore 函数…

Vue页面路由参数的传递和获取

文章目录1. 通过动态路由参数传递2. 通过query字符串传递3. props 隐式传递vue 页面路由切换时传参的方式有如下几种&#xff1a; 动态路由参数 它隐藏字段信息&#xff0c;相对于来说较安全&#xff0c;同时地址栏中的地址也相对较短 它必须是先定义后使用&#xff0c;一般用…

关于嵌套使用 iFrame 出现 Refused to display in aframe 拒绝连接访问 和 ‘X-Frame-Options‘ to ‘SAMEORIGIN‘ 的解决方案【已解决】

目录问题描述原因分析问题解决总结今天在迁移旧项目时&#xff0c;出现了如下错误提示&#xff1a; Refused to display in a frame because it set X-Frame-Options to SAMEORIGIN问题描述 当前项目是一个生产环境正常运行的项目&#xff0c;由于我们要迁移服务器并且部署 k…

Pro2:修改div块的颜色

什么是JavaScript&#xff1f;实现目标实现代码实现效果实现方法&#x1f49b;作者主页&#xff1a;静Yu &#x1f9e1;简介&#xff1a;CSDN全栈优质创作者、华为云享专家、阿里云社区博客专家&#xff0c;前端知识交流社区创建者 &#x1f49b;社区地址&#xff1a;前端知识交…

html里面使用axios发送请求

html里面使用axios 效果展示&#xff1a; 代码展示&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name…

Vue项目实战 —— 后台管理系统( pc端 ) —— Pro最终版本

前期回顾 开源项目 —— 原生JS实现斗地主游戏 ——代码极少、功能都有、直接粘贴即用_js斗地主_0.活在风浪里的博客-CSDN博客JS 实现 斗地主网页游戏https://blog.csdn.net/m0_57904695/article/details/128982118?spm1001.2014.3001.5501 通用版后台管理系统&#xff0c;如果…