前瞻
工作中,需要用到固定模板,考虑到客户的使用习惯,于是就将模板事先定制好,只需要客户按照固定的模板填写即可,大大提高用户的体验

引入Maven依赖

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>easyexcel</artifactId>
	<version>3.2.1</version>
</dependency>

主要功能
工具类中功能主要包含:
1、创建二级、三级级联关系下拉框
2、创建单选下拉框
3、下拉框值校验,只能选择下拉框中的值
4、给表头添加批注

代码实现
创建工具类,实现SheetWriteHandler方法,重写afterSheetCreate方法来实现上面的这些功能

package com.ponshine.applets.excle;

import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;

import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author LiDongYang
 * @description:
 * @date 2024/5/6 10:16
 */
public class CascadeWriteHandler implements SheetWriteHandler {


    private static char[] alphabet = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
            'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};


    //省
    private List<String> provinceList;
    //市
    Map<String, List<String>> cityMap;
    //区
    Map<String, List<String>> areaMap;

    //省列位置
    private int provinceIndex;
    //市列位置
    private int cityIndex;
    //区列位置
    private int areaIndex;

    List<String> stateList;

    private Integer type;//类型。1-三级级联关系,2-二级级联关系


    public CascadeWriteHandler(List<String> provinceList, Map<String, List<String>> cityMap, Map<String, List<String>> areaMap,
                               int provinceIndex, int intcityIndex, int areaIndex, List<String> stateList,int type) {
        this.provinceList = provinceList;
        this.cityMap = cityMap;
        this.areaMap = areaMap;
        this.provinceIndex = provinceIndex;
        this.cityIndex = intcityIndex;
        this.areaIndex = areaIndex;
        this.stateList = stateList;
        this.type = type;
    }



    @Override
    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
        //获取工作簿
        Sheet sheet = writeSheetHolder.getSheet();
        Workbook book = writeWorkbookHolder.getWorkbook();
        if (type == 1) {
            //三级级联关系
            //写入区域信息数据
            writeData(sheet, book, provinceList, cityMap, areaMap, provinceIndex, cityIndex, areaIndex, "area",true);
            addAnnotation(sheet,provinceIndex,null);
            addAnnotation(sheet,cityIndex,null);
            addAnnotation(sheet,areaIndex,null);
        } else if (type == 2) {
            //二级级联关系
            //最后一个参数为false时,说明时二级关联,二级目录没有第三个子下拉框,所以第一个null是没有级联关系,所以为空,第二个null是位置,不存在,所以为null
            writeData(sheet, book, provinceList, cityMap, null, provinceIndex, cityIndex, null, "area",false);
            addAnnotation(sheet,provinceIndex,null);
            addAnnotation(sheet,cityIndex,null);
            addAnnotation(sheet,areaIndex,null);
        }
        //设置状态属性值下拉
        addDropDownList(sheet, 9, 9, stateList);
        addAnnotation(sheet,9,null);

        addAnnotation(sheet,0,"若输入为空,则按规则自动生成,若输入数字编码,则以输入为准");//测站编码
        addAnnotation(sheet,1,"字段必填");//测站名称

    }
    /**
     * 添加批注
     * @param sheet
     * @param col 第几列 从0开始
     * @param content 批注内容
     */
    private void addAnnotation(Sheet sheet,int col,String content) {
        Drawing<?> drawingPatriarch = sheet.createDrawingPatriarch();
        String annotation = null;
        if (Objects.isNull(content)){
            annotation = "必填字段,不能为空";
        }else {
            annotation = content;
        }
        //测站名称
        Comment name = drawingPatriarch.createCellComment(
                new XSSFClientAnchor(0, 0, 0, 0, (short) col, 0, (short) col+1, 1));
        name.setString(new XSSFRichTextString(annotation));
        sheet.createRow(0).createCell(1);
        sheet.getRow(0).getCell(1).setCellComment(name);
    }

    /**
     * 写入级联关系数据
     * @param sheet
     * @param book
     * @param oneList
     * @param twoMap
     * @param threeMap
     * @param oneIndex
     * @param twoIndex
     * @param threeIndex
     * @param areaSheet
     * @param isThree 判断是否三级关系 true有 false没有
     */
    private void writeData(Sheet sheet, Workbook book, List<String> oneList, Map<String, List<String>> twoMap, Map<String, List<String>> threeMap,
                           int oneIndex, int twoIndex, Integer threeIndex, String areaSheet, boolean isThree) {
        //创建一个专门用来存放地区信息的隐藏sheet页
        //因此不能在现实页之前创建,否则无法隐藏。
        Sheet hideSheet = book.createSheet(areaSheet);
        book.setSheetHidden(book.getSheetIndex(hideSheet), true);
        // 将具体的数据写入到每一行中,行开头为父级区域,后面是子区域。
        int rowId = 0;
        Row proviRow = hideSheet.createRow(rowId++);
        proviRow.createCell(0).setCellValue("大类列表");
        for (int i = 0; i < oneList.size(); i++) {
            Cell proviCell = proviRow.createCell(i + 1);
            proviCell.setCellValue(oneList.get(i));
        }
        Iterator<String> keyIterator = twoMap.keySet().iterator();
        while (keyIterator.hasNext()) {
            String key = keyIterator.next();
            List<String> son = twoMap.get(key);
            Row row = hideSheet.createRow(rowId++);
            for (int i = 0; i < son.size(); i++) {
                Cell cell = row.createCell(i + 1);
                cell.setCellValue(son.get(i));
            }
            // 添加名称管理器
            String range = getRange(1, rowId, son.size());
            Name name = book.createName();
            name.setNameName(key);
            String formula = areaSheet + "!" + range;
            name.setRefersToFormula(formula);
        }
        //这个是校验当前级联关系是否存在三级关系,存在则进行三级级联操作,不存在则说明是二级级联
        if (isThree){
            Iterator<String> keyIterator1 = threeMap.keySet().iterator();
            while (keyIterator1.hasNext()) {
                String key = keyIterator1.next();
                List<String> son = threeMap.get(key);
                Row row = hideSheet.createRow(rowId++);
                for (int i = 0; i < son.size(); i++) {
                    Cell cell = row.createCell(i + 1);
                    cell.setCellValue(son.get(i));
                }
                // 添加名称管理器
                String range = getRange(1, rowId, son.size());
                Name name = book.createName();
                name.setNameName(key);
                String formula = areaSheet + "!" + range;
                name.setRefersToFormula(formula);
            }
        }
        ///开始设置(大类小类)下拉框
        DataValidationHelper dvHelper = sheet.getDataValidationHelper();
        // 大类规则
        DataValidationConstraint expConstraint = dvHelper.createExplicitListConstraint(oneList.toArray(new String[]{}));
        CellRangeAddressList expRangeAddressList = new CellRangeAddressList(1, 999, oneIndex, oneIndex);//todo 调整模板后位置需同步修改firstCol
        setValidation(sheet, dvHelper, expConstraint, expRangeAddressList, "提示", "你输入的值未在备选列表中,请下拉选择合适的值");

        // 小类规则(各单元格按个设置)
        // "INDIRECT($A$" + 2 + ")" 表示规则数据会从名称管理器中获取key与单元格 A2 值相同的数据,如果A2是浙江省,那么此处就是浙江省下面的市
        // 为了让每个单元格的公式能动态适应,使用循环挨个给公式。
        // 循环几次,就有几个单元格生效,次数要和上面的大类影响行数一一对应,要不然最后几个没对上的单元格实现不了级联
        for (int i = 2; i < 1000; i++) {
            CellRangeAddressList rangeAddressList = new CellRangeAddressList(i - 1, i - 1, twoIndex, twoIndex);//todo 调整模板后位置需同步修改firstCol
            DataValidationConstraint formula = dvHelper.createFormulaListConstraint("INDIRECT($" + getExcelColumn(twoIndex - 1) + "$" + i + ")");//todo 调整模板后位置需同步修改字母
            setValidation(sheet, dvHelper, formula, rangeAddressList, "提示", "你输入的值未在备选列表中,请下拉选择合适的值");
        }
        if (isThree){
            for (int i = 2; i < 1000; i++) {
                CellRangeAddressList rangeAddressList = new CellRangeAddressList(i - 1, i - 1, threeIndex, threeIndex);//todo 调整模板后位置需同步修改firstCol
                DataValidationConstraint formula = dvHelper.createFormulaListConstraint("INDIRECT($" + getExcelColumn(threeIndex - 1) + "$" + i + ")");//todo 调整模板后位置需同步修改字母
                setValidation(sheet, dvHelper, formula, rangeAddressList, "提示", "你输入的值未在备选列表中,请下拉选择合适的值");
            }
        }
    }


    /**
     * 设置验证规则
     *
     * @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 sheet
     * @param firstCol
     * @param lastCol
     * @param list
     */
    private static void addDropDownList(Sheet sheet, int firstCol, int lastCol, List<String> list) {
        DataValidationHelper helper = sheet.getDataValidationHelper();
        DataValidationConstraint constraint = helper.createExplicitListConstraint(list.toArray(new String[0]));
        //firstRow是从第一行开始,lastRow为到第几行结束,即1-999行设置为下拉框 firstCol为当前设置下拉框的列数
        CellRangeAddressList addressList = new CellRangeAddressList(1, 999, firstCol, lastCol);
        DataValidation dataValidation = helper.createValidation(constraint, addressList);
        // 设置下拉框
        dataValidation.createErrorBox("错误", "只能选择列表中的值");//错误提示
        dataValidation.setSuppressDropDownArrow(true);
        dataValidation.setShowErrorBox(true);
        sheet.addValidationData(dataValidation);

    }


    /**
     * @param offset   偏移量,如果给0,表示从A列开始,1,就是从B列
     * @param rowId    第几行
     * @param colCount 一共多少列
     * @return 如果给入参 1,1,10. 表示从B1-K1。最终返回 $B$1:$K$1
     * @author denggonghai 2016年8月31日 下午5:17:49
     */
    public String getRange(int offset, int rowId, int colCount) {
        char start = (char) ('A' + offset);
        if (colCount <= 25) {
            char end = (char) (start + colCount - 1);
            return "$" + start + "$" + rowId + ":$" + end + "$" + rowId;
        } else {
            char endPrefix = 'A';
            char endSuffix = 'A';
            if ((colCount - 25) / 26 == 0 || colCount == 51) {// 26-51之间,包括边界(仅两次字母表计算)
                if ((colCount - 25) % 26 == 0) {// 边界值
                    endSuffix = (char) ('A' + 25);
                } else {
                    endSuffix = (char) ('A' + (colCount - 25) % 26 - 1);
                }
            } else {// 51以上
                if ((colCount - 25) % 26 == 0) {
                    endSuffix = (char) ('A' + 25);
                    endPrefix = (char) (endPrefix + (colCount - 25) / 26 - 1);
                } else {
                    endSuffix = (char) ('A' + (colCount - 25) % 26 - 1);
                    endPrefix = (char) (endPrefix + (colCount - 25) / 26);
                }
            }
            return "$" + start + "$" + rowId + ":$" + endPrefix + endSuffix + "$" + rowId;
        }
    }

    /**
     * 将数字列转化成为字母列
     *
     * @param num
     * @author: Jesse
     * @date: 2023/6/27
     * @return: String
     */
    private static String getExcelColumn(int num) {
        String column = "";
        int len = alphabet.length - 1;
        int first = num / len;
        int second = num % len;
        if (num <= len) {
            column = alphabet[num] + "";
        } else {
            column = alphabet[first - 1] + "";
            if (second == 0) {
                column = column + alphabet[len] + "";
            } else {
                column = column + alphabet[second - 1] + "";
            }
        }
        return column;
    }

    public static void setResponseConfig(HttpServletResponse response,String fileNameTemp) {
        String fileName = new String(fileNameTemp.getBytes(), StandardCharsets.ISO_8859_1);
        response.setContentType("application/msexcel");
        response.setCharacterEncoding("utf8");
        response.setHeader("Content-disposition", "attachment;filename=" + fileName );
    }

}

创建实体类
该实体类用于确认每个参数表头的位置,回按照实体类中的顺序来进行填补表头,所以下面main中,各个表头下拉框的位置,也要根据实体类中位置的变化进行修改,
其中
@ColumnWidth(22) 表示表头表格的长度
@HeadRowHeight(25) 表示表头表格的宽度
@ExcelProperty 表示表头的名称

package com.ponshine.applets.excle.entity;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;

/**
 * @author LiDongYang
 * @description:
 * @date 2024/5/6 13:23
 */
@ColumnWidth(22)
@HeadRowHeight(25)
@Data
public class DownloadInfoDto {
    /** 编码 */
    @ExcelProperty("编码")
    private String sid;

    @ExcelProperty("名称")
    private String sname;

    /** 省份 */
    @ExcelProperty("省份")
    private String province;

    /** 地市 选项:邮政编码=地市 */
    @ExcelProperty("地市")
    private String city;

    /** 区县 选项:邮政编码=区县 */
    @ExcelProperty("区县")
    private String county;

    /** 状态 选项:0=正常 1=异常  */
    @ExcelProperty("测站状态")
    private String state;


}

创建main或者或者接口生成
我们以实际情况为例,按照调用接口的形式进行下载表格
下面中最后一个参数type=1,我们演示的是三级级联,若想创建二级级联关系,可更改type类型来进行实现,此时对应的参数需要发升变化,工具类的注释中有明确说明,这里不过多解释

package com.ponshine.applets.excle.service;

import com.alibaba.excel.EasyExcel;
import com.ponshine.applets.excle.CascadeWriteHandler;
import com.ponshine.applets.excle.entity.DownloadInfoDto;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

/**
 * @author LiDongYang
 * @description:
 * @date 2024/6/11 10:52
 */
@Service
public class ExcelService {
    public void downModel(HttpServletResponse response) {

        /**
         * 创建地市级联菜单信息
         * 示例:
         *  List<String> areaOneList = new ArrayList<>();
         *  Map<String, List<String>> areaMap = new HashMap<>();
         *         areaOneList.add("广东省");
         *         areaOneList.add("湖北省");
         *         省份-地市
         *         areaMap.put("广东省", Arrays.asList("广州市", "佛山市"));
         *         areaMap.put("湖北省", Arrays.asList("武汉市", "荆州市"));
         *         地市-区县
         *         areaMap.put("广州市", Arrays.asList("白云区", "越秀区"));
         *         areaMap.put("佛山市", Arrays.asList("顺德区", "南海区"));
         *
         */
        List<String> areaOneList = new ArrayList<>();//记录所有省份
        Map<String, List<String>> provinceToCityMap = new HashMap<>();//记录省份-地市关系
        Map<String, List<String>> cityToCountMap = new HashMap<>();//记录地市-区县关系
        areaOneList.add("福建省");
        areaOneList.add("河南省");
        provinceToCityMap.put("福建省", Arrays.asList("杭州市", "温州市"));
        provinceToCityMap.put("河南省", Arrays.asList("郑州市", "漯河市"));
        cityToCountMap.put("杭州市", Arrays.asList("余杭区", "临安区"));
        cityToCountMap.put("温州市", Arrays.asList("文成区", "鹿城区"));
        cityToCountMap.put("郑州市", Arrays.asList("金水区", "二七区"));
        cityToCountMap.put("漯河市", Arrays.asList("郾城区", "源汇区"));

        List<String> stateList = new ArrayList<>();//状态
        stateList.add("正常");
        stateList.add("异常");

        List<DownloadInfoDto> dataList = new ArrayList<>();
        try {
            String fileName = "模板.xlsx";
            CascadeWriteHandler.setResponseConfig(response, fileName);
            EasyExcel.write(response.getOutputStream(), DownloadInfoDto.class)
                    .sheet("sheet").head(DownloadInfoDto.class)
                    // 前面三个参数表示省 市 区的对应关系,后面三个数字表示省市区对应下拉框的位置, stateList为单选下拉框的值,stateIndex为状态下拉框的位置,type表示级联的等级,1:三级级联,2:二级级联
                    .registerWriteHandler(new CascadeWriteHandler(areaOneList, provinceToCityMap, cityToCountMap, 2, 3, 4, stateList, 5,1)).
                    doWrite(dataList);
        } catch (IOException e) {
            System.out.println("下载模板异常");
        }
    }
}

最终效果展示
批注展示
在这里插入图片描述
级联下拉框展示
在这里插入图片描述

单选下拉框展示
在这里插入图片描述

优缺点
优点:
1、所有公共方法都已提取,主要包含:添加批注,单选、多选下拉框,复制即使用
2、快捷、便利,简单上手
缺点:
1、定制化,由于每个需求导出的模板不通,无法实现统一覆盖,需要根据需求进行定制化工具类

注意事项
单选下拉框目前未实现多选值的功能,当需求中出现该类现象时,可能无法正常使用该功能,需要于产品进行沟通,或者查询相关资料进行优化

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐