使用Hutool的ExcelWriter导出复杂模板,支持下拉选项级联筛选

昨天刚接了一个导入导出的需求,前端页面有三个下拉选择框有相对应的关联关系。在生成模板的时候也需要把这种关系给设置出来。 最终实现的效果为 A D E三列的数据能够实现级联效果,

  1. A列下拉选择之后,根据选择的内容是否包含特定条件,给D设置不同的数据有效性。
  2. D列选择之后,匹配对应的id给X列设置值,
  3. E列的数据有效性根据X列的值匹配对应的数据
  4. Y列的值根据E列的值匹配对应的Code

1. 具体的实现思路

  1. 通过隐藏Sheet页将需要用到的数据写入
  2. 通过名称管理器设置数据引用
  3. 通过几组特定的函数实现功能
函数 作用
IF(ISNUMBER(FIND("中介",A{})),hsAgencyNames,CompanyNames) 从A列判断是否包含特定字符串,然后设置对应的 名称管理器引用
IFERROR(VLOOKUP($D{}, CompanyIdMap, 2, FALSE), "") 根据D列的值,去CompanyIdMap这个名称引用中查询对应的ID
CompanyData!A2:A:100 创建名称管理器的引用范围

2. 拆分实现

1. 导出模板功能

java 复制代码
public void downloadTemplateV2(HttpServletResponse response) throws Exception {
    // 1. 设置响应头
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setCharacterEncoding("utf-8");
    String fileName = URLEncoder.encode("级联导入模板", "UTF-8");
    response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
    // 从自己的数据库获取数据
    Map<String, CompanyVo> seaCmpNameIdMap = getCompanyMap();
    // 根据自己的业务调整
    Map<String, List<Org>> orgMap = new HashMap();

    String topTipStr = "导入提示信息";
    try (OutputStream out = response.getOutputStream()) {
        // 2. 创建ExcelWriter
        ExcelWriter writer = ExcelUtil.getWriter(true); // true表示创建xlsx格式
        Sheet sheet = writer.getSheet();
        Workbook workbook = writer.getWorkbook();
        // 写入映射数据到工作表并创建名称管理器
        writeCompanyAndOrgData(workbook, seaCmpNameIdMap, orgMap);
        // 设置级联下拉和对应列填充
        setCompanyValidationAndFormula(sheet, workbook);
        setDeptValidationAndFormula(sheet, workbook);

        List<String> headers = Arrays.asList(
                "表头1","表头2","表头3","表头4"
        );
        writer.merge(0, 0, 0, 24, topTipStr, true);
        writer.passRows(1);
        writer.writeHeadRow(headers);

        // 日期类型限制
        restrictCell2DateFormat(sheet, 1);;
        // 字典项类型的数据下拉设置
        setDictDataValidation(sheet, "spiChecktypeHs", 0);
        for (int i = 0; i < headers.size(); i++) {
            String headerText = headers.get(i);
            // 计算宽度:文字长度 × 2 × 256(Excel的宽度单位)
            int columnWidth = headerText.length() * 2 * 256;
            // 设置最小宽度(避免过窄)
            columnWidth = Math.max(columnWidth, 8 * 256);
            // 设置列宽
            sheet.setColumnWidth(i, columnWidth);
        }
        writer.flush(out).close();
    } catch (Exception e) {
        log.info("下载模板失败", e);
    }
}

2.写入映射数据和创建名称管理器

java 复制代码
private void writeCompanyAndOrgData(Workbook workbook, Map<String, HsCompanyVo> seaCmpNameIdMap,
                                    Map<String, List<Org>> orgMap) {
    // ========== 1. 写入企业数据 ==========
    Sheet companySheet = workbook.createSheet("CompanyData");
    workbook.setSheetHidden(workbook.getSheetIndex(companySheet), true);

    // 企业数据表头
    Row companyHeader = companySheet.createRow(0);
    companyHeader.createCell(0).setCellValue("企业名称");
    companyHeader.createCell(1).setCellValue("企业ID");

    int companyRow = 1;
    for (Map.Entry<String, HsCompanyVo> entry : seaCmpNameIdMap.entrySet()) {
        Row row = companySheet.createRow(companyRow++);
        row.createCell(0).setCellValue(entry.getKey());
        row.createCell(1).setCellValue(entry.getValue().getId());
    }

    // 创建企业名称列表和ID映射的名称管理器
    // 这里我遇到了一个问题, 就是第二个参数也就是名称管理器的名字 必须以_或者 字母开头
    createName(workbook, "CompanyNames", "CompanyData!$A$2:$A$" + companyRow);
    // 
    createName(workbook, "CompanyIdMap", "CompanyData!$A$2:$B$" + companyRow);
    
    
    Sheet orgSheet = workbook.createSheet("OrgData");
    workbook.setSheetHidden(workbook.getSheetIndex(orgSheet), true);

    // 单位数据表头
    Row orgHeader = orgSheet.createRow(0);
    orgHeader.createCell(0).setCellValue("企业ID");
    orgHeader.createCell(1).setCellValue("单位名称");
    orgHeader.createCell(2).setCellValue("单位Code");

    int orgRow = 1;
    int startRow = 2;
    String orgDataNameNamePrefix = "_";
    // 写入所有单位数据
    for (Map.Entry<String, List<Org>> entry : orgMap.entrySet()) {
        String companyId = entry.getKey();
        List<Org> orgs = entry.getValue();

        if (CollUtil.isNotEmpty(orgs)) {
            for (Org org : orgs) {
                Row row = orgSheet.createRow(orgRow++);
                row.createCell(0).setCellValue(companyId);
                row.createCell(1).setCellValue(org.getShortName());
                row.createCell(2).setCellValue(org.getCode()); // 正确写入code
            }
            String referenceStr = StrUtil.format("OrgData!$B${}:$B${}", startRow, orgRow);
            log.info("{} ::: {}", orgDataNameNamePrefix + companyId, referenceStr);
            createName(workbook, orgDataNameNamePrefix + companyId, referenceStr);
            startRow = orgRow + 1;
        }
    }
    log.info("最终的startRow: {}", startRow);
    createName(workbook, "OrgDataMapping", "OrgData!$B$2:$C$" + (startRow - 1));
    // 保护工作表
    companySheet.protectSheet("protected");
    orgSheet.protectSheet("protected");
}
// 创建名称管理器还有引用范围
private void createName(Workbook workbook, String nameName, String reference) {
    if (workbook instanceof XSSFWorkbook) {
        XSSFName name = ((XSSFWorkbook) workbook).createName();
        name.setNameName(nameName);
        name.setRefersToFormula(reference);
    }
}

3. 设置数据有效性,和根据这一列自动填充值的方法

我的E列的数据是根据Y列的数据设置的数据有效性,所以我把Y列用了 _+id的方式做了 名称管理器 引用。所以给E列设置 formula的时候直接写 下百年的代码就可以了, 通过INDIRECT函数引用对应的名称管理器

java 复制代码
StrUtil.format("INDIRECT("_"&X{})", rowIndex + 1)
java 复制代码
private void setCompanyValidationAndFormula(Sheet sheet, Workbook workbook) {
    XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper((XSSFSheet) sheet);
    CellRangeAddressList companyRange;
    String formula;
    String orgIdFormula;
    XSSFDataValidationConstraint companyConstraint;
    XSSFDataValidation companyValidation;
    Row row;
    // 2. 创建锁定样式用于受检企业ID列
    CellStyle lockedStyle = workbook.createCellStyle();
    lockedStyle.setLocked(true);
    for (int rowIndex = 1; rowIndex <= templateEndRow; rowIndex++) {
        // 只为当前行的第4列(索引3)设置数据验证
        companyRange = new CellRangeAddressList(rowIndex, rowIndex, 3, 3);
        // 判断A列选中的数据中是否有删选条件
        // 有的话用 trueName这个名称管理器 否则用falseName 这个要根据自己的条件还有业务逻辑自己修改一下
        formula = StrUtil.format("=IF(ISNUMBER(FIND("筛选条件",A{})),trueName,falseName)", rowIndex + 1);
        log.info("当前函数公式 {}", formula);
        companyConstraint = (XSSFDataValidationConstraint)
                dvHelper.createFormulaListConstraint(formula);

        companyValidation = (XSSFDataValidation)
                dvHelper.createValidation(companyConstraint, companyRange);
        companyValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
        companyValidation.createErrorBox("输入错误", "请从下拉列表选择受检企业");
        companyValidation.setSuppressDropDownArrow(true); // 对于动态公式,建议隐藏下拉箭头
        sheet.addValidationData(companyValidation);

        row = sheet.getRow(rowIndex) != null ? sheet.getRow(rowIndex) : sheet.createRow(rowIndex);
        Cell idCell = row.createCell(23);
        // 设置VLOOKUP公式,根据D列查找对应的ID CompanyIdMap可以从 公式 名称管理器中查询到
        orgIdFormula = StrUtil.format("IFERROR(VLOOKUP($D{}, CompanyIdMap, 2, FALSE), "")", rowIndex + 1);
        idCell.setCellFormula(orgIdFormula);
        // 应用锁定样式
        idCell.setCellStyle(lockedStyle);
    }
}

4. 设置指定列只能填写日期类型和字典项数据有效性

java 复制代码
private void restrictCell2DateFormat(Sheet sheet, int col) {
    CellRangeAddressList regions = new CellRangeAddressList(1, templateEndRow, col, col);
    XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper((XSSFSheet) sheet);
    // 设置数据验证约束:日期格式,介于最小日期和最大日期之间
    XSSFDataValidationConstraint dvConstraint = (XSSFDataValidationConstraint) dvHelper.createDateConstraint(
            XSSFDataValidationConstraint.OperatorType.BETWEEN,
            "DATE(1900,1,1)",  // 最小日期
            "DATE(2100,12,31)", // 最大日期
            "yyyy-mm-dd"        // 日期格式
    );

    // 创建数据验证规则
    XSSFDataValidation validation = (XSSFDataValidation) dvHelper.createValidation(dvConstraint, regions);

    // 设置验证失败时的提示信息
    validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
    validation.createErrorBox("输入错误", "请输入正确的日期格式:yyyy-MM-dd");

    // 设置输入提示信息
    validation.createPromptBox("日期格式提示", "请输入 yyyy-MM-dd 格式的日期,例如:2024-01-01");
    validation.setShowPromptBox(true);
    validation.setSuppressDropDownArrow(false);

    // 将数据验证添加到工作表
    sheet.addValidationData(validation);

    // 设置单元格格式为日期格式
    CellStyle dateCellStyle = sheet.getWorkbook().createCellStyle();
    DataFormat format = sheet.getWorkbook().createDataFormat();
    dateCellStyle.setDataFormat(format.getFormat("yyyy-MM-dd"));

    sheet.addValidationData(validation);
}
java 复制代码
/**
 * 设置字典项数据有效性
 *
 * @param dictType
 */
private void setDictDataValidation(Sheet sheet, String dictType, int col) {
    Map<String, String> dictMap = dictUtil.getDictMap(dictType);
    log.info("当前获取的dictMap {}", dictMap);
    DataValidationHelper dvHelper = sheet.getDataValidationHelper();
    DataValidationConstraint constraint = dvHelper.createExplicitListConstraint(dictMap.values().toArray(new String[0]));
    CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(1, templateEndRow, col, col);
    DataValidation validation = dvHelper.createValidation(constraint, cellRangeAddressList);
    // 显示下拉箭头
    validation.setSuppressDropDownArrow(true);
    // 显示提示框
    validation.setShowPromptBox(true);
    // 设置为INFO级别允许输入
    validation.setErrorStyle(DataValidation.ErrorStyle.INFO);
    sheet.addValidationData(validation);
}
相关推荐
程序员鱼皮1 小时前
10个免费的网站分析工具,竟然比付费的更香?
后端·程序员·数据分析
码一行1 小时前
Eino AI 实战: Eino 的文档加载与解析
后端·go
码一行1 小时前
Eino AI 实战:DuckDuckGo 搜索工具 V1 与 V2
后端·go
未秃头的程序猿1 小时前
🚀 设计模式在复杂支付系统中的应用:策略+工厂+模板方法模式实战
后端·设计模式
踏浪无痕1 小时前
@Transactional的5种失效场景和自检清单
spring boot·后端·spring cloud
6***v4171 小时前
搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程
开发语言·后端·golang
水痕011 小时前
go使用cobra来启动项目
开发语言·后端·golang
用户345848285052 小时前
python在使用synchronized关键字时,需要注意哪些细节问题?
后端
代码扳手2 小时前
Golang 高效内网文件传输实战:零拷贝、断点续传与 Protobuf 指令解析(含完整源码)
后端·go