业务需求

先上业务需求,生产需求有一个新的模板,模板大概长这个样子

Snipaste_2024-01-05_18-52-37-defjdisx.png

遇到的问题

但是easypoi的模板导出在多个遍历导出的时候会存在以下几个问题

  1. 多个$fe标签的使用会提示"for each存在空字符串"

  2. $fe标签在多行遍历工作中合并单元格存在样式问题

这个问题是easypoi的bug,目前只有两条路,改源码或规避bug,本文采用第二种方案,第一种可参考:http://t.csdnimg.cn/PDMLU

解决方案

通过观察模板,发现其实可以把所有数据都放在一条下插遍历list中,如小计、房费、门票这种,难点是如何处理房费的合并,以及小计的样式,我的做法是用poi+自定义正则关键字。

定义正则

@AllArgsConstructor
public enum CellPatternEnum {

    /**
     * excel定制化导出正则枚举 【排列顺序为关键字处理顺序】
     */
    // 红色文字样式
    RED_FONT_STYLE("RED_FONT_STYLE", "样式","###redFontStyle###"),
    // 合并单元格 ###merge_style(占用列(52);;占用行(1);;展示文字)###
    MERGE_STYLE("MERGE_STYLE", "合并单元格", "###merge_style\\((.+?)\\)###"),
    // 隐藏该行
    HIDDEN_ROW("HIDDEN_ROW", "若数据为空则隐藏该行", "###hiddenRow###"),

    ;
    @Getter
    private final String code;
    @Getter
    private final String name;
    @Getter
    private final String pattern;
}

数据中加入自定义关键字

代码中的###merge_style(%s;;%s;;%s)######redFontStyle###即为自定义关键字,后续可以通过poi遍历处理该格子

        // 遍历每种类型的费用
        beanMap.forEach((type, items) -> {
            // 初始化预算金额和实际金额
            BigDecimal budgetAmount = BigDecimal.ZERO;
            BigDecimal totalRealAmount = BigDecimal.ZERO;

            // 遍历各项费用
            for (int i = 0, size = items.getItems().size(); i < size; i++) {
                Bean item = items.getItems().get(i);
                // 将费用信息转换为Map
                Map<String, Object> itemMap = BeanUtil.beanToMap(item);
                if (i == 0) {
                    // 设置合并样式
                    String format = String.format("###merge_style(%s;;%s;;%s)###", 0, size, type);
                    itemMap.put("typeName", format);
                } else {
                    itemMap.put("typeName", "");
                }
                // 累加预算金额和实际金额
                budgetAmount = NumberUtil.add(budgetAmount, item.getBudgetAmount());
                totalRealAmount = NumberUtil.add(totalRealAmount, item.getTotalRealAmount());
                // 添加到列表
                itemsList.add(itemMap);
            }

            // 创建小计项
           	Bean bean = new Bean();
            bean.setItemTitle("小计" + "###redFontStyle###");
            bean.setBudgetAmount(budgetAmount);
            bean.setTotalRealAmount(totalRealAmount);
            // 将小计项转换为Map
            Map<String, Object> sumColumnItemMap = BeanUtil.beanToMap(bean);
            sumColumnItemMap.put("typeName", "");
            // 添加到列表
            itemsList.add(sumColumnItemMap);
        });

使用poi遍历easypoi处理后的Workbook

核心方法

for (Sheet sheet : workbook) {
                for (Row row : sheet) {
                    for (Cell cell : row) {
                        Object valueObj = CommonExcelUtil.getCellValue(cell);
                        if (!Objects.nonNull(valueObj)) {
                            continue;
                        }
                        String value;

                        for (CellPatternEnum cellPatternEnum : CellPatternEnum.values()) {
                            //每次都获取最新值
                            value = String.valueOf(CommonExcelUtil.getCellValue(cell));
                            CommonExcelUtil.handleMatchedCell(workbook, sheet, cell, value, cellPatternEnum);
                        }

                    }
                }
            }

    /**
     * 根据自定义关键字处理excel模板导出
     */
    public static void handleMatchedCell(Workbook workbook, Sheet sheet, Cell cell, String value, CellPatternEnum cellPatternEnum) {
        Pattern pattern = Pattern.compile(cellPatternEnum.getPattern());
        Matcher matcher = pattern.matcher(value);
        //改为循环,修复一行字符串中有多次匹配正则问题
        int matcherStart = 0;
        while (matcher.find(matcherStart)) {
            //重新获取最新值
            value = String.valueOf(CommonExcelUtil.getCellValue(cell));
            if (CellPatternEnum.RED_FONT_STYLE.equals(cellPatternEnum)) {
                String s = StringUtils.replace(value, matcher.group(0), "");
                // 复制新的style
                CellStyle cellStyle = cell.getCellStyle();
                CellStyle newCellStyle = workbook.createCellStyle();
                newCellStyle.cloneStyleFrom(cellStyle);

                // 设置新的style字体大小
                Font font = workbook.getFontAt(newCellStyle.getFontIndexAsInt());
                Font newFont = workbook.createFont();
                BeanUtil.copyProperties(font, newFont);
                newFont.setFontName(font.getFontName());
                //字体样式
                newFont.setColor(Font.COLOR_RED);
                //是否加粗
                newFont.setBold(true);
                newFont.setUnderline(font.getUnderline());
                newFont.setFontHeightInPoints(font.getFontHeightInPoints());
                newCellStyle.setFont(newFont);
                cell.setCellStyle(newCellStyle);
                cell.setCellValue(s);
                return;
            }
            if (CellPatternEnum.HIDDEN_ROW.equals(cellPatternEnum)) {
                Row row = cell.getRow();
                row.setZeroHeight(true);
                row.setHeight((short) 0);
                return;
            }
            if (CellPatternEnum.MERGE_STYLE.equals(cellPatternEnum)) {
                String funcArgs = matcher.group(1);
                String[] funcConfigSplits = funcArgs.split(";;");
                // 合并单元格 ###merge_style(占用列(52);;占用行(1);;展示文字)###
                int colspan = NumberUtil.parseInt(funcConfigSplits[0]);
                int rowspan = NumberUtil.parseInt(funcConfigSplits[1]);
                String s = StringUtils.isBlank(funcConfigSplits[2]) ? "" : funcConfigSplits[2];

                // 清空内容 替换成 展示文字
                String content = StringUtils.replace(value, matcher.group(0), s);
                cell.setCellValue(content);

                CellAddress address = cell.getAddress();

                // 指定合并开始行、合并结束行 合并开始列、合并结束列
                CellRangeAddress rangeAddress = new CellRangeAddress(address.getRow(), address.getRow() + rowspan, address.getColumn(), address.getColumn() + colspan);
                // 先把限定范围内的合并格子取消
                CommonExcelUtil.removeMergedRegion(sheet, rangeAddress);
                // 添加要合并地址到表格
                sheet.addMergedRegion(rangeAddress);
                return;
            }
        }

    }

一些基本方法

    /**
     * 获取单元格 值
     *
     * @param cell cell
     * @return Object
     */
    public static Object getCellValue(Cell cell) {
        CellType cellType = cell.getCellType();
        switch (cellType) {
            case STRING:
                return cell.getStringCellValue();
            case BOOLEAN:
                return cell.getBooleanCellValue();
            case FORMULA:
                return cell.getCellFormula();
            case NUMERIC:
                // 防止double科学计数法
                return BigDecimal.valueOf(cell.getNumericCellValue()).toPlainString();
            default:
                try {
                    return cell.getStringCellValue();
                } catch (Exception ignored) {
                    return null;
                }
        }
    }

    /**
     * 删除合并行
     *
     * @param sheet         sheet
     * @param cellAddresses 限定单元格范围
     */
    public static void removeMergedRegion(Sheet sheet, CellRangeAddress cellAddresses) {
        // 获取所有的合并单元格
        int sheetMergeCount = sheet.getNumMergedRegions();
        // 用于保存要移除的那个单元格序号
        List<Integer> indexList = new ArrayList<>();

        for (int i = 0; i < sheetMergeCount; i++) {
            //获取第i个合并单元格
            CellRangeAddress ca = sheet.getMergedRegion(i);
            // 开始列 开始行
            int caFirstColumn = ca.getFirstColumn();
            int caFirstRow = ca.getFirstRow();

            // 只需要判断 开始列和开始行 有没有在限定坐标内
            if (cellAddresses.containsRow(caFirstRow) && cellAddresses.containsColumn(caFirstColumn)) {
                indexList.add(i);
            }
        }
        if (CollUtil.isNotEmpty(indexList)) {
            //移除合并单元格
            sheet.removeMergedRegions(indexList);
        }
    }

重新定义模板

Snipaste_2024-01-06_11-29-16-vqascwfu.png

导出效果

Snipaste_2024-01-06_11-31-31-ulmefppg.png完美解决需求!

总结

自定义关键字+poi其实可以扩展更多关键字搭配easypoi原生的关键字可以实现更多复杂的业务需求,如:###hiddenRow###,可以搭配原生的三目判断,来判断是否隐藏当前行,如:

{{le:(items) > 0 ? '': '###hiddenRow###'}}

Snipaste_2024-01-06_11-46-42-xzjprvnq.png

文章作者:
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 飞的博客
Java regexp poi EasyPoi
喜欢就支持一下吧