【原创】Ruoyi若依框架微服务架构ExcelUtil扩展——ExcelExtendUtil

Ruoyi若依框架微服务架构ExcelUtil扩展——ExcelExtendUtil

背景

若依框架自带的Excel导出工具类——ExcelUtil基于org.apache.poi,,版本为4.1.2

路径为ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/poi/ExcelUtil.java

因不满足具体工作需求,还有一些bug问题,比如:

1、自定义多级表头

2、灵活的行合并,列合并

3、灵活的单元格背景颜色渲染

所以,根据具体需求,在若依自带的ExcelUtil工具类基础上,新建编写ExcelExtendUtil扩展工具类。

自定义多级表头

在扩展此需求后,考虑扩展和灵活性,使用什么方式能满足这两点?

经过一番思考和查阅资料之后,感觉采用两种方式,基于约定大于配置原则:

1、设置特定格式Json串

2、设置特定格式的集合

Json串格式

[
      {
        "code":"competitiveItem",
        "name":"竞赛项",
        "colIndex": 0,
        "rowIndex": 0,
        "spanRow": "0,1",
        "spanCol": null
      },
      {
        "code":"deptName",
        "name":"单位名称",
        "colIndex": 1,
        "rowIndex": 0,
        "spanRow": "0,1",
        "spanCol": null
      },
      {
        "code":"unit",
        "name":"单位",
        "colIndex": 2,
        "rowIndex": 0,
        "spanRow": "0,1",
        "spanCol": null
      },
      {
        "code":"monthPlanTarget",
        "name":"月计划指标",
        "colIndex": 3,
        "rowIndex": 0,
        "spanRow": "0,1",
        "spanCol": null
      },
      {
        "children":[
          {
            "code":"planTarget",
            "name":"计划指标",
            "parentCode":"-1",
            "colIndex": 4,
            "rowIndex": 1,
            "spanRow": null,
            "spanCol": null
          },
          {
            "code":"performance",
            "name":"完成量",
            "parentCode":"-1",
            "colIndex": 5,
            "rowIndex": 1,
            "spanRow": null,
            "spanCol": null
          },
          {
            "code":"overproduction",
            "name":"超产量",
            "parentCode":"-1",
            "colIndex": 6,
            "rowIndex": 1,
            "spanRow": null,
            "spanCol": null
          },
          {
            "code":"overproductionRatio",
            "name":"超产比例",
            "parentCode":"-1",
            "colIndex": 7,
            "rowIndex": 1,
            "spanRow": null,
            "spanCol": null
          }
        ],
        "code":"-1",
        "name":"日进度完成情况",
        "colIndex": 4,
        "rowIndex": 0,
        "spanRow": null,
        "spanCol": "4,7"
      },
      {
        "children":[
          {
            "code":"rangePlanTarget",
            "name":"应完成量",
            "parentCode":"-1",
            "colIndex": 8,
            "rowIndex": 1,
            "spanRow": null,
            "spanCol": null
          },
          {
            "code":"rangePerformance",
            "name":"实际完成量",
            "parentCode":"-1",
            "colIndex": 9,
            "rowIndex": 1,
            "spanRow": null,
            "spanCol": null
          },
          {
            "code":"rangeOverproduction",
            "name":"实际超产量",
            "parentCode":"-1",
            "colIndex": 10,
            "rowIndex": 1,
            "spanRow": null,
            "spanCol": null
          },
          {
            "code":"rangeOverproductionRatio",
            "name":"实际超产比",
            "parentCode":"-1",
            "colIndex": 11,
            "rowIndex": 1,
            "spanRow": null,
            "spanCol": null
          }
        ],
        "code":"-1",
        "name":"按时间进度完成情况",
        "colIndex": 8,
        "rowIndex": 0,
        "spanRow": null,
        "spanCol": "8,11"
      }

    ]

集合格式

在这里插入图片描述

ExcelExtendUtil类新增代码

1、增加属性

	 /**
     * 标题索引
     */
    private Integer titleRowIndex;	
	/**
     * 表头开始的索引
     */
    private int tableTopStartIndex = 1;

    /**
     * 表数据开始的索引
     */
    private int tableDataStartIndex = 2;	
	
	/**
     * 是否开启多级表头
     */
    private boolean openMultilevelTableTop = false;

    /**
     * 表头所占最大行索引
     */
    private int tableTopMaxIndex = 1;

    /**
     * 表头Json数据
     */
    private String tableTopJsonData;

    /**
     * 表头数据集合列表
     */
    private List<JsonData> tableTopJsonDataList;

2、writeSheet修改为以下代码,主要是根据this.openMultilevelTableTop进行判断,如果设置开启自定义多表头,则调用this.createTableHead()方法处理自定义表头的Json数据或集合数据,否则走若依默认的。

	/**
     * 创建写入数据到Sheet
     */
    public void writeSheet()
    {
        // 取出一共有多少个sheet.
        int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
        for (int index = 0; index < sheetNo; index++)
        {
            createSheet(sheetNo, index);

            // 产生表头信息开始一行

            this.tableTopStartIndex = this.titleRowIndex+1;
            Row row = sheet.createRow(this.tableTopStartIndex);

            int column = 0;
            // TODO 动态添加多级表头
            if(this.openMultilevelTableTop) {
                try {
                    this.createTableHead();
                    this.tableTopMaxIndex = tableTopRowMap.entrySet().stream().map(Map.Entry::getKey).max(Comparator.comparing(i->i)).get();
                    System.out.println(this.tableTopMaxIndex);
                } catch (Exception e) {
                    e.printStackTrace();
                }


            } else {
                //根据字段注解配置参数填充表头
                for (Object[] os : fields)
                {
                    Excel excel = (Excel) os[1];
                    this.createCell(excel, row, column++);
                }
            }
            //表数据开始索引计算
            this.tableDataStartIndex = this.tableTopStartIndex + this.tableTopMaxIndex;
            Row startDataRow = sheet.createRow(this.tableDataStartIndex);

            if (Type.EXPORT.equals(type))
            {
                fillExcelData(index, startDataRow);
                addStatisticsRow();
            }
        }
    }

3、createTableHead()recursionCreateTableTopCell()方法

 	/**
     * 创建多级表头
     */
    public void createTableHead() {
        List<JsonData> list = new ArrayList<>();
        if(StringUtils.isNotEmpty(tableTopJsonData) || (StringUtils.isNotEmpty(tableTopJsonData) && this.tableTopJsonDataList != null)) {
            JSONArray jsonArray = JSONObject.parseArray(this.tableTopJsonData);
            list = jsonArray.toJavaList(JsonData.class);
        } else if(StringUtils.isEmpty(tableTopJsonData) && this.tableTopJsonDataList != null) {
            list = this.tableTopJsonDataList;
        } else {
            throw new ServiceException("请设置表头Json数据字符串或者列表!");
        }

        Row row = sheet.createRow(this.tableTopStartIndex);
        for(int i = 0; i < list.size(); i++) {
            this.recursionCreateTableTopCell(list.get(i), row);
        }

        //EasyExcel多表头格式 保留
        //List<List<String>> headList = head(list);

    }

    /**
     * 递归-深度优先 创建表格单元格
     * @param jsonData
     * @param row
     * @return
     */
    public Cell recursionCreateTableTopCell(JsonData jsonData, Row row) {
        this.tableTopRowMap.put(row.getRowNum(), row);


        // 创建列
        Cell cell = row.createCell(jsonData.getColIndex().intValue());
        // 写入列信息
        cell.setCellValue(jsonData.getName());
        //setDataValidation2(jsonData.getColIndex().intValue());
        cell.setCellStyle(styles.get("header"));

        //合并规则信息
        String spanRow = jsonData.getSpanRow();
        String spanCol = jsonData.getSpanCol();

		//处理多级表头行合并
        if(spanRow != null) {
            String[] spanRowArray = spanRow.split(",");
            int spanRowStartIndex = Integer.parseInt(spanRowArray[0]);
            int spanRowEndIndex = Integer.parseInt(spanRowArray[1]);
            int spanColStartIndex = jsonData.getColIndex().intValue();
            int spanColEndIndex = jsonData.getColIndex().intValue();

            CellRangeAddress region = new CellRangeAddress(spanRowStartIndex+this.tableTopStartIndex, spanRowEndIndex+this.tableTopStartIndex, spanColStartIndex, spanColEndIndex);
            sheet.addMergedRegion(region);
        }
		//处理多级表头列合并
        if(spanCol != null) {
            String[] spanColArray = spanCol.split(",");
            int spanRowStartIndex = jsonData.getRowIndex().intValue();
            int spanRowEndIndex = jsonData.getRowIndex().intValue();
            int spanColStartIndex = Integer.parseInt(spanColArray[0]);
            int spanColEndIndex = Integer.parseInt(spanColArray[1]);

            CellRangeAddress region = new CellRangeAddress(spanRowStartIndex+this.tableTopStartIndex, spanRowEndIndex+this.tableTopStartIndex, spanColStartIndex, spanColEndIndex);
            sheet.addMergedRegion(region);
        }

		
		//判断是否有子集进入递归
        List<JsonData> children = jsonData.getChildren();
        if(children != null) {
            int rowIndex = row.getRowNum()+1;
            Row historyRow = this.tableTopRowMap.get(rowIndex);
            if(historyRow == null) {
                historyRow = sheet.createRow(rowIndex);
            }
            for(int i= 0; i< children.size(); i++) {
                JsonData sonJsonData = children.get(i);
                System.out.println(sonJsonData);
                this.recursionCreateTableTopCell(sonJsonData, historyRow);
            }
        }

        return cell;
    }

	/**
     * 解析的表头实例对象
     */
    @Data
    static class JsonData {
        /** 唯一标识 */
        private String code;
        /** 子集 */
        private List<JsonData> children;
        /** 父唯一标识 */
        private String parentCode;
        /** 单元格内容 */
        private String name;
        /** 列索引,从0开始 */
        private Long colIndex;
        /** 行索引,从0开始,使用时不考虑标题列所占行 */
        private Long rowIndex;
        /** 行合并,比如0,4 */
        private String spanRow;
        /** 列合并,比如0,4 */
        private String spanCol;
    }

    /**
     * 处理表头数据-深度优先
     * @param data
     * @return
     */
    private static List<List<String>> head(List<JsonData> data) {
        List<List<String>> result = new ArrayList<>();

        List<String> temp = new ArrayList<>();

        // 深度优先搜索 使用递归
        for (JsonData jsonData : data) {
            dfs(result, jsonData, temp);
        }

        return result;
    }

    /**
     * 深度搜索复制
     * @param result
     * @param jsonData
     * @param temp
     */
    private static void dfs(List<List<String>> result, JsonData jsonData, List<String> temp) {
        List<JsonData> datas = jsonData.getChildren();
        temp.add(jsonData.getName());
        if (datas == null) {
            //看似浅复制,其实是深复制
            List<String> head = new ArrayList<>(temp);
            result.add(head);
        } else {
            for (JsonData data : datas) {
                dfs(result, data, temp);
            }
        }

        // 移除最后一个
        temp.remove(temp.size() - 1);
    }

Controller使用

/**
     * 导出龙虎榜
     */
    @Log(title = "龙虎榜", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, ProDataReportQuery query)
    {
        try {
            List<WinnersVo> list = winnersService.getList(query);
            Map<Integer, Map<String, String>> alertBackgroundColorByDynamicRuleMap = new HashMap<>();
            ExcelExtendUtil<WinnersVo> util = new ExcelExtendUtil<>(WinnersVo.class);
			//设置表标题所占行索引
            util.setTitleRowIndex(0); 
			//设置是否开启多级表头合并
            util.setOpenMultilevelTableTop(true);
			//设置多级表头规则Json(可以通过json工具配置好后直接复制过来)
            util.setTableTopJsonData("[\n" +
                    "      {\n" +
                    "        \"code\":\"competitiveItem\",\n" +
                    "        \"name\":\"竞赛项\",\n" +
                    "        \"colIndex\": 0,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": \"0,1\",\n" +
                    "        \"spanCol\": null\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"code\":\"deptName\",\n" +
                    "        \"name\":\"单位名称\",\n" +
                    "        \"colIndex\": 1,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": \"0,1\",\n" +
                    "        \"spanCol\": null\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"code\":\"unit\",\n" +
                    "        \"name\":\"单位\",\n" +
                    "        \"colIndex\": 2,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": \"0,1\",\n" +
                    "        \"spanCol\": null\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"code\":\"monthPlanTarget\",\n" +
                    "        \"name\":\"月计划指标\",\n" +
                    "        \"colIndex\": 3,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": \"0,1\",\n" +
                    "        \"spanCol\": null\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"children\":[\n" +
                    "          {\n" +
                    "            \"code\":\"planTarget\",\n" +
                    "            \"name\":\"计划指标\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 4,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"performance\",\n" +
                    "            \"name\":\"完成量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 5,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"overproduction\",\n" +
                    "            \"name\":\"超产量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 6,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"overproductionRatio\",\n" +
                    "            \"name\":\"超产比例\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 7,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          }\n" +
                    "        ],\n" +
                    "        \"code\":\"-1\",\n" +
                    "        \"name\":\"日进度完成情况\",\n" +
                    "        \"colIndex\": 4,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": null,\n" +
                    "        \"spanCol\": \"4,7\"\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"children\":[\n" +
                    "          {\n" +
                    "            \"code\":\"rangePlanTarget\",\n" +
                    "            \"name\":\"应完成量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 8,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"rangePerformance\",\n" +
                    "            \"name\":\"实际完成量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 9,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"rangeOverproduction\",\n" +
                    "            \"name\":\"实际超产量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 10,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"rangeOverproductionRatio\",\n" +
                    "            \"name\":\"实际超产比\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 11,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          }\n" +
                    "        ],\n" +
                    "        \"code\":\"-1\",\n" +
                    "        \"name\":\"按时间进度完成情况\",\n" +
                    "        \"colIndex\": 8,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": null,\n" +
                    "        \"spanCol\": \"8,11\"\n" +
                    "      }\n" +
                    "\n" +
                    "    ]");

            util.exportExcel(response, list, "龙虎榜", "龙虎榜");
        } catch (Exception e) {
            e.printStackTrace();
            throw new ServiceException("导出失败!");
        }

    }

表头导出效果

注意:理论上任意层级深度,三级已通过验证,其他层级可进行尝试!

在这里插入图片描述

特殊说明

1、开启多级表头后,意味着真实数据渲染的开始索引需要往下移动,这里需要动态计算出来真实数据开始的索引位置,即表头的层级所占索引最大值加上标题所占索引;

2、所有的在json或集合中配置的表头信息,涉及到列、行索引都是从0开始计算配置就可以,因为在使用这些进行逻辑处理的时候会自动计算所占索引;

灵活的行、列合并

合并原理

行和列合并的原理是使用CellRangeAddress添加规则:


/**
	参数说明:
	int spanRowStartIndex: 起始行号  ,0是第一行
	int spanRowEndIndex: 终止行号
	int spanColStartIndex: 起始列号,0是第一列
	int spanColEndIndex: 终止列号
从0开始,不是从1开始	
*/
CellRangeAddress region = new CellRangeAddress(spanRowStartIndex, spanRowEndIndex, spanColStartIndex, spanColEndIndex);
sheet.addMergedRegion(region);

通用合并

@Excel注解新增参数
	/**
     * 字段索引
     * @return
     */
    public long index() default -1;
  	/**
     * 合并(行或列)
     * @return
     */
    public String spanCell() default "";

    /**
     * 合并行
     * @return
     */
    public String spanRow() default "";
ExcelExtendUtil实现代码
				//通用合并行
                if(StringUtils.isNotEmpty(spanRow)) {
                   String[] spanRowArray = spanRow.split("\\|");
                   List<String[]> spanRowRuleList = Arrays.stream(spanRowArray).map(i->{
                        return i.split(",");
                    }).collect(Collectors.toList());
                   spanRowRuleList.stream().forEach(i-> {
                       Integer spanRowStart = Integer.parseInt(i[0].trim());
                       Integer spanRowEnd = Integer.parseInt(i[1].trim());
                       Long fieldIndex = null;
                       if(i.length == 3) {
                           fieldIndex = Long.parseLong(i[2].trim());
                       } else if(index != -1){
                           fieldIndex = index;
                       } else {
                           throw new ServiceException("列索引规则配置错误,请检查字段索引值是否配置!");
                       }

                       CellRangeAddress region = new CellRangeAddress(spanRowStart+this.tableDataStartIndex, spanRowEnd+this.tableDataStartIndex, fieldIndex.intValue(), fieldIndex.intValue());
                       sheet.addMergedRegion(region);
                   });
                }
                //通用合并单元格(包括行合并和列合并)
                if(StringUtils.isNotEmpty(spanCell)) {
                    String[] spanCellArray = spanCell.split("\\|");
                    List<String[]> spanCellRuleList = Arrays.stream(spanCellArray).map(i->{
                        return i.split(",");
                    }).collect(Collectors.toList());
                    spanCellRuleList.stream().forEach(i->{
                        Integer spanRowStart = Integer.parseInt(i[0].trim());
                        Integer spanRowEnd = Integer.parseInt(i[1].trim());
                        Integer spanCellStart = Integer.parseInt(i[2].trim());
                        Integer spanCellEnd = Integer.parseInt(i[3].trim());
                        CellRangeAddress region = new CellRangeAddress(spanRowStart+this.tableDataStartIndex, spanRowEnd+this.tableDataStartIndex, spanCellStart, spanCellEnd);
                        sheet.addMergedRegion(region);
                    });
                }
VO对象使用

使用时最好对导出的VO对象的列增加上index字段索引参数,从0开始

  /**
   * 竞赛项
   */
  @Excel(name = "竞赛项", index = 0, spanRow = "0,4|5,8|9,11|12,15|18,22", spanCell = "17,17,0,3|18,18,2,3|19,19,2,3|20,20,2,3|21,21,2,3|22,22,2,3")
  private String competitiveItem;
规则使用说明

1、spanRow 实际是配置索引值,每个规则以|相隔,单个规则0,4表示从索引为0的行合并到索引为13的行,即为行合并开始索引和行合并结束索引

2、spanCell实际是配置索引值,每个规则以|相隔,单个17,17,0,3表示行索引为17的行从列索引为0到列索引为3进行列合并,即索引为17的行合并索引0到3的列

3、注意使用spanCell时,如果spanRow的配置规则已经包括,则使用spanCell进行列合并时最好一行一行的配置,如果不包括则可以只配置spanCell同时进行行和列合并,比如:23,25,0,3表示 行合并从索引23到25,列合并从索引0到3

导出效果

因工作保密原因,不贴图了,可自行测试,望伙计们海含!

快速合并

一个一个的配置索引是为了非常复杂的表格单元格合并,这种我称之为通用合并,但有时一个一个的配置也挺麻烦,所以又增加快速合并功能,目前只实现了相同行合并规则,伙计们可以进行扩展其他规则。

@Excel新增参数
 	/**
     * 是否快速合并
     */
    public boolean isQuickSpan() default false;
    /**
     * 快速合并规则
     * @return
     */
    public QuickSpanRule quickSpanRule() default QuickSpanRule.NULL ;

	/**
     * 快速合并规则枚举
     */
    public enum QuickSpanRule
    {
        NULL(-1, "无规则"),
        IDENTICAL_ROW_MERGE(0, "相同行合并");

        private final int value;
        private final String desc;

        QuickSpanRule(int value, String desc)
        {
            this.value = value;
            this.desc = desc;
        }

        public int value()
        {
            return this.value;
        }
    }

ExcelExtendUtil实现代码

1、新增属性和类

	/**
     * 快速合并计算附加参数
     */
    private Map<Long, ExcelFieldAdditionParams>  fieldAdditionParamsMap;


	/**
     * Excel列辅助参数对象类
     * 解决:
     * 多列同时设置这两个数据会混乱 解决方法:每一列生成一个单独的对象,相互独立
     * this.mergeRowStart = 0;
     * this.mergeRowEnd = 0;
     */
    @Data
    public class ExcelFieldAdditionParams {

        private int mergeRowStart = 0;
        private int mergeRowEnd = 0;
        private int mergeCellStart = 0;
        private int mergeCellEnd = 0;
    }

2、createExcelField()方法初始化fieldAdditionParamsMap参数,为每个列创建一个辅助计算对象ExcelFieldAdditionParams

/**
 * 得到所有定义字段
 */
private void createExcelField()
{
    this.fields = getFields();
    this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
    this.fieldAdditionParamsMap = this.fields.stream().map(i->{
        Excel excel = (Excel) i[1];
        return excel;
    }).filter(excel -> excel.index() != -1).collect(Collectors.toMap(Excel::index, i->new ExcelFieldAdditionParams()));
    this.maxHeight = getRowHeight();
}

3、fillExcelData()方法修改

  /**
     * 填充excel数据
     *
     * @param index 序号
     * @param row 单元格行
     */
    public void fillExcelData(int index, Row row){
		... ...

		//当前行
        int thisLine = startNo;
        for (int i = startNo; i < endNo; i++) {
            ... ...

			//主要新增
            T voPrevious = null;
            //得到上一个导出对象
            if (i != startNo) {
                voPrevious = (T) list.get(i - 1);
            }
            /**
             *取下一个对象   与当前对象对比,如果相同,记住当前列,再与下一个对比,一直对比到不相同,执行合并代码
             * 注解加入 合并行列标识
             */
            int column = 0;
            for (Object[] os : fields) {
                ... ...
                //每个列加入辅助参数对象
                this.addCell(excel, row, vo, field, column++, voPrevious, thisLine);

            }
        }
	}

4、addCell()方法新增参数voPrevious, thisLine和逻辑处理

这里在处理mergeRowStart和mergeRowEnd时,每个列的合并处理对应的值都应该是相互独立的,如果统一使用一个则合并会出现混乱,所以这里采用fieldAdditionParamsMap快速合并计算附加参数,每个列根据自己的ExcelFieldAdditionParams对象来进行独立计算,从而解决合并混乱问题。


    /**
     * 添加单元格
     */
    public Cell addCell(Excel attr, Row row, T vo, Field field, int column, T voPrevious, int thisLine){
		... ...
		// 用于读取对象中的属性
        Object value = getTargetValue(vo, field, attr);

		//主要新增
        //记录上一个对象属性
        Object valuePrevious = null;
        if(voPrevious != null) {
        	valuePrevious = getTargetValue(voPrevious, field, attr);
        }
		... ...

		... ...
				//主要新增
				//快速合并
                if(isQuickSpan) {

                    if (quickSpanRule.value() == Excel.QuickSpanRule.IDENTICAL_ROW_MERGE.value()) {
                        ExcelFieldAdditionParams fieldAdditionParams = this.fieldAdditionParamsMap.get(index);
                        if (value.equals(valuePrevious)) {
                            if (fieldAdditionParams.getMergeRowStart() == 0) {
                               fieldAdditionParams.setMergeRowStart(thisLine - 1);
                            }
                            fieldAdditionParams.setMergeRowEnd(thisLine);
                            //this.mergeRowEnd = thisLine;
                            System.out.println(fieldAdditionParams);
                        } else {
                            if (fieldAdditionParams.getMergeRowStart() != 0 && fieldAdditionParams.getMergeRowEnd()  != 0) {
                                if (fieldAdditionParams.getMergeRowStart() != fieldAdditionParams.getMergeRowEnd()) {
                                    CellRangeAddress region = new CellRangeAddress(fieldAdditionParams.getMergeRowStart(), fieldAdditionParams.getMergeRowEnd(), index.intValue(), index.intValue());
                                    sheet.addMergedRegion(region);
                                    //this.mergeRowStart = 0;
                                    //this.mergeRowEnd = 0;
                                }

                                //多列同时设置这两个数据会混乱 解决方法:每一列生成一个单独的对象,相互独立
                                //this.mergeRowStart = 0;
                                //this.mergeRowEnd = 0;
                                fieldAdditionParams.setMergeRowStart(0);
                                fieldAdditionParams.setMergeRowEnd(0);
                            }
                        }
                    }

                }
		... ...
	}
VO对象使用

主要是两个区域列

  /**
   * 主键
   */
  @JsonSerialize(using = ToStringSerializer.class)
  private Long id;

  /**
   * 项目行code
   */
  private Integer code;
  /**
   * 单位
   */
  @Excel(name = "单位", index = 0, spanRow = "0,13|14,17|21,27|35,36", spanCell = "18,20,0,1|28,30,0,1|31,31,0,1|32,33,0,1|34,34,0,8|37,37,1,4|37,37,6,7|38,38,1,8")
  @ExcelProperty("单位")
  private String mine;
  /**
   * 左侧-区域
   */
  @Excel(name = "区域", index = 1, isQuickSpan=true, quickSpanRule = Excel.QuickSpanRule.IDENTICAL_ROW_MERGE)
  @ExcelProperty("区域")
  private String firstQY;
  /**
   * 左侧-矿废石
   */
  @Excel(name = "矿废石", index = 2)
  @ExcelProperty("矿废石")
  private String firstKfs;
  /**
   * 左侧-车斗
   */
  @Excel(name = "车(斗)", index = 3)
  @ExcelProperty("车(斗)")
  private String firstCar;
  /**
   * 左侧-吨数
   */
  @Excel(name = "吨", index = 4)
  @ExcelProperty("吨")
  private String firstWeight;


  /**
   * 右侧-区域
   */
  @Excel(name = "区域", index = 5, isQuickSpan=true, quickSpanRule = Excel.QuickSpanRule.IDENTICAL_ROW_MERGE)
  @ExcelProperty("区域")
  private String secondQY;
  /**
   * 右侧-矿废石
   */
  @Excel(name = "矿废石", index = 6)
  @ExcelProperty("矿废石")
  private String secondKfs;
  /**
   * 右侧-车斗
   */
  @Excel(name = "车(斗)", index = 7)
  @ExcelProperty("车(斗)")
  private String secondCar;
  /**
   * 右侧-吨数
   */
  @Excel(name = "吨", index = 8)
  @ExcelProperty("吨")
  private String secondWeight;

规则使用说明

使用时最好先设置好index字段索引参数

1、开启快速合并isQuickSpan=true

2、设置快速合并规则quickSpanRule = Excel.QuickSpanRule.IDENTICAL_ROW_MERGE

导出效果

因工作保密原因,不贴图了,可自行测试,望伙计们海含!

灵活的单元格背景颜色渲染

改变单元格背景色方法

两种方式:

  • POI自带颜色根据IndexedColors

    //索引值背景色改变
    CellStyle style = wb.createCellStyle();
    style.setFillForegroundColor(IndexedColors.RED.getIndex());
    style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
    cell.setCellStyle(style);
    
  • 自定义16进制颜色代码

清楚的知道自定义16进制颜色代码非常灵活,则采用这种方式,但是若依自带的ExcelUtil不支持这种,好吧还是自己扩展,既然要扩展就要考虑灵活和扩展性,我理想的状态是:

1、如果你想要单独对某个单元格进行固定的自定义颜色值的背景色改变,直接可以在@Excel注解上设置参数,比较方便。我称之为自定义单元格颜色

2、有可能单元格的颜色是通过真实的业务数据计算所决定的,比如比某个数大就红色,反之就绿色等,这种情况下我希望是给ExcelExtendUtil类设置约定好的改变颜色规则,至于最终的颜色值可以是在业务层就计算处理好了,只需要组合成约定好的颜色规则Setter就可以了,我称之为自定义动态单元格颜色

自定义单元格颜色

@Excel注解新增参数
	/**
     * 是否根据自定义颜色值变色
     * @return
     */
    public boolean isAlertBackgroundColorByCustom() default false;

    /**
     * 自定义颜色值
     * @return
     */
    public String colorValueCustom() default "";

    /**
     * 自定改变颜色指定行索引
     * @return
     */
    public int alertBackgroundColorByCustomRowIndex() default 0;
ExcelExtendUtil实现代码

1、新增方法

 	/**
     * 处理单元格背景颜色根据16进制颜色代码
     * @param cell
     * @param color
     */
    public void handleHexBackgroundColor(Cell cell, String color) {

        try {
            if(StringUtils.isEmpty(color)) throw new ServiceException("颜色代码为空!");
            if(!color.contains("#")) throw new ServiceException("规则中的颜色代码格式不正确,请使用颜色16进制代码!");
            String rgbS = color.substring(1);
            byte[] rgbB = new byte[0]; // get byte array from hex string
            rgbB = Hex.decodeHex(rgbS);
            XSSFColor finalColor = new XSSFColor(rgbB, null); //IndexedColorMap has no usage until now. So it can be set null.
            XSSFCellStyle style = (XSSFCellStyle) wb.createCellStyle();
            style.setAlignment(HorizontalAlignment.CENTER);
            style.setVerticalAlignment(VerticalAlignment.CENTER);
            style.setBorderRight(BorderStyle.THIN);
            style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderLeft(BorderStyle.THIN);
            style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderTop(BorderStyle.THIN);
            style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderBottom(BorderStyle.THIN);
            style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            Font dataFont = wb.createFont();
            dataFont.setFontName("Arial");
            dataFont.setFontHeightInPoints((short) 10);
            style.setFont(dataFont);
            style.setWrapText(true);
            style.setFillForegroundColor(finalColor);
            style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            cell.setCellStyle(style);
        } catch (DecoderException e) {
            e.printStackTrace();
        }

    }

2、addCell()方法逻辑修改

// 创建cell
cell = row.createCell(column);
int align = attr.align().value();

int rowIndex = row.getRowNum();


Cell finalCell = cell;
... ...

// TODO 处理自定义颜色
if(attr.isAlertBackgroundColorByCustom()) {
    if(StringUtils.isEmpty(attr.colorValueCustom())) throw new ServiceException("@Excel为设置自定义颜色值!");
    if(rowIndex == attr.alertBackgroundColorByCustomRowIndex() + this.tableDataStartIndex) {
        String color = attr.colorValueCustom();
        if(StringUtils.isNotEmpty(color)) {
            this.handleHexBackgroundColor(finalCell, color);
        }
    }
}


//默认样式
if(!attr.isAlertBackgroundColorByDynamic() && !attr.isAlertBackgroundColorByCustom()) {
    cell.setCellStyle(styles.get("data" + (align >= 1 && align <= 3 ? align : "")));
}

VO对象使用
package com.ruoyi.system.controller.dispatch.statistical.dto.winners;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.ruoyi.common.core.annotation.Excel;
import com.ruoyi.system.controller.dispatch.quality.centerdown.ScddQualityCenterdownVo;
import com.ruoyi.system.controller.dispatch.statistical.dto.proDataReport.ProductionDataOperationEnum;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

import java.math.BigDecimal;
import java.util.Map;
import java.util.Set;

/**
 * 龙虎榜展示模型
 *
 */
@Data
@Accessors(chain = true)
public class WinnersVo{

  /**
   * 主键
   */
  @JsonSerialize(using = ToStringSerializer.class)
  private Long id;

  /**
   * 项目行code
   */
  private Integer code;

  /**
   * 竞赛项
   */
  @Excel(name = "竞赛项", index = 0, spanRow = "0,4|5,8|9,11|12,15|18,22", spanCell = "17,17,0,3|18,18,2,3|19,19,2,3|20,20,2,3|21,21,2,3|22,22,2,3")
  private String competitiveItem;
  /**
   * 单位(部门名称)
   */
  @Excel(name = "单位名称", index = 1)
  private String deptName;


  /**
   * 单位(计量)
   */
  @Excel(name = "单位" , index = 2)
  private String unit;

  /**
   * 月计划指标
   */
  @Excel(name = "月计划指标", index = 3)
  private String monthPlanTarget;

  /**
   * 计划指标
   */
  @Excel(name = "计划指标", index = 4, isAlertBackgroundColorByCustom = true, colorValueCustom = "#ffff00", alertBackgroundColorByCustomRowIndex = 17)
  private String planTarget;


  /**
   * 完成量
   */
  @Excel(name = "完成量", index = 5, isAlertBackgroundColorByDynamic = true, colorValueField = "color", isAlertBackgroundColorByCustom = true, colorValueCustom = "#5b9bd5", alertBackgroundColorByCustomRowIndex = 17)
  private String performance;


  /**
   * 超产量
   */
  @Excel(name = "超产量", index = 6, isAlertBackgroundColorByCustom = true, colorValueCustom = "#00b050", alertBackgroundColorByCustomRowIndex = 17)
  private String overproduction;


  /**
   * 超产比例
   */
  @Excel(name = "超产比例", index = 7,  isAlertBackgroundColorByCustom = true, colorValueCustom = "#f3541a", alertBackgroundColorByCustomRowIndex = 17)
  private String overproductionRatio;


  /**
   * 固定 计划指标
   */

  private String finalPlanTarget;

  /**
   * 固定 进度
   */

  private String process;



  /**
   * 计划指标-时间范围
   */
  @Excel(name = "应完成量", index = 8, isAlertBackgroundColorByCustom = true, colorValueCustom = "#ffff00", alertBackgroundColorByCustomRowIndex = 17)
  private String rangePlanTarget;


  /**
   * 完成量-时间范围
   */
  @Excel(name = "实际完成量", index = 9, isAlertBackgroundColorByDynamic = true, colorValueField = "rangeColor", isAlertBackgroundColorByCustom = true, colorValueCustom = "#5b9bd5", alertBackgroundColorByCustomRowIndex = 17)
  private String rangePerformance;


  /**
   * 超产量-时间范围
   */
  @Excel(name = "实际超产量", index = 10, isAlertBackgroundColorByCustom = true, colorValueCustom = "#00b050", alertBackgroundColorByCustomRowIndex = 17)
  private String rangeOverproduction;


  /**
   * 超产比例-时间范围
   */
  @Excel(name = "实际超产比", index = 11,  isAlertBackgroundColorByCustom = true, colorValueCustom = "#f3541a", alertBackgroundColorByCustomRowIndex = 17)
  private String rangeOverproductionRatio;


  /**
   * 备注
   */
  private String remark;

  /**
   * 颜色
   */
  private String color;

  /**
   * 颜色-时间范围
   */
  private String rangeColor;

  /**
   * 小数位数
   */
  private Integer scale;

  /**
   * 处理规则
   * ROUND_DOWN
   * ROUND_CEILING
   * ROUND_FLOOR
   * ROUND_HALF_UP
   * ROUND_HALF_DOWN
   * ROUND_HALF_EVEN
   * ROUND_UNNECESSARY
   */
  private Integer scaleRule;

  /**
   * 计划量唯一标识简单
   * 规则:
   * 如果有字典id 规则为:模块名字_字典数据id
   * 如果没有字典id有code 规则为:模块名字_code
   * 如果以上两者都没有 规则为:模块名字_名称_类型
   */
  private Set<String> performanceSoleSign;


  /**
   * 完成量唯一标识简单
   * 规则:
   * 如果有字典id 规则为:模块名字_字典数据id
   * 如果没有字典id有code 规则为:模块名字_code
   * 如果以上两者都没有 规则为:模块名字_名称_类型
   */
  private Set<String> planTargetSoleSign;



  /**
   * 操作指令
   */
  private ProductionDataOperationEnum operationEnum;


  /**
   * 当操作指令为SUM_SIMPLE_COEFFICIENT时 可传入此参数进行参数计算
   */
  private BigDecimal simpleCoefficient;


  /**
   * 当操作指令为SUM_MAP_COEFFICIENT时 可传入此参数进行参数计算
   */
  private BigDecimal mapCoefficient;



  /**
   * 当其中操作指令为CUSTOM时 可传入自定义参数
   */
  private Map<String, Object> customParams;





}

规则使用说明

1、isAlertBackgroundColorByCustom = true 开启自定义单元格颜色

2、colorValueCustom = “#ffff00” 设置颜色值

3、alertBackgroundColorByCustomRowIndex = 17 指定改变颜色单元格所在行索引,从0开始(列索引不用传,会自动对应,也从0开始)

导出效果

主要看竞赛项一行的单元格颜色,即行索引为17
在这里插入图片描述

自定义动态单元格颜色

@Excel注解新增参数
 	/**
     * 是否根据动态数据改变背景色
     * @return
     */
    public boolean isAlertBackgroundColorByDynamic() default false;

    /**
     * 指定根据列表数据实体类的属性进行变色,默认为color属性
     *
     * @return
     */
    public String colorValueField() default "color";

ExcelExtendUtil实现代码

1、新增属性

/**
 * 根据动态数据改变单元格颜色规则
 * 需与isAlertBackgroundColorByDynamic配合使用
 */
private Map<Integer, Map<String, String>> alertBackgroundColorByDynamicRuleMap;

2、addCell()方法逻辑处理

				// 创建cell
                cell = row.createCell(column);
                int align = attr.align().value();


                int rowIndex = row.getRowNum();


                Cell finalCell = cell;
                // TODO 处理动态数据改变颜色
                if(attr.isAlertBackgroundColorByDynamic()) {
                    if(this.alertBackgroundColorByDynamicRuleMap == null) throw new ServiceException("未设置动态数据改变颜色规则集合!");
                    if(StringUtils.isEmpty(attr.colorValueField())) throw new ServiceException("@Excel为设置定数据实体类的颜色对应属性!");
                    this.alertBackgroundColorByDynamicRuleMap.entrySet().stream().forEach(i->{
                        Map<String, String> colorMap = i.getValue();
                        if(rowIndex == i.getKey()+this.tableDataStartIndex) {
                            String color = colorMap.get(attr.colorValueField());
                            if(StringUtils.isNotEmpty(color)) {

                               this.handleHexBackgroundColor(finalCell, color);

                            }
                        }
                    });

                }

               ... ...


                //默认样式
                if(!attr.isAlertBackgroundColorByDynamic() && !attr.isAlertBackgroundColorByCustom()) {
                    cell.setCellStyle(styles.get("data" + (align >= 1 && align <= 3 ? align : "")));
                }
VO对象使用

自定义单元格颜色参数与其公用并不冲突!

package com.ruoyi.system.controller.dispatch.statistical.dto.winners;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.ruoyi.common.core.annotation.Excel;
import com.ruoyi.system.controller.dispatch.quality.centerdown.ScddQualityCenterdownVo;
import com.ruoyi.system.controller.dispatch.statistical.dto.proDataReport.ProductionDataOperationEnum;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

import java.math.BigDecimal;
import java.util.Map;
import java.util.Set;

/**
 * 龙虎榜展示模型
 *
 */
@Data
@Accessors(chain = true)
public class WinnersVo{

 ... ...
  /**
   * 完成量
   */
  @Excel(name = "完成量", index = 5, isAlertBackgroundColorByDynamic = true, colorValueField = "color", isAlertBackgroundColorByCustom = true, colorValueCustom = "#5b9bd5", alertBackgroundColorByCustomRowIndex = 17)
  private String performance;


 

  /**
   * 完成量-时间范围
   */
  @Excel(name = "实际完成量", index = 9, isAlertBackgroundColorByDynamic = true, colorValueField = "rangeColor", isAlertBackgroundColorByCustom = true, colorValueCustom = "#5b9bd5", alertBackgroundColorByCustomRowIndex = 17)
  private String rangePerformance;


 ... ...

}

Controller使用

以下示例注意://list列表中每条数据存储着color和rangeColor属性,是根据业务已经计算好的对应的颜色值,color和rangeColor需与Vo对象属性@Excel注解的colorValueField参数值对应

 /**
     * 导出龙虎榜
     */
    @Log(title = "龙虎榜", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, ProDataReportQuery query)
    {
        try {
            List<WinnersVo> list = winnersService.getList(query);
            Map<Integer, Map<String, String>> alertBackgroundColorByDynamicRuleMap = new HashMap<>();
			
			//组合动态颜色规则			
			//list列表中每条数据存储着color和rangeColor属性,是根据业务已经计算好的对应的颜色值,color和rangeColor需与Vo对象属性@Excel注解的colorValueField参数值对应
            AtomicReference<Integer> index = new AtomicReference<>(0);
            list.stream().forEach(i-> {
                Map<String, String> temp = new HashMap<>();
                temp.put("color", i.getColor());
                temp.put("rangeColor", i.getRangeColor());
                alertBackgroundColorByDynamicRuleMap.put(index.get(), temp);
               index.getAndSet(index.get() + 1);
            });

            ExcelExtendUtil<WinnersVo> util = new ExcelExtendUtil<>(WinnersVo.class);
			//设置表标题所占行索引
            util.setTitleRowIndex(0);
			//设置动态颜色规则
            util.setAlertBackgroundColorByDynamicRuleMap(alertBackgroundColorByDynamicRuleMap);
			//开启多级表头
            util.setOpenMultilevelTableTop(true);
			//设置多级表头Json
            util.setTableTopJsonData("[\n" +
                    "      {\n" +
                    "        \"code\":\"competitiveItem\",\n" +
                    "        \"name\":\"竞赛项\",\n" +
                    "        \"colIndex\": 0,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": \"0,1\",\n" +
                    "        \"spanCol\": null\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"code\":\"deptName\",\n" +
                    "        \"name\":\"单位名称\",\n" +
                    "        \"colIndex\": 1,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": \"0,1\",\n" +
                    "        \"spanCol\": null\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"code\":\"unit\",\n" +
                    "        \"name\":\"单位\",\n" +
                    "        \"colIndex\": 2,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": \"0,1\",\n" +
                    "        \"spanCol\": null\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"code\":\"monthPlanTarget\",\n" +
                    "        \"name\":\"月计划指标\",\n" +
                    "        \"colIndex\": 3,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": \"0,1\",\n" +
                    "        \"spanCol\": null\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"children\":[\n" +
                    "          {\n" +
                    "            \"code\":\"planTarget\",\n" +
                    "            \"name\":\"计划指标\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 4,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"performance\",\n" +
                    "            \"name\":\"完成量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 5,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"overproduction\",\n" +
                    "            \"name\":\"超产量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 6,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"overproductionRatio\",\n" +
                    "            \"name\":\"超产比例\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 7,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          }\n" +
                    "        ],\n" +
                    "        \"code\":\"-1\",\n" +
                    "        \"name\":\"日进度完成情况\",\n" +
                    "        \"colIndex\": 4,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": null,\n" +
                    "        \"spanCol\": \"4,7\"\n" +
                    "      },\n" +
                    "      {\n" +
                    "        \"children\":[\n" +
                    "          {\n" +
                    "            \"code\":\"rangePlanTarget\",\n" +
                    "            \"name\":\"应完成量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 8,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"rangePerformance\",\n" +
                    "            \"name\":\"实际完成量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 9,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"rangeOverproduction\",\n" +
                    "            \"name\":\"实际超产量\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 10,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          },\n" +
                    "          {\n" +
                    "            \"code\":\"rangeOverproductionRatio\",\n" +
                    "            \"name\":\"实际超产比\",\n" +
                    "            \"parentCode\":\"-1\",\n" +
                    "            \"colIndex\": 11,\n" +
                    "            \"rowIndex\": 1,\n" +
                    "            \"spanRow\": null,\n" +
                    "            \"spanCol\": null\n" +
                    "          }\n" +
                    "        ],\n" +
                    "        \"code\":\"-1\",\n" +
                    "        \"name\":\"按时间进度完成情况\",\n" +
                    "        \"colIndex\": 8,\n" +
                    "        \"rowIndex\": 0,\n" +
                    "        \"spanRow\": null,\n" +
                    "        \"spanCol\": \"8,11\"\n" +
                    "      }\n" +
                    "\n" +
                    "    ]");
            System.out.println(util);
            util.exportExcel(response, list, "生产调度-统计分析-龙虎榜", "龙虎榜");
        } catch (Exception e) {
            e.printStackTrace();
            throw new ServiceException("导出失败!");
        }

    }
规则使用说明

1、isAlertBackgroundColorByDynamic = true是否开启自定义动态单元格颜色

2、colorValueField = "rangeColor"动态颜色规则中颜色Map的key值,则取颜色Map中rangeColor的16进制颜色值

3、注意colorValueField和Controller传入的动态颜色规则Map中颜色Map取数的key的对应关系

导出效果

在这里插入图片描述

完整代码

@Excel

package com.ruoyi.common.core.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.math.BigDecimal;
import com.ruoyi.common.core.utils.poi.ExcelHandlerAdapter;

/**
 * 自定义导出Excel数据注解
 *
 * @author ruoyi
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel
{
    /**
     * 导出时在excel中排序
     */
    public int sort() default Integer.MAX_VALUE;

    /**
     * 导出到Excel中的名字.
     */
    public String name() default "";

    /**
     * 日期格式, 如: yyyy-MM-dd
     */
    public String dateFormat() default "";

    /**
     * 读取内容转表达式 (如: 0=男,1=女,2=未知)
     */
    public String readConverterExp() default "";

    /**
     * 分隔符,读取字符串组内容
     */
    public String separator() default ",";

    /**
     * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化)
     */
    public int scale() default -1;

    /**
     * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
     */
    public int roundingMode() default BigDecimal.ROUND_HALF_EVEN;

    /**
     * 导出类型(0数字 1字符串)
     */
    public ColumnType cellType() default ColumnType.STRING;

    /**
     * 导出时在excel中每个列的高度 单位为字符
     */
    public double height() default 14;

    /**
     * 导出时在excel中每个列的宽 单位为字符
     */
    public double width() default 16;

    /**
     * 文字后缀,如% 90 变成90%
     */
    public String suffix() default "";

    /**
     * 当值为空时,字段的默认值
     */
    public String defaultValue() default "";

    /**
     * 提示信息
     */
    public String prompt() default "";

    /**
     * 设置只能选择不能输入的列内容.
     */
    public String[] combo() default {};

    /**
     * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写.
     */
    public boolean isExport() default true;

    /**
     * 另一个类中的属性名称,支持多级获取,以小数点隔开
     */
    public String targetAttr() default "";

    /**
     * 是否自动统计数据,在最后追加一行统计数据总和
     */
    public boolean isStatistics() default false;

    /**
     * 导出字段对齐方式(0:默认;1:靠左;2:居中;3:靠右)
     */
    public Align align() default Align.AUTO;

    /**
     * 自定义数据处理器
     */
    public Class<?> handler() default ExcelHandlerAdapter.class;

    /**
     * 自定义数据处理器参数
     */
    public String[] args() default {};

    /**
     * 合并列
     * @return
     */
    public String spanCell() default "";

    /**
     * 合并行
     * @return
     */
    public String spanRow() default "";


    /**
     * 字段索引
     * @return
     */
    public long index() default -1;

    /**
     * 是否快速合并
     */
    public boolean isQuickSpan() default false;

    /**
     * 是否根据动态数据改变背景色
     * @return
     */
    public boolean isAlertBackgroundColorByDynamic() default false;

    /**
     * 指定根据列表数据实体类的属性进行变色,默认为color属性
     *
     * @return
     */
    public String colorValueField() default "color";

    /**
     * 是否根据自定义颜色值变色
     * @return
     */
    public boolean isAlertBackgroundColorByCustom() default false;

    /**
     * 自定义颜色值
     * @return
     */
    public String colorValueCustom() default "";

    /**
     * 自定改变颜色指定行索引
     * @return
     */
    public int alertBackgroundColorByCustomRowIndex() default 0;

    /**
     * 快速合并规则
     * @return
     */
    public QuickSpanRule quickSpanRule() default QuickSpanRule.NULL ;


    /**
     * 快速合并规则枚举
     */
    public enum QuickSpanRule
    {
        NULL(-1, "无规则"),
        IDENTICAL_ROW_MERGE(0, "相同行合并");

        private final int value;
        private final String desc;

        QuickSpanRule(int value, String desc)
        {
            this.value = value;
            this.desc = desc;
        }

        public int value()
        {
            return this.value;
        }
    }

    public enum Align
    {
        AUTO(0), LEFT(1), CENTER(2), RIGHT(3);
        private final int value;

        Align(int value)
        {
            this.value = value;
        }

        public int value()
        {
            return this.value;
        }
    }

    /**
     * 字段类型(0:导出导入;1:仅导出;2:仅导入)
     */
    Type type() default Type.ALL;

    public enum Type
    {
        ALL(0), EXPORT(1), IMPORT(2);
        private final int value;

        Type(int value)
        {
            this.value = value;
        }

        public int value()
        {
            return this.value;
        }
    }

    public enum ColumnType
    {
        NUMERIC(0), STRING(1), IMAGE(2);
        private final int value;

        ColumnType(int value)
        {
            this.value = value;
        }

        public int value()
        {
            return this.value;
        }
    }
}

ExcelExtendUtil

package com.ruoyi.system.common.util;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.poi.ExcelHandlerAdapter;
import io.swagger.models.auth.In;
import lombok.Data;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.core.annotation.Excel;
import com.ruoyi.common.core.annotation.Excel.ColumnType;
import com.ruoyi.common.core.annotation.Excel.Type;
import com.ruoyi.common.core.annotation.Excels;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.file.FileTypeUtils;
import com.ruoyi.common.core.utils.file.ImageUtils;
import com.ruoyi.common.core.utils.reflect.ReflectUtils;

/**
 * Excel扩展相关处理
 *
 * @author lotoze
 */
public class ExcelExtendUtil<T>
{
    private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class);

    public static final String[] FORMULA_STR = { "=", "-", "+", "@" };

    /**
     * Excel sheet最大行数,默认65536
     */
    public static final int sheetSize = 65536;

    /**
     * 工作表名称
     */
    private String sheetName;

    /**
     * 导出类型(EXPORT:导出数据;IMPORT:导入模板)
     */
    private Type type;

    /**
     * 工作薄对象
     */
    private Workbook wb;

    /**
     * 工作表对象
     */
    private Sheet sheet;

    /**
     * 样式列表
     */
    private Map<String, CellStyle> styles;

    /**
     * 导入导出数据列表
     */
    private List<T> list;

    /**
     * 注解列表
     */
    private List<Object[]> fields;

    /**
     * 当前行号
     */
    private int rownum;

    /**
     * 标题索引
     */
    private Integer titleRowIndex;

    /**
     * 标题
     */
    private String title;

    /**
     * 最大高度
     */
    private short maxHeight;

    /**
     * 统计列表
     */
    private Map<Integer, Double> statistics = new HashMap<Integer, Double>();

    /**
     * 数字格式
     */
    private static final DecimalFormat DOUBLE_FORMAT = new DecimalFormat("######0.00");

    /**
     * 实体对象
     */
    public Class<T> clazz;

    /**
     * 表头开始的索引
     */
    private int tableTopStartIndex = 1;

    /**
     * 表数据开始的索引
     */
    private int tableDataStartIndex = 2;

    /**
     * 表头行映射 创建时复用
     */
    private  Map<Integer, Row> tableTopRowMap = new HashMap<>();


    /**
     * 是否开启多级表头
     */
    private boolean openMultilevelTableTop = false;

    /**
     * 表头所占最大行索引
     */
    private int tableTopMaxIndex = 1;

    /**
     * 表头Json数据
     */
    private String tableTopJsonData;

    /**
     * 表头数据集合列表
     */
    private List<JsonData> tableTopJsonDataList;

    /**
     * 快速合并计算附加参数
     */
    private Map<Long, ExcelFieldAdditionParams>  fieldAdditionParamsMap;

    /**
     * 根据动态数据改变单元格颜色规则
     * 需与isAlertBackgroundColorByDynamic配合使用
     */
    private Map<Integer, Map<String, String>> alertBackgroundColorByDynamicRuleMap;


    public ExcelExtendUtil(Class<T> clazz)
    {
        this.clazz = clazz;
    }

    public void init(List<T> list, String sheetName, String title, Type type)
    {
        if (list == null)
        {
            list = new ArrayList<T>();
        }
        this.list = list;
        this.sheetName = sheetName;
        this.type = type;
        this.title = title;
        createExcelField();
        createWorkbook();
        createTitle();
    }

    /**
     * 创建excel第一行标题
     */
    public void createTitle()
    {
        if (StringUtils.isNotEmpty(title))
        {
            Row titleRow = sheet.createRow(titleRowIndex == null ? 0 : titleRowIndex);
            titleRow.setHeightInPoints(30);
            Cell titleCell = titleRow.createCell(0);
            titleCell.setCellStyle(styles.get("title"));
            titleCell.setCellValue(title);
            sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(),
                    this.fields.size() - 1));
        }
    }

    /**
     * 对excel表单默认第一个索引名转换成list
     *
     * @param is 输入流
     * @return 转换后集合
     */
    public List<T> importExcel(InputStream is) throws Exception
    {
        return importExcel(is, 0);
    }

    /**
     * 对excel表单默认第一个索引名转换成list
     *
     * @param is 输入流
     * @param titleNum 标题占用行数
     * @return 转换后集合
     */
    public List<T> importExcel(InputStream is, int titleNum) throws Exception
    {
        return importExcel(StringUtils.EMPTY, is, titleNum);
    }

    /**
     * 对excel表单指定表格索引名转换成list
     *
     * @param sheetName 表格索引名
     * @param titleNum 标题占用行数
     * @param is 输入流
     * @return 转换后集合
     */
    public List<T> importExcel(String sheetName, InputStream is, int titleNum) throws Exception
    {
        this.type = Type.IMPORT;
        this.wb = WorkbookFactory.create(is);
        List<T> list = new ArrayList<T>();
        // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
        Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);
        if (sheet == null)
        {
            throw new IOException("文件sheet不存在");
        }

        // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1
        int rows = sheet.getLastRowNum();

        if (rows > 0)
        {
            // 定义一个map用于存放excel列的序号和field.
            Map<String, Integer> cellMap = new HashMap<String, Integer>();
            // 获取表头
            Row heard = sheet.getRow(titleNum);
            for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++)
            {
                Cell cell = heard.getCell(i);
                if (StringUtils.isNotNull(cell))
                {
                    String value = this.getCellValue(heard, i).toString();
                    cellMap.put(value, i);
                }
                else
                {
                    cellMap.put(null, i);
                }
            }
            // 有数据时才处理 得到类的所有field.
            List<Object[]> fields = this.getFields();
            Map<Integer, Object[]> fieldsMap = new HashMap<Integer, Object[]>();
            for (Object[] objects : fields)
            {
                Excel attr = (Excel) objects[1];
                Integer column = cellMap.get(attr.name());
                if (column != null)
                {
                    fieldsMap.put(column, objects);
                }
            }
            for (int i = titleNum + 1; i <= rows; i++)
            {
                // 从第2行开始取数据,默认第一行是表头.
                Row row = sheet.getRow(i);
                // 判断当前行是否是空行
                if (isRowEmpty(row))
                {
                    continue;
                }
                T entity = null;
                for (Map.Entry<Integer, Object[]> entry : fieldsMap.entrySet())
                {
                    Object val = this.getCellValue(row, entry.getKey());

                    // 如果不存在实例则新建.
                    entity = (entity == null ? clazz.newInstance() : entity);
                    // 从map中得到对应列的field.
                    Field field = (Field) entry.getValue()[0];
                    Excel attr = (Excel) entry.getValue()[1];
                    // 取得类型,并根据对象类型设置值.
                    Class<?> fieldType = field.getType();
                    if (String.class == fieldType)
                    {
                        String s = Convert.toStr(val);
                        if (StringUtils.endsWith(s, ".0"))
                        {
                            val = StringUtils.substringBefore(s, ".0");
                        }
                        else
                        {
                            String dateFormat = field.getAnnotation(Excel.class).dateFormat();
                            if (StringUtils.isNotEmpty(dateFormat))
                            {
                                val = parseDateToStr(dateFormat, (Date) val);
                            }
                            else
                            {
                                val = Convert.toStr(val);
                            }
                        }
                    }
                    else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val)))
                    {
                        val = Convert.toInt(val);
                    }
                    else if ((Long.TYPE == fieldType || Long.class == fieldType) && StringUtils.isNumeric(Convert.toStr(val)))
                    {
                        val = Convert.toLong(val);
                    }
                    else if (Double.TYPE == fieldType || Double.class == fieldType)
                    {
                        val = Convert.toDouble(val);
                    }
                    else if (Float.TYPE == fieldType || Float.class == fieldType)
                    {
                        val = Convert.toFloat(val);
                    }
                    else if (BigDecimal.class == fieldType)
                    {
                        val = Convert.toBigDecimal(val);
                    }
                    else if (Date.class == fieldType)
                    {
                        if (val instanceof String)
                        {
                            val = DateUtils.parseDate(val);
                        }
                        else if (val instanceof Double)
                        {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    }
                    else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
                    {
                        val = Convert.toBool(val, false);
                    }
                    if (StringUtils.isNotNull(fieldType))
                    {
                        String propertyName = field.getName();
                        if (StringUtils.isNotEmpty(attr.targetAttr()))
                        {
                            propertyName = field.getName() + "." + attr.targetAttr();
                        }
                        else if (StringUtils.isNotEmpty(attr.readConverterExp()))
                        {
                            val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator());
                        }
                        else if (!attr.handler().equals(ExcelHandlerAdapter.class))
                        {
                            val = dataFormatHandlerAdapter(val, attr);
                        }
                        ReflectUtils.invokeSetter(entity, propertyName, val);
                    }
                }
                list.add(entity);
            }
        }
        return list;
    }

    /**
     * 对list数据源将其里面的数据导入到excel表单
     *
     * @param response 返回数据
     * @param list 导出数据集合
     * @param sheetName 工作表的名称
     * @return 结果
     * @throws IOException
     */
    public void exportExcel(HttpServletResponse response, List<T> list, String sheetName)
    {
        exportExcel(response, list, sheetName, StringUtils.EMPTY);
    }

    /**
     * 对list数据源将其里面的数据导入到excel表单
     *
     * @param response 返回数据
     * @param list 导出数据集合
     * @param sheetName 工作表的名称
     * @param title 标题
     * @return 结果
     * @throws IOException
     */
    public void exportExcel(HttpServletResponse response, List<T> list, String sheetName, String title)
    {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        this.init(list, sheetName, title, Type.EXPORT);
        exportExcel(response);
    }

    /**
     * 对list数据源将其里面的数据导入到excel表单
     *
     * @param sheetName 工作表的名称
     * @return 结果
     */
    /**
     * 对list数据源将其里面的数据导入到excel表单
     *
     * @param sheetName 工作表的名称
     * @return 结果
     */
    public void importTemplateExcel(HttpServletResponse response, String sheetName)
    {
        importTemplateExcel(response, sheetName, StringUtils.EMPTY);
    }

    /**
     * 对list数据源将其里面的数据导入到excel表单
     *
     * @param sheetName 工作表的名称
     * @param title 标题
     * @return 结果
     */
    public void importTemplateExcel(HttpServletResponse response, String sheetName, String title)
    {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        this.init(null, sheetName, title, Type.IMPORT);
        exportExcel(response);
    }

    /**
     * 对list数据源将其里面的数据导入到excel表单
     *
     * @return 结果
     */
    public void exportExcel(HttpServletResponse response)
    {
        try
        {
            writeSheet();
            wb.write(response.getOutputStream());
        }
        catch (Exception e)
        {
            log.error("导出Excel异常{}", e.getMessage());
        }
        finally
        {
            IOUtils.closeQuietly(wb);
        }
    }

    /**
     * 创建写入数据到Sheet
     */
    public void writeSheet()
    {
        // 取出一共有多少个sheet.
        int sheetNo = Math.max(1, (int) Math.ceil(list.size() * 1.0 / sheetSize));
        for (int index = 0; index < sheetNo; index++)
        {
            createSheet(sheetNo, index);

            // 产生表头信息开始一行

            this.tableTopStartIndex = this.titleRowIndex+1;
            Row row = sheet.createRow(this.tableTopStartIndex);

            int column = 0;
            // TODO 动态添加多级表头
            if(this.openMultilevelTableTop) {
                try {
                    this.createTableHead();
                    this.tableTopMaxIndex = tableTopRowMap.entrySet().stream().map(Map.Entry::getKey).max(Comparator.comparing(i->i)).get();
                } catch (Exception e) {
                    e.printStackTrace();
                }


            } else {
                //根据字段注解配置参数填充表头
                for (Object[] os : fields)
                {
                    Excel excel = (Excel) os[1];
                    this.createCell(excel, row, column++);
                }
            }
            //表数据开始索引计算
            this.tableDataStartIndex = this.tableTopStartIndex + this.tableTopMaxIndex;
            Row startDataRow = sheet.createRow(this.tableDataStartIndex);

            if (Type.EXPORT.equals(type))
            {
                fillExcelData(index, startDataRow);
                addStatisticsRow();
            }
        }
    }

    /**
     * 创建多级表头
     */
    public void createTableHead() {
        List<JsonData> list = new ArrayList<>();
        if(StringUtils.isNotEmpty(tableTopJsonData) || (StringUtils.isNotEmpty(tableTopJsonData) && this.tableTopJsonDataList != null)) {
            JSONArray jsonArray = JSONObject.parseArray(this.tableTopJsonData);
            list = jsonArray.toJavaList(JsonData.class);
        } else if(StringUtils.isEmpty(tableTopJsonData) && this.tableTopJsonDataList != null) {
            list = this.tableTopJsonDataList;
        } else {
            throw new ServiceException("请设置表头Json数据字符串或者列表!");
        }

        Row row = sheet.createRow(this.tableTopStartIndex);
        for(int i = 0; i < list.size(); i++) {
            this.recursionCreateTableTopCell(list.get(i), row);
        }

        //EasyExcel多表头格式
        //List<List<String>> headList = head(list);

    }

    /**
     * 递归-深度优先 创建表格单元格
     * @param jsonData
     * @param row
     * @return
     */
    public Cell recursionCreateTableTopCell(JsonData jsonData, Row row) {
        this.tableTopRowMap.put(row.getRowNum(), row);


        // 创建列
        Cell cell = row.createCell(jsonData.getColIndex().intValue());
        // 写入列信息
        cell.setCellValue(jsonData.getName());
        //setDataValidation2(jsonData.getColIndex().intValue());
        cell.setCellStyle(styles.get("header"));

        //合并规则信息
        String spanRow = jsonData.getSpanRow();
        String spanCol = jsonData.getSpanCol();
        //处理多级表头行合并
        if(spanRow != null) {
            String[] spanRowArray = spanRow.split(",");
            int spanRowStartIndex = Integer.parseInt(spanRowArray[0]);
            int spanRowEndIndex = Integer.parseInt(spanRowArray[1]);
            int spanColStartIndex = jsonData.getColIndex().intValue();
            int spanColEndIndex = jsonData.getColIndex().intValue();

            CellRangeAddress region = new CellRangeAddress(spanRowStartIndex+this.tableTopStartIndex, spanRowEndIndex+this.tableTopStartIndex, spanColStartIndex, spanColEndIndex);
            sheet.addMergedRegion(region);
        }
        //处理多级表头列合并
        if(spanCol != null) {
            String[] spanColArray = spanCol.split(",");
            int spanRowStartIndex = jsonData.getRowIndex().intValue();
            int spanRowEndIndex = jsonData.getRowIndex().intValue();
            int spanColStartIndex = Integer.parseInt(spanColArray[0]);
            int spanColEndIndex = Integer.parseInt(spanColArray[1]);

            CellRangeAddress region = new CellRangeAddress(spanRowStartIndex+this.tableTopStartIndex, spanRowEndIndex+this.tableTopStartIndex, spanColStartIndex, spanColEndIndex);
            sheet.addMergedRegion(region);
        }

        //判断是否有子集进入递归
        List<JsonData> children = jsonData.getChildren();
        if(children != null) {
            int rowIndex = row.getRowNum()+1;
            Row historyRow = this.tableTopRowMap.get(rowIndex);
            if(historyRow == null) {
                historyRow = sheet.createRow(rowIndex);
            }
            for(int i= 0; i< children.size(); i++) {
                JsonData sonJsonData = children.get(i);
                this.recursionCreateTableTopCell(sonJsonData, historyRow);
            }
        }

        return cell;
    }

    //public static void main(String[] args) {
    //    String data = getDataString();
    //    JSONObject jsonObject = JSONObject.parseObject(data);
    //    //读取
    //    JSONObject jsonObject1 = jsonObject.getJSONObject("data");
    //    JSONArray jsonArray = jsonObject1.getJSONArray("headerNodes");
    //
    //    List<JsonData> list = jsonArray.toJavaList(JsonData.class);
    //
    //    System.out.println(list);
    //    List<List<String>> head = head(list);
    //    System.out.println(head);
    //}

    /**
     * 解析的表头实例对象
     */
    @Data
    static class JsonData {
        /** 唯一标识 */
        private String code;
        /** 子集 */
        private List<JsonData> children;
        /** 父唯一标识 */
        private String parentCode;
        /** 单元格内容 */
        private String name;
        /** 列索引,从0开始 */
        private Long colIndex;
        /** 行索引,从0开始,使用时不考虑标题列 */
        private Long rowIndex;
        /** 行合并,比如0,4 */
        private String spanRow;
        /** 列合并,比如0,4 */
        private String spanCol;
    }

    /**
     * 处理表头数据-深度优先
     * @param data
     * @return
     */
    private static List<List<String>> head(List<JsonData> data) {
        List<List<String>> result = new ArrayList<>();

        List<String> temp = new ArrayList<>();

        // 深度优先搜索 使用递归
        for (JsonData jsonData : data) {
            dfs(result, jsonData, temp);
        }

        return result;
    }

    /**
     * 深度搜索复制
     * @param result
     * @param jsonData
     * @param temp
     */
    private static void dfs(List<List<String>> result, JsonData jsonData, List<String> temp) {
        List<JsonData> datas = jsonData.getChildren();
        temp.add(jsonData.getName());
        if (datas == null) {
            //看似浅复制,其实是深复制
            List<String> head = new ArrayList<>(temp);
            result.add(head);
        } else {
            for (JsonData data : datas) {
                dfs(result, data, temp);
            }
        }

        // 移除最后一个
        temp.remove(temp.size() - 1);
    }



    /**
     * 填充excel数据
     *
     * @param index 序号
     * @param row 单元格行
     */
    public void fillExcelData(int index, Row row)
    {
        int startNo = index;
        int endNo = Math.min(startNo + sheetSize, list.size());


        //当前行
        int thisLine = startNo;
        for (int i = startNo; i < endNo; i++) {
            if(i > 0) {
                row = sheet.createRow(row.getRowNum() + 1 - startNo);
            }
            thisLine = row.getRowNum();
            // 得到导出对象.
            T vo = (T) list.get(i);
            T voPrevious = null;
            //得到上一个导出对象
            if (i != startNo) {
                voPrevious = (T) list.get(i - 1);
            }
            /**
             *取下一个对象   与当前对象对比,如果相同,记住当前列,再与下一个对比,一直对比到不相同,执行合并代码
             * 注解加入 合并行列标识
             */
            int column = 0;
            for (Object[] os : fields) {
                Field field = (Field) os[0];
                Excel excel = (Excel) os[1];
                // 设置实体类私有属性可访问
                field.setAccessible(true);
                //每个列加入辅助参数对象
                this.addCell(excel, row, vo, field, column++, voPrevious, thisLine);

            }
        }

    }

    /**
     * 创建表格样式
     *
     * @param wb 工作薄对象
     * @return 样式列表
     */
    private Map<String, CellStyle> createStyles(Workbook wb)
    {
        // 写入各条记录,每条记录对应excel表中的一行
        Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
        CellStyle style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        Font titleFont = wb.createFont();
        titleFont.setFontName("Arial");
        titleFont.setFontHeightInPoints((short) 16);
        titleFont.setBold(true);
        style.setFont(titleFont);
        styles.put("title", style);

        style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setBorderRight(BorderStyle.THIN);
        style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setBorderLeft(BorderStyle.THIN);
        style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setBorderTop(BorderStyle.THIN);
        style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setBorderBottom(BorderStyle.THIN);
        style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
        Font dataFont = wb.createFont();
        dataFont.setFontName("Arial");
        dataFont.setFontHeightInPoints((short) 10);
        style.setFont(dataFont);
        style.setWrapText(true); //TODO 设置自动换行不起作用
        styles.put("data", style);

        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        Font headerFont = wb.createFont();
        headerFont.setFontName("Arial");
        headerFont.setFontHeightInPoints((short) 10);
        headerFont.setBold(true);
        headerFont.setColor(IndexedColors.WHITE.getIndex());
        style.setFont(headerFont);
        styles.put("header", style);

        style = wb.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        Font totalFont = wb.createFont();
        totalFont.setFontName("Arial");
        totalFont.setFontHeightInPoints((short) 10);
        style.setFont(totalFont);
        styles.put("total", style);

        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.LEFT);
        styles.put("data1", style);

        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.CENTER);
        styles.put("data2", style);

        style = wb.createCellStyle();
        style.cloneStyleFrom(styles.get("data"));
        style.setAlignment(HorizontalAlignment.RIGHT);
        styles.put("data3", style);

        return styles;
    }

    /**
     * 创建单元格
     */
    public Cell createCell(Excel attr, Row row, int column)
    {
        // 创建列
        Cell cell = row.createCell(column);
        // 写入列信息
        cell.setCellValue(attr.name());
        setDataValidation(attr, row, column);
        cell.setCellStyle(styles.get("header"));
        return cell;
    }

    /**
     * 设置单元格信息
     *
     * @param value 单元格值
     * @param attr 注解相关
     * @param cell 单元格信息
     */
    public void setCellVo(Object value, Excel attr, Cell cell)
    {
        if (ColumnType.STRING == attr.cellType())
        {
            String cellValue = Convert.toStr(value);
            // 对于任何以表达式触发字符 =-+@开头的单元格,直接使用tab字符作为前缀,防止CSV注入。
            if (StringUtils.containsAny(cellValue, FORMULA_STR))
            {
                cellValue = StringUtils.replaceEach(cellValue, FORMULA_STR, new String[] { "\t=", "\t-", "\t+", "\t@" });
            }
            cell.setCellValue(StringUtils.isNull(cellValue) ? attr.defaultValue() : cellValue + attr.suffix());
        }
        else if (ColumnType.NUMERIC == attr.cellType())
        {
            if (StringUtils.isNotNull(value))
            {
                // cell.setCellValue(StringUtils.contains(Convert.toStr(value), ".") ? Convert.toDouble(value) : Convert.toInt(value));
                if (StringUtils.contains(Convert.toStr(value), ".")) {
                    DataFormat df = this.wb.createDataFormat();
                    cell.getCellStyle().setDataFormat(df.getFormat("0.000_"));
                    cell.setCellValue(Convert.toDouble(value));
                } else {
                    DataFormat df = this.wb.createDataFormat();
                    cell.getCellStyle().setDataFormat(df.getFormat("0_"));
                    cell.setCellValue(Convert.toInt(value));
                }
            }
        }
        else if (ColumnType.IMAGE == attr.cellType())
        {
            ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() + 1), cell.getRow().getRowNum() + 1);
            String imagePath = Convert.toStr(value);
            if (StringUtils.isNotEmpty(imagePath))
            {
                byte[] data = ImageUtils.getImage(imagePath);
                getDrawingPatriarch(cell.getSheet()).createPicture(anchor,
                        cell.getSheet().getWorkbook().addPicture(data, getImageType(data)));
            }
        }
    }

    /**
     * 获取画布
     */
    public static Drawing<?> getDrawingPatriarch(Sheet sheet)
    {
        if (sheet.getDrawingPatriarch() == null)
        {
            sheet.createDrawingPatriarch();
        }
        return sheet.getDrawingPatriarch();
    }

    /**
     * 获取图片类型,设置图片插入类型
     */
    public int getImageType(byte[] value)
    {
        String type = FileTypeUtils.getFileExtendName(value);
        if ("JPG".equalsIgnoreCase(type))
        {
            return Workbook.PICTURE_TYPE_JPEG;
        }
        else if ("PNG".equalsIgnoreCase(type))
        {
            return Workbook.PICTURE_TYPE_PNG;
        }
        return Workbook.PICTURE_TYPE_JPEG;
    }

    /**
     * 创建表格样式
     */
    public void setDataValidation(Excel attr, Row row, int column)
    {
        if (attr.name().indexOf("注:") >= 0)
        {
            sheet.setColumnWidth(column, 6000);
        }
        else
        {
            // 设置列宽
            sheet.setColumnWidth(column, (int) ((attr.width() + 0.72) * 256));
        }
        // 如果设置了提示信息则鼠标放上去提示.
        if (StringUtils.isNotEmpty(attr.prompt()))
        {
            // 这里默认设了2-101列提示.
            setXSSFPrompt(sheet, "", attr.prompt(), 1, 100, column, column);
        }
        // 如果设置了combo属性则本列只能选择不能输入
        if (attr.combo().length > 0)
        {
            // 这里默认设了2-101列只能选择不能输入.
            setXSSFValidation(sheet, attr.combo(), 1, 100, column, column);
        }
    }


    /**
     * 创建表格样式
     */
    public void setDataValidation2(int column)
    {
        // 设置列宽
        sheet.setColumnWidth(column, (int) ((60 + 0.72) * 256));
    }

    /**
     * 添加单元格
     */
    public Cell addCell(Excel attr, Row row, T vo, Field field, int column, T voPrevious, int thisLine)
    {
        Cell cell = null;
        try
        {
            // 设置行高
            row.setHeight(maxHeight);
            // 根据Excel中设置情况决定是否导出,有些情况需要保持为空,希望用户填写这一列.
            if (attr.isExport())
            {
                // 创建cell
                cell = row.createCell(column);
                int align = attr.align().value();

                //索引值背景色改变
                //CellStyle style = wb.createCellStyle();
                //style.setFillForegroundColor(IndexedColors.RED.getIndex());
                //style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
                //cell.setCellStyle(style);


                int rowIndex = row.getRowNum();


                Cell finalCell = cell;
                // TODO 处理动态数据改变颜色
                if(attr.isAlertBackgroundColorByDynamic()) {
                    if(this.alertBackgroundColorByDynamicRuleMap == null) throw new ServiceException("未设置动态数据改变颜色规则集合!");
                    if(StringUtils.isEmpty(attr.colorValueField())) throw new ServiceException("@Excel为设置定数据实体类的颜色对应属性!");
                    this.alertBackgroundColorByDynamicRuleMap.entrySet().stream().forEach(i->{
                        Map<String, String> colorMap = i.getValue();
                        if(rowIndex == i.getKey()+this.tableDataStartIndex) {
                            String color = colorMap.get(attr.colorValueField());
                            if(StringUtils.isNotEmpty(color)) {

                               this.handleHexBackgroundColor(finalCell, color);

                            }
                        }
                    });

                }

                // TODO 处理自定义颜色
                if(attr.isAlertBackgroundColorByCustom()) {
                    if(StringUtils.isEmpty(attr.colorValueCustom())) throw new ServiceException("@Excel为设置自定义颜色值!");
                    if(rowIndex == attr.alertBackgroundColorByCustomRowIndex() + this.tableDataStartIndex) {
                        String color = attr.colorValueCustom();
                        if(StringUtils.isNotEmpty(color)) {
                            this.handleHexBackgroundColor(finalCell, color);
                        }
                    }
                }


                //默认样式
                if(!attr.isAlertBackgroundColorByDynamic() && !attr.isAlertBackgroundColorByCustom()) {
                    cell.setCellStyle(styles.get("data" + (align >= 1 && align <= 3 ? align : "")));
                }

                // 用于读取对象中的属性
                Object value = getTargetValue(vo, field, attr);

                //记录上一个对象属性
                Object valuePrevious = null;
                if(voPrevious != null) {
                    valuePrevious = getTargetValue(voPrevious, field, attr);
                }
                String dateFormat = attr.dateFormat();
                String readConverterExp = attr.readConverterExp();
                String separator = attr.separator();
                String spanCell = attr.spanCell();
                String spanRow = attr.spanRow();
                Long index = attr.index();
                Excel.QuickSpanRule quickSpanRule = attr.quickSpanRule();
                boolean isQuickSpan = attr.isQuickSpan();




                if (StringUtils.isNotEmpty(dateFormat) && StringUtils.isNotNull(value))
                {
                    cell.setCellValue(parseDateToStr(dateFormat, (Date) value));
                }
                else if (StringUtils.isNotEmpty(readConverterExp) && StringUtils.isNotNull(value))
                {
                    cell.setCellValue(convertByExp(Convert.toStr(value), readConverterExp, separator));
                }
                else if (value instanceof BigDecimal && -1 != attr.scale())
                {
                    cell.setCellValue((((BigDecimal) value).setScale(attr.scale(), attr.roundingMode())).toString());
                }
                else if (!attr.handler().equals(ExcelHandlerAdapter.class))
                {
                    cell.setCellValue(dataFormatHandlerAdapter(value, attr));
                }
                else
                {
                    // 设置列类型
                    setCellVo(value, attr, cell);
                }
                addStatisticsData(column, Convert.toStr(value), attr);

                //快速合并
                if(isQuickSpan) {

                    if (quickSpanRule.value() == Excel.QuickSpanRule.IDENTICAL_ROW_MERGE.value()) {
                        ExcelFieldAdditionParams fieldAdditionParams = this.fieldAdditionParamsMap.get(index);
                        if (value.equals(valuePrevious)) {
                            if (fieldAdditionParams.getMergeRowStart() == 0) {
                               fieldAdditionParams.setMergeRowStart(thisLine - 1);
                            }
                            fieldAdditionParams.setMergeRowEnd(thisLine);
                            //this.mergeRowEnd = thisLine;
                        } else {
                            if (fieldAdditionParams.getMergeRowStart() != 0 && fieldAdditionParams.getMergeRowEnd()  != 0) {
                                if (fieldAdditionParams.getMergeRowStart() != fieldAdditionParams.getMergeRowEnd()) {
                                    CellRangeAddress region = new CellRangeAddress(fieldAdditionParams.getMergeRowStart(), fieldAdditionParams.getMergeRowEnd(), index.intValue(), index.intValue());
                                    sheet.addMergedRegion(region);
                                    //this.mergeRowStart = 0;
                                    //this.mergeRowEnd = 0;
                                }

                                //多列同时设置这两个数据会混乱 解决方法:每一列生成一个单独的对象,相互独立
                                //this.mergeRowStart = 0;
                                //this.mergeRowEnd = 0;
                                fieldAdditionParams.setMergeRowStart(0);
                                fieldAdditionParams.setMergeRowEnd(0);
                            }
                        }
                    }

                }


                //通用合并行
                if(StringUtils.isNotEmpty(spanRow)) {
                   String[] spanRowArray = spanRow.split("\\|");
                   List<String[]> spanRowRuleList = Arrays.stream(spanRowArray).map(i->{
                        return i.split(",");
                    }).collect(Collectors.toList());
                   spanRowRuleList.stream().forEach(i-> {
                       Integer spanRowStart = Integer.parseInt(i[0].trim());
                       Integer spanRowEnd = Integer.parseInt(i[1].trim());
                       Long fieldIndex = null;
                       if(i.length == 3) {
                           fieldIndex = Long.parseLong(i[2].trim());
                       } else if(index != -1){
                           fieldIndex = index;
                       } else {
                           throw new ServiceException("列索引规则配置错误,请检查字段索引值是否配置!");
                       }

                       CellRangeAddress region = new CellRangeAddress(spanRowStart+this.tableDataStartIndex, spanRowEnd+this.tableDataStartIndex, fieldIndex.intValue(), fieldIndex.intValue());
                       sheet.addMergedRegion(region);
                   });
                }
                //通用合并单元格(包括行合并和列合并)
                if(StringUtils.isNotEmpty(spanCell)) {
                    String[] spanCellArray = spanCell.split("\\|");
                    List<String[]> spanCellRuleList = Arrays.stream(spanCellArray).map(i->{
                        return i.split(",");
                    }).collect(Collectors.toList());
                    spanCellRuleList.stream().forEach(i->{
                        Integer spanRowStart = Integer.parseInt(i[0].trim());
                        Integer spanRowEnd = Integer.parseInt(i[1].trim());
                        Integer spanCellStart = Integer.parseInt(i[2].trim());
                        Integer spanCellEnd = Integer.parseInt(i[3].trim());
                        CellRangeAddress region = new CellRangeAddress(spanRowStart+this.tableDataStartIndex, spanRowEnd+this.tableDataStartIndex, spanCellStart, spanCellEnd);
                        sheet.addMergedRegion(region);
                    });
                }
                //CellRangeAddress region1 = new CellRangeAddress(1, 18, 0, 0);
                //sheet.addMergedRegion(region1);
                //CellRangeAddress region2 = new CellRangeAddress(19, 21, 0, 1);
                //sheet.addMergedRegion(region2);

            }
        }
        catch (Exception e)
        {
            log.error("导出Excel失败{}", e);
        }
        return cell;
    }

    /**
     * 处理单元格背景颜色根据16进制颜色代码
     * @param cell
     * @param color
     */
    public void handleHexBackgroundColor(Cell cell, String color) {

        try {
            if(StringUtils.isEmpty(color)) throw new ServiceException("颜色代码为空!");
            if(!color.contains("#")) throw new ServiceException("规则中的颜色代码格式不正确,请使用颜色16进制代码!");
            String rgbS = color.substring(1);
            byte[] rgbB = new byte[0]; // get byte array from hex string
            rgbB = Hex.decodeHex(rgbS);
            XSSFColor finalColor = new XSSFColor(rgbB, null); //IndexedColorMap has no usage until now. So it can be set null.
            XSSFCellStyle style = (XSSFCellStyle) wb.createCellStyle();
            style.setAlignment(HorizontalAlignment.CENTER);
            style.setVerticalAlignment(VerticalAlignment.CENTER);
            style.setBorderRight(BorderStyle.THIN);
            style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderLeft(BorderStyle.THIN);
            style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderTop(BorderStyle.THIN);
            style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderBottom(BorderStyle.THIN);
            style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            Font dataFont = wb.createFont();
            dataFont.setFontName("Arial");
            dataFont.setFontHeightInPoints((short) 10);
            style.setFont(dataFont);
            style.setWrapText(true);
            style.setFillForegroundColor(finalColor);
            style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            cell.setCellStyle(style);
        } catch (DecoderException e) {
            e.printStackTrace();
        }

    }

    /**
     * Excel列辅助参数对象类
     * 解决:
     * 多列同时设置这两个数据会混乱 解决方法:每一列生成一个单独的对象,相互独立
     * this.mergeRowStart = 0;
     * this.mergeRowEnd = 0;
     */
    @Data
    public class ExcelFieldAdditionParams {

        private int mergeRowStart = 0;
        private int mergeRowEnd = 0;
        private int mergeCellStart = 0;
        private int mergeCellEnd = 0;
    }



    /**
     * 设置 POI XSSFSheet 单元格提示
     *
     * @param sheet 表单
     * @param promptTitle 提示标题
     * @param promptContent 提示内容
     * @param firstRow 开始行
     * @param endRow 结束行
     * @param firstCol 开始列
     * @param endCol 结束列
     */
    public void setXSSFPrompt(Sheet sheet, String promptTitle, String promptContent, int firstRow, int endRow,
            int firstCol, int endCol)
    {
        DataValidationHelper helper = sheet.getDataValidationHelper();
        DataValidationConstraint constraint = helper.createCustomConstraint("DD1");
        CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
        DataValidation dataValidation = helper.createValidation(constraint, regions);
        dataValidation.createPromptBox(promptTitle, promptContent);
        dataValidation.setShowPromptBox(true);
        sheet.addValidationData(dataValidation);
    }

    /**
     * 设置某些列的值只能输入预制的数据,显示下拉框.
     *
     * @param sheet 要设置的sheet.
     * @param textlist 下拉框显示的内容
     * @param firstRow 开始行
     * @param endRow 结束行
     * @param firstCol 开始列
     * @param endCol 结束列
     * @return 设置好的sheet.
     */
    public void setXSSFValidation(Sheet sheet, String[] textlist, int firstRow, int endRow, int firstCol, int endCol)
    {
        DataValidationHelper helper = sheet.getDataValidationHelper();
        // 加载下拉列表内容
        DataValidationConstraint constraint = helper.createExplicitListConstraint(textlist);
        // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
        CellRangeAddressList regions = new CellRangeAddressList(firstRow, endRow, firstCol, endCol);
        // 数据有效性对象
        DataValidation dataValidation = helper.createValidation(constraint, regions);
        // 处理Excel兼容性问题
        if (dataValidation instanceof XSSFDataValidation)
        {
            dataValidation.setSuppressDropDownArrow(true);
            dataValidation.setShowErrorBox(true);
        }
        else
        {
            dataValidation.setSuppressDropDownArrow(false);
        }

        sheet.addValidationData(dataValidation);
    }

    /**
     * 解析导出值 0=男,1=女,2=未知
     *
     * @param propertyValue 参数值
     * @param converterExp 翻译注解
     * @param separator 分隔符
     * @return 解析后值
     */
    public static String convertByExp(String propertyValue, String converterExp, String separator)
    {
        StringBuilder propertyString = new StringBuilder();
        String[] convertSource = converterExp.split(",");
        for (String item : convertSource)
        {
            String[] itemArray = item.split("=");
            if (StringUtils.containsAny(separator, propertyValue))
            {
                for (String value : propertyValue.split(separator))
                {
                    if (itemArray[0].equals(value))
                    {
                        propertyString.append(itemArray[1] + separator);
                        break;
                    }
                }
            }
            else
            {
                if (itemArray[0].equals(propertyValue))
                {
                    return itemArray[1];
                }
            }
        }
        return StringUtils.stripEnd(propertyString.toString(), separator);
    }

    /**
     * 反向解析值 男=0,女=1,未知=2
     *
     * @param propertyValue 参数值
     * @param converterExp 翻译注解
     * @param separator 分隔符
     * @return 解析后值
     */
    public static String reverseByExp(String propertyValue, String converterExp, String separator)
    {
        StringBuilder propertyString = new StringBuilder();
        String[] convertSource = converterExp.split(",");
        for (String item : convertSource)
        {
            String[] itemArray = item.split("=");
            if (StringUtils.containsAny(separator, propertyValue))
            {
                for (String value : propertyValue.split(separator))
                {
                    if (itemArray[1].equals(value))
                    {
                        propertyString.append(itemArray[0] + separator);
                        break;
                    }
                }
            }
            else
            {
                if (itemArray[1].equals(propertyValue))
                {
                    return itemArray[0];
                }
            }
        }
        return StringUtils.stripEnd(propertyString.toString(), separator);
    }

    /**
     * 数据处理器
     *
     * @param value 数据值
     * @param excel 数据注解
     * @return
     */
    public String dataFormatHandlerAdapter(Object value, Excel excel)
    {
        try
        {
            Object instance = excel.handler().newInstance();
            Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class });
            value = formatMethod.invoke(instance, value, excel.args());
        }
        catch (Exception e)
        {
            log.error("不能格式化数据 " + excel.handler(), e.getMessage());
        }
        return Convert.toStr(value);
    }

    /**
     * 合计统计信息
     */
    private void addStatisticsData(Integer index, String text, Excel entity)
    {
        if (entity != null && entity.isStatistics())
        {
            Double temp = 0D;
            if (!statistics.containsKey(index))
            {
                statistics.put(index, temp);
            }
            try
            {
                temp = Double.valueOf(text);
            }
            catch (NumberFormatException e)
            {
            }
            statistics.put(index, statistics.get(index) + temp);
        }
    }

    /**
     * 创建统计行
     */
    public void addStatisticsRow()
    {
        if (statistics.size() > 0)
        {
            Row row = sheet.createRow(sheet.getLastRowNum() + 1);
            Set<Integer> keys = statistics.keySet();
            Cell cell = row.createCell(0);
            cell.setCellStyle(styles.get("total"));
            cell.setCellValue("合计");

            for (Integer key : keys)
            {
                cell = row.createCell(key);
                cell.setCellStyle(styles.get("total"));
                cell.setCellValue(DOUBLE_FORMAT.format(statistics.get(key)));
            }
            statistics.clear();
        }
    }

    /**
     * 获取bean中的属性值
     *
     * @param vo 实体对象
     * @param field 字段
     * @param excel 注解
     * @return 最终的属性值
     * @throws Exception
     */
    private Object getTargetValue(T vo, Field field, Excel excel) throws Exception
    {
        Object o = field.get(vo);
        if (StringUtils.isNotEmpty(excel.targetAttr()))
        {
            String target = excel.targetAttr();
            if (target.contains("."))
            {
                String[] targets = target.split("[.]");
                for (String name : targets)
                {
                    o = getValue(o, name);
                }
            }
            else
            {
                o = getValue(o, target);
            }
        }
        return o;
    }

    /**
     * 以类的属性的get方法方法形式获取值
     *
     * @param o
     * @param name
     * @return value
     * @throws Exception
     */
    private Object getValue(Object o, String name) throws Exception
    {
        if (StringUtils.isNotNull(o) && StringUtils.isNotEmpty(name))
        {
            Class<?> clazz = o.getClass();
            Field field = clazz.getDeclaredField(name);
            field.setAccessible(true);
            o = field.get(o);
        }
        return o;
    }

    /**
     * 得到所有定义字段
     */
    private void createExcelField()
    {
        this.fields = getFields();
        this.fields = this.fields.stream().sorted(Comparator.comparing(objects -> ((Excel) objects[1]).sort())).collect(Collectors.toList());
        this.fieldAdditionParamsMap = this.fields.stream().map(i->{
            Excel excel = (Excel) i[1];
            return excel;
        }).filter(excel -> excel.index() != -1).collect(Collectors.toMap(Excel::index, i->new ExcelFieldAdditionParams()));
        this.maxHeight = getRowHeight();
    }

    /**
     * 获取字段注解信息
     */
    public List<Object[]> getFields()
    {
        List<Object[]> fields = new ArrayList<Object[]>();
        List<Field> tempFields = new ArrayList<>();
        tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
        tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
        for (Field field : tempFields)
        {
            // 单注解
            if (field.isAnnotationPresent(Excel.class))
            {
                Excel attr = field.getAnnotation(Excel.class);
                if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
                {
                    field.setAccessible(true);
                    fields.add(new Object[] { field, attr });
                }
            }

            // 多注解
            if (field.isAnnotationPresent(Excels.class))
            {
                Excels attrs = field.getAnnotation(Excels.class);
                Excel[] excels = attrs.value();
                for (Excel attr : excels)
                {
                    if (attr != null && (attr.type() == Type.ALL || attr.type() == type))
                    {
                        field.setAccessible(true);
                        fields.add(new Object[] { field, attr });
                    }
                }
            }
        }
        return fields;
    }

    /**
     * 根据注解获取最大行高
     */
    public short getRowHeight()
    {
        double maxHeight = 0;
        for (Object[] os : this.fields)
        {
            Excel excel = (Excel) os[1];
            maxHeight = Math.max(maxHeight, excel.height());
        }
        return (short) (maxHeight * 20);
    }

    /**
     * 创建一个工作簿
     */
    public void createWorkbook()
    {
        this.wb = new SXSSFWorkbook(500);
        this.sheet = wb.createSheet();
        wb.setSheetName(0, sheetName);
        this.styles = createStyles(wb);
    }

    /**
     * 创建工作表
     *
     * @param sheetNo sheet数量
     * @param index 序号
     */
    public void createSheet(int sheetNo, int index)
    {
        // 设置工作表的名称.
        if (sheetNo > 1 && index > 0)
        {
            this.sheet = wb.createSheet();
            this.createTitle();
            wb.setSheetName(index, sheetName + index);
        }
    }

    /**
     * 获取单元格值
     *
     * @param row 获取的行
     * @param column 获取单元格列号
     * @return 单元格值
     */
    public Object getCellValue(Row row, int column)
    {
        if (row == null)
        {
            return row;
        }
        Object val = "";
        try
        {
            Cell cell = row.getCell(column);
            if (StringUtils.isNotNull(cell))
            {
                if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA)
                {
                    val = cell.getNumericCellValue();
                    if (DateUtil.isCellDateFormatted(cell))
                    {
                        val = DateUtil.getJavaDate((Double) val); // POI Excel 日期格式转换
                    }
                    else
                    {
                        if ((Double) val % 1 != 0)
                        {
                            val = new BigDecimal(val.toString());
                        }
                        else
                        {
                            val = new DecimalFormat("0").format(val);
                        }
                    }
                }
                else if (cell.getCellType() == CellType.STRING)
                {
                    val = cell.getStringCellValue();
                }
                else if (cell.getCellType() == CellType.BOOLEAN)
                {
                    val = cell.getBooleanCellValue();
                }
                else if (cell.getCellType() == CellType.ERROR)
                {
                    val = cell.getErrorCellValue();
                }

            }
        }
        catch (Exception e)
        {
            return val;
        }
        return val;
    }

    /**
     * 判断是否是空行
     *
     * @param row 判断的行
     * @return
     */
    private boolean isRowEmpty(Row row)
    {
        if (row == null)
        {
            return true;
        }
        for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++)
        {
            Cell cell = row.getCell(i);
            if (cell != null && cell.getCellType() != CellType.BLANK)
            {
                return false;
            }
        }
        return true;
    }

    /**
     * 格式化不同类型的日期对象
     *
     * @param dateFormat 日期格式
     * @param val 被格式化的日期对象
     * @return 格式化后的日期字符
     */
    public String parseDateToStr(String dateFormat, Object val)
    {
        if (val == null)
        {
            return "";
        }
        String str;
        if (val instanceof Date)
        {
            str = DateUtils.parseDateToStr(dateFormat, (Date) val);
        }
        else if (val instanceof LocalDateTime)
        {
            str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDateTime) val));
        }
        else if (val instanceof LocalDate)
        {
            str = DateUtils.parseDateToStr(dateFormat, DateUtils.toDate((LocalDate) val));
        }
        else
        {
            str = val.toString();
        }
        return str;
    }


    public Integer getTitleRowIndex() {
        return titleRowIndex;
    }

    public void setTitleRowIndex(Integer titleRowIndex) {
        this.titleRowIndex = titleRowIndex;
    }

    public boolean isOpenMultilevelTableTop() {
        return openMultilevelTableTop;
    }

    public void setOpenMultilevelTableTop(boolean openMultilevelTableTop) {
        this.openMultilevelTableTop = openMultilevelTableTop;
    }

    public String getTableTopJsonData() {
        return tableTopJsonData;
    }

    public void setTableTopJsonData(String tableTopJsonData) {
        this.tableTopJsonData = tableTopJsonData;
    }

    public List<JsonData> getTableTopJsonDataList() {
        return tableTopJsonDataList;
    }

    public void setTableTopJsonDataList(List<JsonData> tableTopJsonDataList) {
        this.tableTopJsonDataList = tableTopJsonDataList;
    }

    public Map<Integer, Map<String, String>> getAlertBackgroundColorByDynamicRuleMap() {
        return alertBackgroundColorByDynamicRuleMap;
    }

    public void setAlertBackgroundColorByDynamicRuleMap(Map<Integer, Map<String, String>> alertBackgroundColorByDynamicRuleMap) {
        this.alertBackgroundColorByDynamicRuleMap = alertBackgroundColorByDynamicRuleMap;
    }
}

结语

如果有其他更好的思路或其他问题欢迎评论和留言,只是根据业务做了一些扩展,能力有限,大佬勿喷!

又是敲代码的一天~

  • 6
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
若依框架系列视频教程是一套针对若依框架微服务架构的教学视频。若依框架是一款开源的Java开发框架,它提供了一套完善的开发工具和组件,可以帮助开发者快速构建稳定、高效的企业级应用程序。 ruoyi-cloud微服务版本是若依框架微服务架构版本,它将应用程序按照不同的功能模块拆分成多个微服务,每个微服务独立运行,通过服务间的通信协议进行交互和协作,从而实现了系统的松耦合和可伸缩性。 这套视频教程以实际项目为案例,详细介绍了若依框架ruoyi-cloud微服务版本的搭建和使用方法。从环境准备、开发工具的选择,到项目的创建、模块的划分,再到各个微服务的设计和实现,视频教程一步步指导学习者完成整个过程。 通过学习这套视频教程,学员可以了解和掌握若依框架微服务架构的开发原理和实践经验。视频教程不仅注重理论知识的掌握,还通过实际操作和示例代码演示,帮助学员更好地理解和运用所学内容。 此外,教程还提供了常见问题解答、技术论坛等学习资源和互动平台,学员可以通过这些渠道与其他学员一起共同学习和交流,进一步提高自己的应用开发能力。 总之,若依框架系列视频教程是一套系统、全面地介绍若依框架ruoyi-cloud微服务版本的教学资源,通过学习这套教程,可以帮助开发者快速上手和应用这一先进的开发技术,提高开发效率和代码质量。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值