EasyExcel - table写入复杂表头及内容

需求:在一个工作簿中,需要填充固定字段信息,并写入多个不同的标题列的表格及内容。
常规Excel写入一般是一个工作簿一个表头。

一、复杂表单分析

1.表单示例

复杂表单示例1

2.复杂表单拆解

复杂表单示例拆解
示例的模板,可以拆解为6个组成部分:
(1)1-7行:表格固定的部分,需要在指定的单元格动态填充信息
(2)8-10行:表格动态写入的部分,由【工单器材】的字段列标题和内容组成。
(3)11-13行:表格动态写入的部分,由【工单服务】的字段列标题和内容组成。
(4)14行:表格动态写入的部分,由【工单结论】的字段列标题组成
(5)15-16行:表格动态写入的部分,由【费用计算】的字段列标题组成
(6)17-19行:表格固定的部分,但是因为无法往表格中间插入table,所以此处只能将固定的格式数据以table方法写入,由【表格固定结尾】的字段内容组成。

3.准备模板

因为表格的前7行是固定格式的,为了简化操作,我们直接将格式定好作为模板。而余下的行是根据数据量动态写入的。
那么,我们在代码中需要对该模板进行填充写入的操作。
在这里插入图片描述

二、EasyExcel文档

有了以上思路,开始翻阅easyExcel文档,查找可以使用的方法。最终确定使用以下两个方法去实现。

1.最简单的填充Excel

用于填充固定模板中的指定字段。
在这里插入图片描述

2.使用table去写入

用于写入动态的标题和内容。
在这里插入图片描述

三、代码示例

1.实体类

工单模板的信息来源于工单信息、工单申请信息、工单设备申请明细、工单服务申请明细表。

1)工单信息表

/**
 * 工单信息表
 */
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Data
public class WorkOrderInfo implements Serializable {

	// 工单ID
    private Long workOrderId;

	// 工单号码
    private String workOrderCode;

    // 发起人名称
    private String creatorName;

    // 处理人名称
    private String handerName;

    // 工单标题
    private String workOrderTitle;
    
	// 工单内容
    private String workOrderContent;

   // 工单结论
    private String workOrderResult;
}

2)工单申请信息表

/**
 * 工单申请信息表
 */
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Data
public class WorkOrderApplyInfo implements Serializable {

	// 工单申请id
    private Long workOrderApplyId;

	// 工单id
    private Long workOrderId;
    
 	// 工单上门次数
    private String visitNum;
    
	// 工单单次支付(元)
    private String singlePay;
    
 	// 工单总计费用(元)
    private String totalPay;

    // 设备申请明细
    private List<WorkOrderDevice> deviceList;

    // 服务申请明细
    private List<WorkOrderService> serviceList;
}

3)工单设备申请明细表

/**
 * 工单设备申请明细表
 */
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Data
public class WorkOrderDevice implements Serializable {

	// 工单设备申请明细id
    private Long workOrderDeviceId;

	// 工单id
    private Long workOrderId;
    
	// 工单申请id
    private Long workOrderApplyId;
    
 	// 设备名称
    private String deviceName;
    
	// 设备明数
    private String deviceContent;
    
 	// 申请数量
    private String applyNum;
}

4)工单服务申请表

/**
 * 工单服务申请明细表
 */
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Data
public class WorkOrderService implements Serializable {

	// 工单设备申请明细id
    private Long workOrderServiceId;

	// 工单id
    private Long workOrderId;
    
	// 工单申请id
    private Long workOrderApplyId;
    
 	// 服务名称
    private String serviceName;
    
	// 服务描述
    private String serviceContent;
    
 	// 申请工时(时)
    private String hourNum;
}

2.工具类

1)内容导出工具类

因为该模板中不同的table,标题不同,占用合并的列数对应字段也不同。所以此处将设备、服务、工单结论、费用合计、固定结尾的【标题】、【字段映射列及列数】分别单独设置。最后通过【字段映射列及列数】和实际数据,构造【内容】列。

tips:
excel写入实际上是一行一行的写入数据
一行的数据结构是List<String>
多行的数据结构是List<List<String>>
/**
 * 工单确认单内容导出工具类
 */
@Component
@Slf4j
public class WorkOrderDataUtils {

	// >>>>>>>>>>>>>>> 标题构造 >>>>>>>>>>>>>>>>>>>>>>>>>>>
    /**
     * 设备列表标题
     * @return
     */
    public static List<List<String>> deviceHead() {
        List<List<String>> list = new ArrayList<List<String>>();
        List<String> head0 = new ArrayList<String>();
        head0.add("工单器材");
        // 占用3列
        list.add(head0);
        list.add(head0);
        list.add(head0);

        List<String> head1 = new ArrayList<String>();
        head1.add("器材描述");
        // 占用2列
        list.add(head1);
        list.add(head1);

        List<String> head2 = new ArrayList<String>();
        head2.add("数量");
        // 占用两列
        list.add(head2);
        list.add(head2);
        return list;
    }

	/**
     * 服务列表标题
     * @return
     */
    public static List<List<String>> serviceHead() {
        List<List<String>> list = new ArrayList<List<String>>();
        List<String> head0 = new ArrayList<String>();
        head0.add("工单服务");
        // 占用3列
        list.add(head0);
        list.add(head0);
        list.add(head0);

        List<String> head1 = new ArrayList<String>();
        head1.add("服务描述");
        // 占用2列
        list.add(head1);
        list.add(head1);

        List<String> head2 = new ArrayList<String>();
        head2.add("工时");
        // 占用两列
        list.add(head2);
        list.add(head2);
        return list;
    }

	/**
     * 工单结论标题:
     * 将结论传入作为标题
     * @return
     */
    public static List<List<String>> workOrderResultHead(String workOrderResult) {
        List<List<String>> list = new ArrayList<List<String>>();
        List<String> head0 = new ArrayList<String>();
        head0.add("工单结论");
        // 占用3列
        list.add(head0);
        list.add(head0);
        list.add(head0);

        List<String> head1 = new ArrayList<String>();
        head1.add(workOrderResult);
        // 占用4列
        list.add(head1);
        list.add(head1);
        list.add(head1);
        list.add(head1);

        return list;
    }
	/**
     * 费用合计标题
     * 将上门次数,每次支付(元),总费用传入作为标题
     * @return
     */
    public static List<List<String>> totalHead(String visitNum, String singlePay, String totalPay) {
        List<List<String>> list = new ArrayList<List<String>>();
        List<String> head0 = new ArrayList<String>();
        head0.add("上门次数");
        head0.add(visitNum);

        // 占用3列
        list.add(head0);
        list.add(head0);
        list.add(head0);

        List<String> head1 = new ArrayList<String>();
        head1.add("每次支付(元)");
        head1.add(singlePay);
        // 占用2列
        list.add(head1);
        list.add(head1);

        List<String> head2 = new ArrayList<String>();
        head2.add("总费用");
        head2.add(totalPay);
        // 占用两列
        list.add(head2);
        list.add(head2);
        return list;
    }
    
 // >>>>>>>>>>>>>>> 结尾固定内容 >>>>>>>>>>>>>>>>>>>>>>>>>>>
    /**
     * 结尾固定内容
     * @return
     */
    public static List<List<Object>> getFinalData(String content) {
        List<List<Object>> list = new ArrayList<List<Object>>();
        List<Object> row0 = new ArrayList<Object>();
        row0.add(content);
        row0.add(content);
        row0.add(content);
        row0.add(content);
        row0.add(content);
        row0.add(content);
        row0.add(content);

        List<Object> row5 = new ArrayList<Object>();
        row5.add("发起人签字");
        row5.add("");
        row5.add("");
        row5.add("");
        row5.add("处理人签字");
        row5.add("");
        row5.add("");

        list.add(row0);
        list.add(row0);
        list.add(row5);
        return list;
    }
    
 // >>>>>>>>>>>>>> 内容构造 >>>>>>>>>>>>>>>>>>>>>>>>>>>>
 /**
     * 设备字段映射及占用列数
     * @return
     */
    public static LinkedHashMap<String, Integer> fieldRowNumMapByDevice() {
        LinkedHashMap<String, Integer> fieldRowNumMap = new LinkedHashMap<>();
        // 器材名称
        fieldRowNumMap.put("deviceName", 3);
        // 器材描述
        fieldRowNumMap.put("deviceContent", 2);
        // 申请数量
        fieldRowNumMap.put("applyNum", 2);
        return fieldRowNumMap;
    }
    
 /**
     * 服务字段映射及占用列数
     * @return
     */
    public static LinkedHashMap<String, Integer> fieldRowNumMapByDevice() {
        LinkedHashMap<String, Integer> fieldRowNumMap = new LinkedHashMap<>();
        // 服务名称
        fieldRowNumMap.put("serviceName", 3);
        // 服务描述
        fieldRowNumMap.put("serviceContent", 2);
        // 服务工时
        fieldRowNumMap.put("hourName", 2);
        return fieldRowNumMap;
    }
    
// >>>>>>>>>>>>>> 内容构造 >>>>>>>>>>>>>>>>>>>>>>>>>>>>

    /**
     * 设备/服务列表内容构造
     * 通过反射获取对应字段的内容值
     * @param detailsList 设备/服务列表
     * @param fieldRowNumMap 对应的字段及列数对象
     * @return
     */
    public static List<List<Object>> contentRowData(List<?> applyList, Class<?> clazz
            , LinkedHashMap<String, Integer> fieldRowNumMap) throws NoSuchFieldException, IllegalAccessException {

        List<List<Object>> list = new ArrayList<List<Object>>();

        for (BusinessInternalDetailsExcelVO deviceDTO : detailsList) {

            List<Object> rown = new ArrayList<Object>();


            // 防止物料名称和规格列内容相同合并
            String previouFieldContent = "";

            // {"key":"productName", value:"3"} 字段和对应设置的个数,后续设置了相同内容合并的策略
            for (Map.Entry <String, Integer>  entry : fieldRowNumMap.entrySet()) {
                String fieldName = entry.getKey();
                Integer fieldRowNum = entry.getValue();

                // 通过反射,根据字段名获取该对象中的字段值
                Field declaredField = clazz.class.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                String fieldContent = " ";
                if (declaredField.get(deviceDTO) != null) {
                    // 当为空字符串时,不赋值
                    if (!"".equals(declaredField.get(deviceDTO))) {
                        fieldContent = declaredField.get(deviceDTO).toString();
                    }

                    // 防止相邻两列的内容相同合并
                    if (fieldContent.equals(previouFieldContent)) {
                        fieldContent = " " + fieldContent;
                    }
                }
                previouFieldContent = fieldContent;

                for (int i=0; i < fieldRowNum; i++) {
                    rown.add(fieldContent);
                }
            }

            list.add(rown);
        }
        return list;
    }

// >>>>>>>>>>>>>> 生成模板 >>>>>>>>>>>>>>>>>>>>>>>>>>>>

    /**
     * 工单确认单-生成模板
     */
    public static void generateWorkOrderTemplate(ExcelWriter excelWriter, WriteSheet writeSheet
            , WorkOrderApplyInfo orderApply) throws IOException {
        
        try {

            // table集成sheet配置(有/无头),此处多table需要各自的表头
            // 设备列表
            WriteTable writeTable0 = EasyExcel.writerTable(0).needHead(Boolean.TRUE).build();
            // 服务列表
            WriteTable writeTable1 = EasyExcel.writerTable(1).needHead(Boolean.TRUE).build();
            // 工单结论列
            WriteTable writeTable2 = EasyExcel.writerTable(2).needHead(Boolean.TRUE).build();
            // 费用合计
            WriteTable writeTableTotal = EasyExcel.writerTable(3).needHead(Boolean.TRUE).build();
            // 固定内容部分(备注)
            WriteTable writeTableFin = EasyExcel.writerTable(4).needHead(Boolean.FALSE).build();

            // 设备table写入头和内容
            writeTable0.setHead(this.deviceHead());
            excelWriter.write(contentRowData(orderApply.getDeviceList(), WorkOrderDevice.class, this.fieldRowNumMapByDevice())
                    , writeSheet, writeTable0);

            // 服务 table写入头和内容
            writeTable1.setHead(this.serviceHead());
            excelWriter.write(contentRowData(this.getServiceList(), WorkOrderService.class, this.fieldRowNumMapByService())
                    , writeSheet, writeTable1);

            // 工单结论 table仅写入头
            writeTable2.setHead(this.workOrderResultHead( orderApply.getWorkOrderResult()));
            excelWriter.write(new ArrayList<>(), writeSheet, writeTable2);


            // 合计 table仅写入头
            writeTableTotal.setHead(this.workOrderResultHead(orderApply.getVisitNum(), orderApply.getWorkOrderResult(), orderApply.getSinglePay(), orderApply.getTotalPay()));
            excelWriter.write(new ArrayList<>(), writeSheet, writeTableTotal);

            // 固定内容备注+签名行不创建头,在合计table之后写入数据
            excelWriter.write(this.getFinalData("备注 ....."), writeSheet, writeTableFin);

            //完成
            excelWriter.finish();
        } catch (Exception e) {
            log.error("工单确认单模板生成,方法异常>>>>>>>>>>>>>>", e);
        }
    }
}

2)合并策略

import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.*;

/**
 * 功能描述:规则: 优先合并列,再合并行
 *
 * @author SXT
 * @version 1.0
 * @date 2024/3/9 15:12
 */
public class MergeCellStrategyHandler extends AbstractMergeStrategy {
    /**
     * 相同列合并
     */
    private boolean alikeColumn;

    /**
     * 相同行合并
     */
    private boolean alikeRow;

    /** 开始进行合并的行index */
    private int rowIndex;

    /** 开始进行合并的行index */
    private int rowIndexStart;

    /** 跨行合并的列index */
    private Set<Integer> columns;

    private int currentRowIndex = 0;

    /**
     * 构造方法,指定合并方式
     * @param alikeColumn
     * @param alikeRow
     * @param rowIndex
     * @param columns
     */
    public MergeCellStrategyHandler(boolean alikeColumn, boolean alikeRow, int rowIndex, Set<Integer> columns){
        this.alikeColumn = alikeColumn;
        this.alikeRow = alikeRow;
        this.rowIndex = rowIndex;
        this.columns = columns;
    }

    /**
     * 构造方法,指定合并方式
     * @param alikeColumn
     * @param alikeRow
     * @param rowIndex
     * @param columns
     */
    public MergeCellStrategyHandler(boolean alikeColumn, boolean alikeRow, int rowIndex, Set<Integer> columns, int rowIndexStart){
        this.alikeColumn = alikeColumn;
        this.alikeRow = alikeRow;
        this.rowIndex = rowIndex;
        this.columns = columns;
        this.rowIndexStart = rowIndexStart;
    }

    /**
     * 指定是否进行跨列合并单元格
     * @param alikeColumn
     * @param rowIndex
     */
    public MergeCellStrategyHandler(boolean alikeColumn, int rowIndex){
        this(alikeColumn, false, rowIndex, new HashSet<>());
    }

    /**
     * 指定是否进行跨列合并单元格
     * @param alikeColumn
     * @param rowIndex
     * @param rowIndexStart 开始行合并的行数
     */
    public MergeCellStrategyHandler(boolean alikeColumn, int rowIndex, int rowIndexStart){
        this(alikeColumn, false, rowIndex, new HashSet<>(), rowIndexStart);
    }

    /**
     * 指定是否进行跨行合并单元格
     * @param alikeRow
     * @param rowIndex
     * @param columns
     */
    public MergeCellStrategyHandler(boolean alikeRow, int rowIndex, Set<Integer> columns){
        this(false, alikeRow, rowIndex, columns);
    }


    @Override
    protected void merge(Sheet sheet, Cell cell, Head head, Integer integer) {
        int rowId = cell.getRowIndex();
        currentRowIndex = rowId == currentRowIndex ? currentRowIndex : rowId;
        if (rowIndex > rowId) {
            return;
        }

        int columnId = cell.getColumnIndex();
        // 列合并
        if (alikeColumn && columnId > 0) {
            String currentCellVal = this.getCellVal(cell);
            Cell preCell = cell.getRow().getCell(columnId - 1);
            String preCellVal = this.getCellVal(preCell);
            if (null != currentCellVal && null != preCellVal && !preCellVal.isEmpty() && !currentCellVal.isEmpty()) {
                // 当前单元格内容与上一个单元格内容相等,进行合并处理
                if (preCellVal.equals(currentCellVal)) {
                    CellRangeAddress rangeAddress = new CellRangeAddress(currentRowIndex, currentRowIndex, columnId - 1, columnId);
                    rangeAddress = this.findExistAddress(sheet, rangeAddress, currentCellVal);
                    if (null != rangeAddress) {
                        sheet.addMergedRegion(rangeAddress);
                    }
                }
            }
        }

        // 限制开始行合并的行数
        if (rowId > rowIndexStart) {
            // 行合并
            if (alikeRow && rowIndex < rowId && columns.contains(columnId)) {
                String currentCellVal = this.getCellVal(cell);
                Cell preCell = sheet.getRow(rowId - 1).getCell(columnId);
                String preCellVal = this.getCellVal(preCell);
                if (null != currentCellVal && null != preCellVal && !preCellVal.isEmpty() && !currentCellVal.isEmpty()) {
                    // 当前单元格内容与上一行单元格内容相等,进行合并处理
                    if (preCellVal.equals(currentCellVal)) {
                        //sheet.validateMergedRegions();
                        CellRangeAddress rangeAddress = new CellRangeAddress(currentRowIndex - 1, currentRowIndex, columnId, columnId);
                        rangeAddress = this.findExistAddress(sheet, rangeAddress, currentCellVal);
                        if (null != rangeAddress) {
                            sheet.addMergedRegion(rangeAddress);
                        }
                    }
                }
            }
        }


    }

    /**
     * 合并单元格地址范围,发现存在相同的地址则进行扩容合并
     *
     * @param sheet
     * @param rangeAddress  单元格合并地址
     * @param currentVal 当前单元格中的值
     * @return
     */
    private CellRangeAddress findExistAddress(Sheet sheet, CellRangeAddress rangeAddress, String currentVal) {
        List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
        int existIndex = 0;
        Map<Integer, CellRangeAddress> existIdexMap = new LinkedHashMap<>();
        if (null != mergedRegions && !mergedRegions.isEmpty()) {
            //验证当前合并的单元格是否存在重复
            for (CellRangeAddress mergedRegion : mergedRegions) {
                if (mergedRegion.intersects(rangeAddress)) {
                    existIdexMap.put(existIndex, mergedRegion);
                }
                existIndex++;
            }
        }
        if (existIdexMap.isEmpty()) {
            return rangeAddress;
        }
        List<Integer> existIndexList = new ArrayList<>(existIdexMap.size());
        for (Map.Entry<Integer, CellRangeAddress> addressEntry : existIdexMap.entrySet()) {
            CellRangeAddress exist = addressEntry.getValue();
            // 自动进行单元格合并处理
            int firstRow = rangeAddress.getFirstRow();
            int lastRow = rangeAddress.getLastRow();
            int firstColumn = rangeAddress.getFirstColumn();
            int lastColumn = rangeAddress.getLastColumn();

            int firstRow1 = exist.getFirstRow();
            int lastRow1 = exist.getLastRow();
            int firstColumn1 = exist.getFirstColumn();
            int lastColumn1 = exist.getLastColumn();
            // 跨行合并 最后一列相等, 行不相等
            if (lastRow > lastRow1 && lastColumn == lastColumn1) {
                // 检查进行跨行合并的单元格是否已经存在跨列合并
                if (lastColumn > 0 && firstColumn1 != lastColumn1) {
                    // 获取当前单元格的前一列单元格
                    String cellVal = this.getCellVal(sheet.getRow(lastRow).getCell(lastColumn - 1));
                    if (null != cellVal && cellVal.equals(currentVal)) {
                        exist.setLastRow(lastRow);
                    }
                } else {
                    exist.setLastRow(lastRow);
                }
                rangeAddress = exist;
                existIndexList.add(addressEntry.getKey());
            }

            // 跨列合并 行相等,列不相等
            if (lastColumn > lastColumn1 && firstRow == firstRow1 ) {
                exist.setLastColumn(lastColumn);
                rangeAddress = exist;
                existIndexList.add(addressEntry.getKey());
            }
        }
        // 移除已经存在且冲突的合并数据
        if (existIndexList.isEmpty()) {
            rangeAddress = null;
        }else {
            sheet.removeMergedRegions(existIdexMap.keySet());
        }
        return rangeAddress;
    }

    /**
     * 获取单元格中的内容
     * @param cell
     * @return
     */
    private String getCellVal(Cell cell) {
        String val = null;
        try {
            val = cell.getStringCellValue();
        }catch (Exception e){
            System.out.printf("读取单元格内容失败:行%d 列%d %n", (cell.getRowIndex() + 1), (cell.getColumnIndex() + 1));
        }
        return val;
    }
}

3)单元格样式策略

import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.util.StyleUtil;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import org.apache.poi.ss.usermodel.*;
import java.util.HashMap;
import java.util.List;

/**
 * @Desc 拦截处理单元格创建
 */
public class CellStyleWriteHandler implements CellWriteHandler {
    /**
     * map
     * key:第i行
     * value:第i行中单元格索引集合
     */
    private HashMap<Integer,List<Integer>> map;

    /**
     * 颜色
     */
    private Short colorIndex;

    /**
     * 有参构造
     * map:用来记录需要为第key行中的第value.get(i)列设置样式
     * colorIndex:表示单元格需要设置的颜色
     */
    public CellStyleWriteHandler(HashMap<Integer, List<Integer>> map, Short colorIndex) {
        this.map = map;
        this.colorIndex = colorIndex;
    }

    /**
     * 无参构造
     */
    public CellStyleWriteHandler() {

    }

    /**
     * 在创建单元格之前调用
     */
    @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) {
    }

    /**
     * 在单元上的所有操作完成后调用
     * 指定单元格特殊处理 todo 待修改为指定样式
     */
    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {

        /**
         * 考虑到导出数据量过大的情况,不对每一行的每一个单元格进行样式设置,只设置必要行中的某个单元格的样式
         */
        //当前行的第i列
        int i = cell.getColumnIndex();
        //不处理第一行
        if (0 != cell.getRowIndex()) {
            List<Integer> integers = map.get(cell.getRowIndex());
            if (integers != null && integers.size() > 0) {
                if (integers.contains(i)) {
                    // 根据单元格获取workbook
                    Workbook workbook = cell.getSheet().getWorkbook();
                    //设置行高
                    writeSheetHolder.getSheet().getRow(cell.getRowIndex()).setHeight((short) (4.5 * 256));
                    // 单元格策略
                    WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
                    // 设置背景颜色白色
                    contentWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
                    // 设置垂直居中为向上对齐
                    contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.TOP);
                    // 设置左右对齐为靠左对齐
                    contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
                    // 设置单元格上下左右边框为细边框
                    contentWriteCellStyle.setBorderBottom(BorderStyle.THIN);
                    contentWriteCellStyle.setBorderLeft(BorderStyle.THIN);
                    contentWriteCellStyle.setBorderRight(BorderStyle.THIN);
                    contentWriteCellStyle.setBorderTop(BorderStyle.THIN);
                    // 创建字体实例
                    WriteFont cellWriteFont = new WriteFont();
                    // 设置字体大小
                    cellWriteFont.setFontName("宋体");
                    cellWriteFont.setFontHeightInPoints((short) 11);
                    //设置字体颜色:无效
                    cellWriteFont.setColor(colorIndex);
                    //单元格颜色:无效
                    contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
                    contentWriteCellStyle.setWriteFont(cellWriteFont);

                    CellStyle cellStyle = StyleUtil.buildCellStyle(workbook, null, contentWriteCellStyle);
                    //设置当前行第i列的样式
                    cell.getRow().getCell(i).setCellStyle(cellStyle);
                }
            }
        }
    }
}

3.模板导出

/**
 * 导出工单表单模板
 */
public void exportWorkOrderTemplate(Long orderId) throws IOException {
        /**:
         * 根据表单id查询表单信息,回填模板表
         */
        WorkOrderInfo workOrderInfo = workOrderInfoMapper.selectById(orderId);
        if (workOrderInfo == null) {
            throw new BizException("工单信息不存在");
        }

        /**
         * 查询工单设备、服务申请情况
         */
        WorkOrderApplyInfo orderApply = workOrderApplyService.getWorkOrderApplayInfo(olderId);

		// 开始合并的行数
        int addRowNum = 3;
      
        // 设备列表
        List<WorkOrderDevice> deviceList = orderApply.getDeviceList();
        // 服务列表
        List<WorkOrderService> serviceList = orderApply.getServiceList();

        // 尾部固定行开始位置 = 前置固定行(7) + 设备列表行 + 服务列表行 + 合计行
        int endCol = MictConstants.EIGHT + deviceList.size() + serviceList.size() + addRowNum;

        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 文件生成准备 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse();
        // 获取模版文件
        ClassPathResource classPathResource = new ClassPathResource("xls_template/work_order_template.xlsx");
        InputStream fis = classPathResource.getInputStream();

        // 文件输出流
        String fileName = URLEncoder.encode(String.format("%s.xlsx", "工单确认单"), "UTF-8");
        response.setContentType("application/x-download;charset=UTF-8");
        response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        response.addHeader("Pragma", "no-cache");
        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
        OutputStream out = response.getOutputStream();
        try {

            // 构造固定填充模板对象
            WorkOrderInfo workOrderPrefix = WorkOrderInfo.builder()
                    .workOrderCode(workOrderInfo.getWorkOrderCode())
                    .creatorName(workOrderInfo.getCreatorName())
                    .handerName(workOrderInfo.getHanderName())
                    .workOrderTitle(workOrderInfo.getWorkOrderTitle())
                    .workOrderContent(businessInfo.getWorkOrderContent())
.build();

            /**
             * 用来记录需要为第`key`行中的第`value.get(i)`列设置样式
             */
            // 指定需要跨行合并的列项:7列
            HashSet<Integer> colSet = new HashSet<>(Arrays.asList(0, 1, 2, 3, 4, 5, 6));
            HashMap<Integer, List<Integer>> map = new HashMap<>(MictConstants.SIX);
            for (int i = endCol; i < endCol + MictConstants.SIX; i++) {
                map.put(i, Arrays.asList(0,1,2,3,4,5,6));
            }

            // 设置excel输出策略
            ExcelWriter excelWriter = EasyExcel
                    .write(out, WorkOrderInfo.class)
                    .withTemplate(fis)
                    // 默认样式策略
                    .registerWriteHandler(EasyExcelUtils.defaultStyles())
                    // 指定单元格样式(备注说明内容栏)
                    .registerWriteHandler(new CellStyleWriteHandler(map, IndexedColors.RED.getIndex()))
                    // 行和列合并策略
                    .registerWriteHandler(new MergeCellStrategyHandler(true, true, 7, colSet, endCol))
                    .build();


            // sheet设置不需要头
            WriteSheet writeSheet = EasyExcel.writerSheet(0,"工单确认单").needHead(Boolean.FALSE).build();

            // 填充固定值
            excelWriter.fill(workOrderPrefix, writeSheet);
			// 使用table写入模板
           	WorkOrderDataUtils.generateWorkOrderTemplate(excelWriter, writeSheet, orderApply);

            out.flush();
            out.close();
            fis.close();

        } catch (Exception e) {
            log.error("工单确认单生成,方法异常>>>>>>>>>>>>>>", e);
        }
    }

参考文章

EasyExcel动态单元格合并(跨行或跨列) by 酸菜鱼没有鱼

easyExcel实现单sheet多子表,并结合动态表头,复杂表头 by IM@taoyalong

easyExcel官方文档

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在使用 easyexcel-2.1.6 进行导出时,可以通过自定义格式化器实现相同列内容的合并。具体步骤如下: 1. 自定义格式化器 ```java public class MergeCellStrategy implements WriteHandler { private int mergeRowIndex = 0; private int mergeCellIndex = 0; private Object preCellValue = null; @Override public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { } @Override public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { } @Override public void afterWorkbookCreate(WriteWorkbookHolder writeWorkbookHolder) { } @Override public void beforeRowCreate(WriteSheetHolder writeSheetHolder, WriteRowHolder writeRowHolder, Row row, Integer rowIndex, Integer relativeRowIndex, Boolean isHead) { } @Override public void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteRowHolder writeRowHolder, Row row, Integer rowIndex, Boolean isHead) { } @Override public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteCellHolder writeCellHolder, Cell cell, Integer cellIndex, String cellValue, Boolean isHead) { } @Override public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteCellHolder writeCellHolder, Cell cell, Integer cellIndex, String cellValue, Boolean isHead) { if (!isHead) { if (cellIndex == mergeCellIndex && preCellValue != null && preCellValue.equals(cellValue)) { // 添加合并单元格的区域 writeSheetHolder.getSheet().addMergedRegion(new CellRangeAddress(mergeRowIndex, rowIndex, cellIndex, cellIndex)); } else { // 更新前一个单元格的值和行索引 preCellValue = cellValue; mergeRowIndex = rowIndex; mergeCellIndex = cellIndex; } } } } ``` 在自定义格式化器中,我们维护了前一个单元格的值和行索引,当当前单元格的值和前一个单元格的值相同时,将当前单元格和前一个单元格合并。 2. 注册自定义格式化器 ```java ExcelWriterBuilder writerBuilder = EasyExcel.write(outputStream, clazz) .registerWriteHandler(new MergeCellStrategy()); ``` 在注册写处理器时,将自定义格式化器注册进去即可。 3. 编写导出代码 在编写导出代码时,需要指定合并单元格的列,例如对于以下实体类: ```java @Data public class User { private Long id; private String name; private Integer age; } ``` 如果要对 name 列进行合并,可以这样写: ```java List<User> userList = getUserList(); ExcelWriterBuilder writerBuilder = EasyExcel.write(outputStream, User.class) .registerWriteHandler(new MergeCellStrategy()); writerBuilder.sheet().registerWriteHandler(new MergeCellStrategy()); writerBuilder.sheet().tableStyle(TableStyleTypeEnum.TABLE_STYLE_MEDIUM); Sheet sheet = writerBuilder.sheet().build(); List<List<String>> head = new ArrayList<>(); // 表头 head.add(Arrays.asList("ID", "姓名", "年龄")); // 内容 List<List<Object>> content = new ArrayList<>(); for (User user : userList) { List<Object> row = new ArrayList<>(); row.add(user.getId()); row.add(user.getName()); row.add(user.getAge()); content.add(row); } writerBuilder.head(head).sheet("Sheet1").doWrite(content); ``` 在导出代码中,我们将自定义格式化器注册到 sheet 中,通过 `registerWriteHandler` 方法实现。同时,我们在导出时指定了表头内容,并将其写入 Excel 中。 这样,就可以实现对相同列内容的合并了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值