EasyExcel根据项目模板导出Excel带公式的Excel文件

需求

项目中有个功能是根据填写的数据,计算对应的值,前端已经实现了对应的计算公式。但是客户需要能够将页面导出成模板,然后模板中也能实现填写后计算出对应的值。这里面最坑的部分是表头:表头每个W代表一个周,但是每个月的周数不固定,有4周,5周,6周的情况,然后最后一周的计算公式又跟其他周的不同,因此需要动态去展示后面对应周的公式。
EasyExcel根据项目模板导出Excel带公式的Excel文件_第1张图片

第一次尝试

因为项目中之前使用了easyExcel来实现简单表格的导出功能,于是开始研究怎么动态设置公式。
通过实现CellWriteHandler,再把PlanCellWriteHandler注册到easyExcel中,在excel中是用POI的cell.setCellFormula(“H6/SUM(D5:H5)*30”)方式来逐行写入公式【记住公式前面不用写=,一个小坑会报错】
EasyExcel根据项目模板导出Excel带公式的Excel文件_第2张图片
单个传值用map,列表传值用FillWrapper里放list

源码:

import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.digitalchina.psimgr.system.redis.RedisCache;
import org.apache.poi.ss.usermodel.*;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class PlanCellWriteHandler implements CellWriteHandler {
    @Resource
    private RedisCache redisCache;
    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
                                 Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {

    }
    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
                                Head head, Integer relativeRowIndex, Boolean isHead) {

    }
    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
        CellWriteHandler.super.afterCellDispose(context);
        //sheet
        String sheetName = context.getWriteSheetHolder().getSheetName();
        Cell cell = context.getCell();
        //第一页
        if (sheetName.equals("计划填写")) {
            //行
            int rowNum = cell.getRowIndex();
            //列
            int columnNum = cell.getColumnIndex();
            String w6 = null;
            if(rowNum == 0 && columnNum == 8 ){
                w6 = cell.getStringCellValue();
                redisCache.setCacheObject("w6", w6);
            }
			// 第六周没有值,则第五周为最后一周,动态设置w5的公式
            if (redisCache.getCacheObject("w6") == null) {
                if(rowNum == 8 && columnNum == 7){
                    cell.setCellFormula("H6/SUM(D5:H5)*30");
                }else if(rowNum == 9 && columnNum == 7){
                    cell.setCellFormula("H8/SUM(D5:H5)*30");
                }
            }else{
                if(rowNum == 8 && columnNum == 7){
                    cell.setCellFormula("H6/H11*7");
                }else if(rowNum == 9 && columnNum == 7){
                    cell.setCellFormula("H8/H11*7");
                }else if(rowNum == 5 && columnNum == 8){
                    cell.setCellFormula("H6+I3-I5");
                }else if(rowNum == 6 && columnNum == 8){
                    cell.setCellFormula("G7+I2-I3");
                }else if(rowNum == 7 && columnNum == 8){
                    cell.setCellFormula("H8+I2-I5");
                }else if(rowNum == 8 && columnNum == 8){
                    cell.setCellFormula("I6/SUM(D5:I5)*30");
                }else if(rowNum == 9 && columnNum == 8){
                    cell.setCellFormula("I8/SUM(D5:I5)*30");
                }else if(rowNum == 10 && columnNum == 8){
                    cell.setCellFormula("SUM(F5:I5)/4");
                }
            }
        }
    }
}

// 处理数据
        List<Map<String, Object>> planHead = getWeekPlanHead(month);
        Map<String, Object> map = new HashMap<>();
        Map<String, Object> collect = planHead.forEach(head -> {map.put(head.get("prop"),head.get("label"));
        map.putAll(collect);
        
        List<LastFourWeekCostDTO> fourWeekAgoCost = getFourWeekAgoCost(organizationId, month);
        String fileName;
        if(businessType == 2){
            fileName =  "/计划填写-非寄售模板.xlsx";
        }else{
            fileName = "/计划填写-寄售模板.xlsx";
        }
        //读取导出模版
        String path = Objects.requireNonNull(this.getClass().getClassLoader().getResource("template")).getPath() + fileName;
        String templateFilePath = path;
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        ExcelWriter excelWriter = null;
        try (OutputStream os = response.getOutputStream()){
            excelWriter = EasyExcel.write(os).withTemplate(templateFilePath).build();
        } catch (IOException e) {
            throw new PlanException(ResultCode.FILE_READ_FAILED, "获取模板失败");
        }
        Workbook workbook = excelWriter.writeContext().writeWorkbookHolder().getWorkbook();
        workbook.setForceFormulaRecalculation(true);
        WriteSheet lastMonthCost = EasyExcel.writerSheet("上月成本").build();
        excelWriter.fill(new FillWrapper("data", fourWeekAgoCost), lastMonthCost);
        // 将拦截器注册到WriteSheet
        WriteSheet planSheet = EasyExcel.writerSheet("计划填写").registerWriteHandler(planCellWriteHandler).build();
        //动态填充excel的head行
        excelWriter.fill(map, planSheet);
        excelWriter.fill(new FillWrapper("data", dataList), planSheet);
        excelWriter.finish();

结果EasyExcel根据项目模板导出Excel带公式的Excel文件_第3张图片这里面有两个问题:

  • PlanCellWriteHandler拦截器没处理一个cell进入一次,所以当你需要动态写公式的地方是没有通过excelWriter.fill()去填值的时候,拦截器是不会进去的,也就没办法动态setCellFormula,因此需要去填写公式的地方动态填充任意值。
  • FillWrapper里放list动态设值的cell的格式会按照你写变量的那一列的样式去填充下面的列,因为如果有不同的格式要求,需要单独给Cell对象设置对应的格式,这里也可以去搜POI设置格式需要注意的事项;

第二次尝试

随着对需求的分析与理解,我觉得POI手动设置公式做的有点复杂了,我转换了公式的展现形式,开始不纠结与代码层面去展现公式了,于是我直接打起了Excel的公式的想法,于是我尝试了一下IF(ISBLANK(I5)," ",I6/SUM(D5:I5)*30),这种写法,直接判断需要填写的cell是否为空,为空则对应的计算也不存在直接显示空字符串,不为空,则使用计算公式,直接完美的以最小的代价解决了我的需求,问题就告一段落了
模板:
EasyExcel根据项目模板导出Excel带公式的Excel文件_第4张图片
结果:(七月有6周)
EasyExcel根据项目模板导出Excel带公式的Excel文件_第5张图片
(六月只有5周)
EasyExcel根据项目模板导出Excel带公式的Excel文件_第6张图片
一个思考过程,希望也可以打开你解决类似问题的思路!

你可能感兴趣的:(Java知识点,java)