基于 easyExcel 3.1.5依赖的包 实现动态表头 动态表格内容

1.需求:需要导出的EXCEL示例:

2.依赖:

java 复制代码
<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.1.5</version>
        </dependency>

3.工具类:

java 复制代码
package com.minex.web.device.utils;

import com.minex.web.device.entity.vo.ReadRecordsVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Component
public class ExcelExporter {

    private static final String DEFAULT_SHEET_NAME = "Sheet1";

    public void export(HttpServletResponse response, Map<String, List<ReadRecordsVO>> sheetDataMap,String fileName) throws IOException {
        try (Workbook workbook = new XSSFWorkbook()) {
            // 创建并写入多个Sheet页
//            Map<String, List<ReadRecordsVO>> sheetDataMap = splitDataIntoSheets(data);
            for (String sheetName : sheetDataMap.keySet()) {
                Sheet sheet = createSheet(workbook, sheetName);
                writeDataToSheet(sheet, sheetDataMap.get(sheetName));
            }

            // 设置响应头以触发浏览器下载
            String utf8EncodedFilename = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
            response.setHeader("Content-Disposition",
                    "attachment; filename*=utf-8''" + utf8EncodedFilename+".xlsx");
            // 响应类型,编码
            response.setContentType("application/vnd.ms-excel;charset=utf-8");
            response.setCharacterEncoding("utf-8");
            // 将工作簿写入响应输出流
            try (OutputStream os = response.getOutputStream()) {
                workbook.write(os);
            }
        }
    }

    private Sheet createSheet(Workbook workbook, String sheetName) {
        return workbook.createSheet(sheetName);
    }

    private void writeDataToSheet(Sheet sheet, List<ReadRecordsVO> data) {
        // 获取所有列信息和它们对应的父级表头
        Map<String, String> headerToParentHeaderMap = getHeadersWithParent(data);
        // 写入表头
        Row headerRow = sheet.createRow(0);
        Row subHeaderRow = sheet.createRow(1);
        int columnIndex = 0;
        Map<String, Integer> startColumnIndexMap = new HashMap<>();
        Map<String, Integer> endColumnIndexMap = new HashMap<>();
        for (String header : headerToParentHeaderMap.keySet()) {
            String parentHeader = headerToParentHeaderMap.get(header);
            // 记录每个父级表头的起始列索引
            startColumnIndexMap.putIfAbsent(parentHeader, columnIndex);
            Cell cell = subHeaderRow.createCell(columnIndex++);
            cell.setCellValue(header);
            // 更新每个父级表头的最后一列索引
            endColumnIndexMap.put(parentHeader, columnIndex - 1);
        }
        // 合并第一行相同父级表头的单元格
        mergeCells(sheet, startColumnIndexMap, endColumnIndexMap);
        // 写入父级表头(第一行)
        columnIndex = 0;
        for (String parentHeader : startColumnIndexMap.keySet()) {
            Cell cell = headerRow.createCell(startColumnIndexMap.get(parentHeader));
            cell.setCellValue(parentHeader);
        }
        // 写入数据行
        writeDataRow(sheet, data, headerToParentHeaderMap.keySet(), 2);
        // 自动调整列宽
        autoSizeColumns(sheet, headerToParentHeaderMap.size());
    }

    private void writeDataRow(Sheet sheet, List<ReadRecordsVO> data, Set<String> allHeaders, int rowIndex) {
        for (ReadRecordsVO record : data) {
            Row row = sheet.createRow(rowIndex++);
            int columnIndex = 0;
            for (String header : allHeaders) {
                Cell cell = row.createCell(columnIndex++);
                record.getRecordItems().stream()
                        .filter(item -> item.getHeader().equals(header))
                        .findFirst()
                        .ifPresentOrElse(
                                item -> cell.setCellValue(item.getValue()),
                                () -> cell.setCellValue("")
                        );
            }
        }
    }

    private void autoSizeColumns(Sheet sheet, int columnCount) {
        for (int i = 0; i < columnCount; i++) {
            sheet.autoSizeColumn(i);
        }
    }

    private Map<String, String> getHeadersWithParent(List<ReadRecordsVO> data) {
        Map<String, String> headerToParentHeaderMap = new LinkedHashMap<>();
        for (ReadRecordsVO record : data) {
            for (ReadRecordsVO.RecordItem item : record.getRecordItems()) {
                headerToParentHeaderMap.putIfAbsent(item.getHeader(), item.getParentHeader());
            }
        }
        return headerToParentHeaderMap;
    }

    private void mergeCells(Sheet sheet, Map<String, Integer> startColumnIndexMap, Map<String, Integer> endColumnIndexMap) {
        Set<String> mergedParents = new HashSet<>();
        for (String parentHeader : startColumnIndexMap.keySet()) {
            if (!mergedParents.contains(parentHeader)) {
                int startColumnIndex = startColumnIndexMap.get(parentHeader);
                int endColumnIndex = endColumnIndexMap.getOrDefault(parentHeader, startColumnIndex);
                if (startColumnIndex != endColumnIndex) {
                    sheet.addMergedRegion(new CellRangeAddress(0, 0, startColumnIndex, endColumnIndex));
                }
                mergedParents.add(parentHeader);
            }
        }
    }

    private Map<String, List<ReadRecordsVO>> splitDataIntoSheets(List<ReadRecordsVO> data) {
        // 根据业务逻辑分割数据到不同的Sheet页中
        // 这里简单地将所有数据放入一个名为DEFAULT_SHEET_NAME的Sheet页中
        Map<String, List<ReadRecordsVO>> result = new HashMap<>();

        result.put(DEFAULT_SHEET_NAME, data);
        return result;
    }
}

4.controller调用工具类导出

java 复制代码
@PostMapping("/export")
    public void export(@RequestBody SubstationMeterReadRecordListRO query,
                                                         HttpServletResponse response,
                                                         @ApiIgnore CurrentUser user) throws IOException {
        List<ReadRecordsVO> dataList = substationMeterReadRecordService.readRecords(query, user);
        if (dataList.isEmpty()) {
            throw ExceptionFactory.warnBizException("没有数据可导出");
        }
        Map<String, List<ReadRecordsVO>> listMap = dataList.stream().collect(Collectors.groupingBy(ReadRecordsVO::getRecordDate));
        excelExporter.export(response, listMap, dataList.get(0).getSubstationName() + "抄表记录");
    }

5.数据类

java 复制代码
package com.minex.web.device.entity.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;
import java.util.List;

@Data
public class ReadRecordsVO {

    private String substationName;

    private String recordDate;

    private List<RecordItem> recordItems;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class RecordItem {

        private Integer order = 0;

        private String parentHeader;

        private String header;

        private String value;
    }
}

6.运行效果

相关推荐
Sheep Shaun1 小时前
深入理解AVL树:从概念到完整C++实现详解
服务器·开发语言·数据结构·c++·后端·算法
帆张芳显1 小时前
智表zcell产品V3.5 版发布,新增行列选中操作等功能
前端·javascript·excel·插件·canva可画
leiming61 小时前
C语言联合体union的用法(非常详细,附带示例)
java·python·算法
颢珂智库Haokir Insights1 小时前
如何把 MCP 接入到文档 / Issue / CI,形成可复用的工程外脑
服务器·人工智能·ai编程·vllm·vibecoding
程序员小寒1 小时前
前端性能优化之首屏时间采集篇
前端·性能优化
a程序小傲1 小时前
Maven 4 要来了:15 年后,Java 构建工具迎来“彻底重构”
java·开发语言·spring boot·后端·spring·重构·maven
GGGG寄了1 小时前
HTML——div和span标签和字符实体
前端·html
牧小七1 小时前
java Records 怎么使用
java
这儿有一堆花1 小时前
网页开发的基石:深入解析 HTML 与 PHP 的本质差异
前端·html·php
RFCEO1 小时前
网页编程 课程三、:HTML 核心规范(语义化+易维护)详解课程
前端·html·语义化·html核心规范·易维护·前端基础课