工作总结—基于Easyexcel的级联下拉选择实现

业务梳理

某业务可以根据Excel模板的方式新增数据,其导入模板中包含组织机构信息,人员信息。以往的做法是用户在导入时自己填写模板。

1.弊端

  1. 大数据模板导入时,用户体验较差;
  2. 客户输入容易出错,导致导入时易产生数据校验错误。

针对如上弊端,产品提出如下需求

需求

  1. 对于已知固定数据,采用数据下拉选择填入的方式填充数据
  2. 对于具有关联关系的数据,能根据上一列填值确定下一列数据的下拉范围
  3. 某一列的数据可根据前几列是否已填入值确定该列数据的下拉范围

实现

思路

1.学习Excel如何制作下拉框
2.基于Easyexcel实现Excel下拉框选择数据

实现

1.确定下拉框实现数据结构

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

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExcelCascade {
    /**
     *下拉选项
     */
    private List<String> options;

    /**
     * 级联模板
     */
    private String parentColTpl;

    /**
     * 当前下拉列索引
     */
    private Integer curColIndex;

    /**
     * 下拉列表首行
     */
    private Integer firstRow;

    /**
     * 下拉列表末行
     */
    private Integer lastRow;

    /**
     * 下拉列表名称
     */
    private String title;
}
2.构造下拉框数据
  public static List<ExcelCascade> init() {
        List<ExcelCascade> excelCascades = new ArrayList<>();
        List<OrganizationVO> organizationVOS = new ArrayList<>() {{
            add(OrganizationVO.builder().id("1").code("1").name("公司").parentId("0").type("10").build());
            add(OrganizationVO.builder().id("2").code("2").name("处室").parentId("1").type("20").build());
            add(OrganizationVO.builder().id("3").code("3").name("科室").parentId("2").type("30").build());
            add(OrganizationVO.builder().id("4").code("4").name("班组").parentId("3").type("30").build());
        }};

        Map<String, OrganizationVO> orgIdOrgMap = organizationVOS.stream().collect(Collectors.toMap(OrganizationVO::getId, Function.identity()));
        Map<String, List<OrganizationVO>> orgGroupingByParentIdMap = organizationVOS.stream().filter(o -> !StrUtil.equals(o.getType(), "10")).collect(Collectors.groupingBy(OrganizationVO::getParentId));
        orgGroupingByParentIdMap.forEach((k, v) -> {
            OrganizationVO parentOrganizationVO = orgIdOrgMap.get(k);
            if (Objects.nonNull(parentOrganizationVO)) {
                int curColIndex = 0;
                String parentColTpl = "";
                String type = parentOrganizationVO.getType();
                if (StrUtil.equals(type, "10")) {
                    curColIndex = 1;
                }
                if (StrUtil.equals(type, "20")) {
                    curColIndex = 2;
                    parentColTpl = "INDIRECT($B${})";
                }

                if (StrUtil.equals(type, "30")) {
                    curColIndex = 3;
                    parentColTpl = "INDIRECT($C${})";
                }
                ExcelCascade tmp = ExcelCascade.builder()
                        //考虑名称中不能包含特殊字符 TODO
                        .title(parentOrganizationVO.getName() + "_" + parentOrganizationVO.getCode())
                        .options(v.stream().map(o -> o.getName() + "_" + o.getCode()).collect(Collectors.toList()))
                        .curColIndex(curColIndex)
                        .parentColTpl(parentColTpl)
                        .firstRow(1)
                        .lastRow(999)
                        .build();
                excelCascades.add(tmp);
            }

        });


        List<UserVO> userVOS = new ArrayList<>() {{
            add(UserVO.builder().id("1").name("处室的人").code("1").orgId("2").build());
            add(UserVO.builder().id("2").name("科室的人").code("2").orgId("3").build());
            add(UserVO.builder().id("3").name("班组的人").code("3").orgId("4").build());
            add(UserVO.builder().id("3").name("班组的人").code("4").orgId("4").build());
        }};
        Map<String, List<String>> userGroupByOrgIdMap = userVOS.stream().collect(Collectors.groupingBy(UserVO::getOrgId, Collectors.mapping(o -> o.getName() + "_" + o.getCode(), Collectors.toList())));
        userGroupByOrgIdMap.forEach((k, v) -> {
            OrganizationVO organizationVO = orgIdOrgMap.get(k);
            if (Objects.nonNull(organizationVO)) {
                ExcelCascade tmp = ExcelCascade.builder()
                        //考虑名称中不能包含特殊字符 TODO
                        .title(organizationVO.getName() + "_" + organizationVO.getCode() + "_成员")
                        .options(v)
                        .curColIndex(4)
                        .parentColTpl("INDIRECT((IF(ISBLANK($D${}),IF(ISBLANK($C${}),IF(ISBLANK($B${}),$B${},$B${}),$C${}),$D${}))&\"_成员\")")
                        .firstRow(1)
                        .lastRow(999)
                        .build();
                excelCascades.add(tmp);
            }

        });
        return excelCascades;
    }

注意

  1. 下拉数据中只能包含有下划线“_”这一特殊字符,且不能是数字开头
  2. 保证名称唯一
3.制作下拉框

import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.util.StringUtils;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.example.demo.entity.ExcelCascade;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public class CascadeWriteHandler implements SheetWriteHandler {

    /**
     * 下拉框信息
     */
    private List<ExcelCascade> selectData;

    public CascadeWriteHandler(List<ExcelCascade> selectData) {
        this.selectData = selectData;
    }

    @Override
    public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {

    }

    @Override
    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
        //获取工作簿
        Sheet sheet = writeSheetHolder.getSheet();
        DataValidationHelper dvHelper = sheet.getDataValidationHelper();

        Workbook book = writeWorkbookHolder.getWorkbook();
        //创建一个专门用来存放地区信息的隐藏sheet页
        //因此不能在现实页之前创建,否则无法隐藏。
        String hideSheetName = "hideSheet";
        Sheet hideSheet = book.createSheet(hideSheetName);
        //设置隐藏 建议在构建模板时注释掉改行
        //book.setSheetHidden(book.getSheetIndex(hideSheet), true);
        // 将具体的数据写入到每一行中,行开头为父级区域即公式名称,后面是子区域,即下拉选项。
        AtomicInteger rowId = new AtomicInteger(1);
        List<Integer> hasSet = new CopyOnWriteArrayList<>();


        selectData.forEach(sd -> {
            int rowIdNumber = rowId.getAndIncrement();
            Row row = hideSheet.createRow(rowIdNumber);
            String title = sd.getTitle();
            row.createCell(0).setCellValue(title);
            List<String> options = sd.getOptions();
            for (int i = 0; i < options.size(); i++) {
                Cell cell = row.createCell(i + 1);
                cell.setCellValue(options.get(i));
            }
            // 添加名称管理器
            String range = getRange(1, rowIdNumber + 1, options.size());
            Name name = book.createName();
            name.setNameName(title);
            String formula = hideSheetName + "!" + range;
            name.setRefersToFormula(formula);
            //设置下拉框
            if (StringUtils.isBlank(sd.getParentColTpl()) && !hasSet.contains(sd.getCurColIndex())) {
                hasSet.add(sd.getCurColIndex());
                DataValidationConstraint expConstraint = dvHelper.createFormulaListConstraint(title);
                CellRangeAddressList expRangeAddressList = new CellRangeAddressList(sd.getFirstRow(), sd.getLastRow(), sd.getCurColIndex(), sd.getCurColIndex());
                setValidation(sheet, dvHelper, expConstraint, expRangeAddressList, "提示", "你输入的值未在备选列表中,请下拉选择合适的值");
            } else {
                if (!hasSet.contains(sd.getCurColIndex())) {
                    hasSet.add(sd.getCurColIndex());
                    for (int i = sd.getFirstRow() + 1; i < sd.getLastRow(); i++) {
                        DataValidationConstraint expConstraint = dvHelper.createFormulaListConstraint(StrUtil.format(sd.getParentColTpl(), i, i, i, i, i, i, i));
                        CellRangeAddressList expRangeAddressList = new CellRangeAddressList(i - 1, i - 1, sd.getCurColIndex(), sd.getCurColIndex());
                        setValidation(sheet, dvHelper, expConstraint, expRangeAddressList, "提示", "你输入的值未在备选列表中,请下拉选择合适的值");
                    }
                }
            }
        });
    }

    /**
     * 设置验证规则
     *
     * @param sheet       sheet对象
     * @param helper      验证助手
     * @param constraint  createExplicitListConstraint
     * @param addressList 验证位置对象
     * @param msgHead     错误提示头
     * @param msgContext  错误提示内容
     */
    private void setValidation(Sheet sheet, DataValidationHelper helper, DataValidationConstraint constraint, CellRangeAddressList addressList, String msgHead, String msgContext) {
        DataValidation dataValidation = helper.createValidation(constraint, addressList);
        dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
        dataValidation.setShowErrorBox(true);
        dataValidation.setSuppressDropDownArrow(true);
        dataValidation.createErrorBox(msgHead, msgContext);
        sheet.addValidationData(dataValidation);
    }

    /** 获取数据范围
     * @param offset   偏移量,如果给0,表示从A列开始,1,就是从B列
     * @param rowId    第几行
     * @param colCount 一共多少列
     * @return 数据范围
     */
    public String getRange(int offset, int rowId, int colCount) {
        char start = (char) ('A' + offset);
        return "$" + start + "$" + rowId + ":$" + numberToColumn(colCount+offset) + "$" + rowId;
    }


    /**
     * 10 进制转 26进制
     * @param number
     * @return
     */
    String numberToColumn(int number) {
        StringBuilder column = new StringBuilder();
        while (number > 0) {
            int remainder = (number - 1) % 26;
            column.insert(0, (char) ('A' + remainder));
            number = (number - 1) / 26;
        }
        return column.toString();
    }
}

4、导出模板

  1. 模板数据结构

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ExportExcel {

    @ExcelProperty(value = "业务名称", index = 0)
    private String name;

    @ExcelProperty(value = "责任部门", index = 1)
    private String org;

    @ExcelProperty(value = "责任科室", index = 2)
    private String dept;

    @ExcelProperty(value = "责任班组", index = 3)
    private String team;

    @ExcelProperty(value = "责任人", index = 4)
    private String rspUserName;

    @ExcelProperty(value = "业务开始时间", index = 5)
    private String startTime;

    @ExcelProperty(value = "业务完成时间", index = 6)
    private String endTime;

    @ExcelProperty(value = "备注", index = 7)
    private String goal;
}

  1. 小试牛刀
 public static void main(String[] args) {
        List<ExcelCascade> excelCascades = init();

        // 写出数据
        EasyExcel.write(new File("业务导入模板.xlsx"), ExportExcel.class)
                .sheet("sheet1")
                .registerWriteHandler(new CascadeWriteHandler(excelCascades))
                .doWrite(new ArrayList<>());
    }
  1. 其他类

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

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrganizationVO {
    /**
     * 部门Id
     */
    private String id;

    /**
     * 部门名称
     */
    private String name;

    /**
     * 部门代码
     */
    private String code;

    /**
     * 父级部门Id
     */
    private String parentId;

    /**
     * 部门类型
     */
    private String type;

}



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

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserVO {
    /**
     * 用户Id
     */
    private String id;

    /**
     * 用户名称
     */
    private String name;

    /**
     * 用户代码
     */
    private String code;


    /**
     * 归属部门Id
     */
    private String orgId;

}

总结

  1. 书到用时方恨少,Java 基础,Excel 使用
  2. 敢于思考,敢于尝试,例如,在根据选择部门级别的不同,做到选择责任人下拉框下拉数据范围的确定?;下拉数据是以列的方式写入还是以行的方式写入?
  3. 优化,比如使用注解的方式,模板的替换?

参考文档

使用EasyExcel导出模板并设置级联下拉及其原理分析

致谢

十分感谢参考文档提供的示例,以及对Excel设置下拉框步骤的讲解。同时也感谢十分强大的Excel处理工具Easyexcel。

你可能感兴趣的:(java,后端)