springboot实现下载Excel模板下载,实现单选下拉框、二级或二级关联、表头添加备注
springboot实现定制化excel表格下载,实现二级、三级级联下拉框,表头批注、单选下拉框等功能
前瞻
工作中,需要用到固定模板,考虑到客户的使用习惯,于是就将模板事先定制好,只需要客户按照固定的模板填写即可,大大提高用户的体验
引入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、定制化,由于每个需求导出的模板不通,无法实现统一覆盖,需要根据需求进行定制化工具类
注意事项
单选下拉框目前未实现多选值的功能,当需求中出现该类现象时,可能无法正常使用该功能,需要于产品进行沟通,或者查询相关资料进行优化
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)