0%

Easyexcel常用示例代码

前言

  • 记录阿里的easyexcel工具的相关使用:
  • 导入、导出、下拉、级联下拉

一、快速开始

1、导入依赖

  • 导入pom
1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.6</version>
</dependency>
  • 通过pom依赖,我们知道它是基于poi再次封装的。

2、开始使用

  • easyexcel封装了一个工厂类:EasyExcelFactory,我们直接调用即可。
  • 一般在项目中会用到:EasyExcel它其实就是EasyExcelFactory的子类且没有自己的功能。

3、写Excel

  • EasyExcel 封装了很多方法,可以自己点进去看
  • 导出模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class Demo {
public static void main(String[] args) {
EasyExcel.write("D:/demo.xlsx", ExcelDataDto.class).sheet("模板").doWrite(data());
}

public static List<ExcelDataDto> data(){
List<ExcelDataDto> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
ExcelDataDto data = new ExcelDataDto();
data.setString("字符串" + i);
data.setDate(new Date());
data.setDoubleData(0.56);
list.add(data);
}
return list;
}
}

@Data
public class ExcelDataDto {
// @ExcelProperty 是excel的标题名称注解
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
}

@Slf4j
public class ExcelDataListener extends AnalysisEventListener<ExcelDataDto> {

public ExcelDataListener(){}

// private Service todoService;
// public ExcelDataListener(Service todoService){
// this.todoService = todoService;
// }

@Override
public void invoke(ExcelDataDto excelDataDto, AnalysisContext analysisContext) {
log.info("解析到一条数据:{}", JSON.toJSONString(excelDataDto));
// todo 保存数据库
// todoService.save();
}

@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("所有数据解析完成!");
}
}

4、读Excel

  • 读取excel
1
2
3
4
5
6
7
8

public class Demo {
public static void main(String[] args) {
// 这里读取D盘下的dmeo.xlsx 文件
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read("D://demo.xlsx", ExcelDataDto.class, new ExcelDataListener()).sheet().doRead();
}
}

二、上传下载

  • 上次下载,需要用到servlet,这边直接使用springboot
  • dmeo如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Controller
@RequestMapping("/test")
public class WebTestController {

/**
* 文件上传
*/
@PostMapping("/upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), ExcelDataDto.class, new ExcelDataListener()).sheet().doRead();
return "success";
}

/**
* 文件下载(失败了会返回一个有部分数据的Excel)
*/
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), ExcelDataDto.class).sheet("模板").doWrite(new ArrayList());
}
}

三、图片导出

  • 为啥只有导出呢,因为目前( (ง •_•)ง 2021-05-28) easyexcel暂不支持图片的导入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Controller
@RequestMapping("/images")
public class ImagesController {

/**
* 导出图片类型
* @param response
* @throws Exception
*/
@GetMapping("/export")
public void export(HttpServletResponse response) throws Exception {
ClassPathResource classPathResource = new ClassPathResource("images/avatar.jpg");
InputStream inputStream =classPathResource.getInputStream();
// InputStream resourceAsStream = this.getClass().getResourceAsStream("/images/avatar.jpg");

String fileName = URLEncoder.encode("图片excel", "UTF-8").replaceAll("\\+", "%20");
try {
List<ImageDto> list = new ArrayList<ImageDto>();
ImageDto imageDto = new ImageDto();
list.add(imageDto);
File file = ResourceUtils.getFile("classpath:images/avatar.jpg");
// 放入五种类型的图片 实际使用只要选一种即可
imageDto.setByteArray(FileUtils.readFileToByteArray(file));
imageDto.setFile(file);
imageDto.setString(file.getAbsolutePath());
imageDto.setInputStream(inputStream);
imageDto.setUrl(new URL("http://portrait.gitee.com/uploads/avatars/user/286/858520_rstyro_1578934054.png!avatar30"));

response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), ImageDto.class).sheet().doWrite(list);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}

}

四、自定义格式转换

  • 格式转换,一般用的最多的就是时间格式
  • 时间类型一般有:Date 和 LocalDateTime
  • 如果使用的是Date可以用注解格式化,如果使用LocalDateTime需要自己写转换器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* ColumnWidth 定义列宽
*/
/**
* ColumnWidth 定义列宽
*/
@ColumnWidth(25)
@Data
@Accessors(chain = true)
public class UserDto {

@ExcelProperty("用户名称")
private String username;

@ExcelProperty("年龄")
private Integer age;

@ExcelProperty("部门")
@DropDownFields(source = {"财务部","人事部","研发部","商务部"})
private String department;

@ExcelProperty("职业")
private String occupation;

/**
* 自定义日期转换器 LocalDateTimeConverter
*/
@ColumnWidth(50)
@ExcelProperty(value = "注册时间",converter = LocalDateTimeConverter.class)
private LocalDateTime createTime;

/**
* 使用注解 @DateTimeFormat
*/
@ExcelProperty(value = "发财时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ColumnWidth(50)
private Date startTime;
}



/**
* 自定义 LocalDateTime 日期转换器
*/
public class LocalDateTimeConverter implements Converter<LocalDateTime> {

@Override
public Class<LocalDateTime> supportJavaTypeKey() {
return LocalDateTime.class;
}

@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}

@Override
public LocalDateTime convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return LocalDateTime.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

@Override
public CellData<String> convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
return new CellData<>(value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}

}
  • 如果是其他需要用到转换,只需实现Converter<T> 接口即可。
  • convertToJavaData() 这个方法是从Excel转到Java类型,反之convertToExcelData()是Java到Excel。
  • 自定义转换器,只需重写上面两个方法即可。

五、实现下拉框

  • 实现动态下拉框导入功能
  • 为了方便复用,这边使用自定义注解的方式

1、自定义注解

  • 我们自定义一个放在字段上的注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DropDownFields {
/**
* 固定下拉
* @return
*/
String[] source() default {};

/**
* 动态下拉内容,可查数据库返回等,其他操作
* @return
*/
Class[] sourceClass() default {};

/**
* 下拉类型枚举,可能动态查询的时候需要用到
* @return
*/
DropDownType type() default DropDownType.NONE;

}

2、自定义Handler

  • 自定义一个Handler,给Excel设置下拉框数据约束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* excel 下拉框 handler
*/
public class DropDownWriteHandler implements SheetWriteHandler {

private final Map<Integer, String[]> map;

/**
* 设置阈值,避免生成的导入模板下拉值获取不到
*/
private static final Integer LIMIT_NUMBER = 100;

public DropDownWriteHandler(Map<Integer, String[]> map) {
this.map = map;
}

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

}

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

// 这里可以对cell进行任何操作
Sheet sheet = writeSheetHolder.getSheet();
DataValidationHelper helper = sheet.getDataValidationHelper();

// k 为存在下拉数据集的单元格下标 v为下拉数据集
map.forEach((k, v) -> {
// 设置下拉单元格的首行 末行 首列 末列
CellRangeAddressList rangeList = new CellRangeAddressList(1, 65536, k, k);
// 如果下拉值总数大于100,则使用一个新sheet存储,避免生成的导入模板下拉值获取不到
if (v.length > LIMIT_NUMBER) {
//定义sheet的名称
//1.创建一个隐藏的sheet 名称为 hidden + k
String sheetName = "hidden" + k;
Workbook workbook = writeWorkbookHolder.getWorkbook();
Sheet hiddenSheet = workbook.createSheet(sheetName);
for (int i = 0, length = v.length; i < length; i++) {
// 开始的行数i,列数k
hiddenSheet.createRow(i).createCell(k).setCellValue(v[i]);
}
Name category1Name = workbook.createName();
category1Name.setNameName(sheetName);
String excelLine = EasyExcelUtils.getColNum(k);
// =hidden!$H:$1:$H$50 sheet为hidden的 H1列开始H50行数据获取下拉数组
String refers = "=" + sheetName + "!$" + excelLine + "$1:$" + excelLine + "$" + (v.length + 1);
// 将刚才设置的sheet引用到你的下拉列表中
DataValidationConstraint constraint = helper.createFormulaListConstraint(refers);
DataValidation dataValidation = helper.createValidation(constraint, rangeList);
writeSheetHolder.getSheet().addValidationData(dataValidation);
// 设置存储下拉列值得sheet为隐藏
int hiddenIndex = workbook.getSheetIndex(sheetName);
if (!workbook.isSheetHidden(hiddenIndex)) {
workbook.setSheetHidden(hiddenIndex, true);
}
}
// v 就是下拉列表的具体数据,下拉列表约束数据
DataValidationConstraint constraint = helper.createExplicitListConstraint(v);
// 设置下拉约束
DataValidation validation = helper.createValidation(constraint, rangeList);
// 阻止输入非下拉选项的值
validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
validation.setShowErrorBox(true);
validation.setSuppressDropDownArrow(true);
validation.createErrorBox("提示", "此值与单元格定义格式不一致");
sheet.addValidationData(validation);
});
}

}

3、导出的实体类

  • 固定下拉使用:source,动态下拉使用:sourceClass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Data
// 内容行高度
@ContentRowHeight(20)
// 头部行高度
@HeadRowHeight(25)
// 列宽,可在类或属性中使用
@ColumnWidth(25)
public class UserTemplate {

@ExcelProperty("用户名称")
private String username;

@ExcelProperty("年龄")
private Integer age;

/**
* 固定下拉
*/
@ExcelProperty("部门")
@DropDownFields(source = {"财务部","人事部","研发部","商务部"})
private String department;

/**
* 动态下拉
*/
@ExcelProperty("职业")
@DropDownFields(sourceClass = OccupationDropDownService.class,type = DropDownType.OCCUPATION)
private String occupation;

@ColumnWidth(50)
@ExcelProperty(value = "注册时间",converter = LocalDateTimeConverter.class)
private LocalDateTime createTime=LocalDateTime.now();
}

4、动态下拉实现类

  • 只需要实现getSource()方法,返回下拉的集合即可
  • 这边模拟了下数据库查询,因为这里不能直接使用注解自动注入
  • 所有通过上下文工具类获取示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 职业的下拉 实现类
*/
public class OccupationDropDownService implements IDropDownService{

@Override
public String[] getSource(String typeValue) {
// 没法用 @Autowired 注入,所以用ApplicationContext
IDictValueService dictValueService = ApplicationContextUtils.getBean(IDictValueService.class);
List<DictValue> list = dictValueService.list(new LambdaQueryWrapper<DictValue>().eq(DictValue::getType, typeValue));
if(ObjectUtils.isEmpty(list)){
return null;
}
Set<String> collect = list.stream().map(DictValue::getValue).collect(Collectors.toSet());
return collect.toArray(new String[collect.size()]);
}
}

5、导出模板

  • 这边演示导出到浏览器
  • 因为很多内容可以复用,随意封装了下工具类:EasyExcelUtils

EasyExcelUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* easyExcel 工具类
* @author rstyro
*/
@Slf4j
public class EasyExcelUtils {

/**
* 浏览器导出excel文件
* 官网文档地址:https://www.yuque.com/easyexcel/doc/easyexcel
* @param data 数据
* @param templateClass 模板对象class
* @param pageSize 每页多少条
* @param fileName 文件名称
* @param response 输出流
* @throws Exception err
*/
public static void exportBrowser(List data, Class templateClass, Integer pageSize, String fileName, HttpServletResponse response) throws Exception {
pageSize = Optional.ofNullable(pageSize).orElse(50000);
fileName = fileName + System.currentTimeMillis();
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

// 获取改类声明的所有字段
Field[] fields = templateClass.getDeclaredFields();
// 响应字段对应的下拉集合
Map<Integer, String[]> map = processDropDown(fields);
// 解析级联下拉字段
Map<Integer, ChainDropDown> integerChainDropDownMap = processChainDropDown(fields);
ExcelWriter excelWriter = null;
OutputStream out = null;
try {
out = response.getOutputStream();
excelWriter = EasyExcel.write(out, templateClass).registerWriteHandler(new DropDownWriteHandler(map))
.registerWriteHandler(new ChainDropDownWriteHandler(integerChainDropDownMap)).build();
// 分页写入
pageWrite(excelWriter,data,pageSize);
} catch (Throwable e) {
response.setHeader("Content-Disposition", "attachment;filename=下载失败");
e.printStackTrace();
log.error("文档下载失败:" + e.getMessage());
} finally {
data.clear();
if (excelWriter != null) {
excelWriter.finish();
}
assert out != null;
out.flush();
out.close();
}
}

/**
* 分页写入
* @param writer ExcelWriter
* @param data 数据
* @param pageSize 分页大小
*/
public static void pageWrite(ExcelWriter writer,List<Object> data,Integer pageSize){
List<List<Object>> lt = Lists.partition(data, pageSize);
for (int i = 0; i < lt.size(); i++) {
int j = i + 1;
WriteSheet writeSheet = EasyExcel.writerSheet(i, "第" + j + "页").build();
writer.write(lt.get(i), writeSheet);
}
}


/**
* 读取excel 并解析
* @param file 文件
* @param clazz 解析成哪个dto
* @param <T> t
* @return list
* @throws IOException error
*/
public static <T> List<T> read(MultipartFile file,Class<T> clazz) throws IOException {
BaseExcelDataListener<T> baseExcelDataListener = new BaseExcelDataListener();
EasyExcel.read(file.getInputStream(), clazz, baseExcelDataListener).sheet().doRead();
return baseExcelDataListener.getResult();
}

/**
* 处理有下拉框注解的属性
* @param fields 字段属性集合
* @return map
*/
public static Map<Integer, String[]> processDropDown(Field[] fields){
// 响应字段对应的下拉集合
Map<Integer, String[]> map = new HashMap<>();
Field field = null;
// 循环判断哪些字段有下拉数据集,并获取
for (int i = 0; i < fields.length; i++) {
field = fields[i];
// 解析注解信息
DropDownFields dropDownField = field.getAnnotation(DropDownFields.class);
if (null != dropDownField) {
String[] sources = ResolveAnnotation.resolve(dropDownField);
if (null != sources && sources.length > 0) {
map.put(i, sources);
}
}
}
return map;
}

public static Map<Integer, ChainDropDown> processChainDropDown(Field[] fields){
// 响应字段对应的下拉集合
Map<Integer, ChainDropDown> map = new HashMap<>();
Field field = null;
int rowIndex=0;
// 循环判断哪些字段有下拉数据集,并获取
for (int i = 0; i < fields.length; i++) {
field = fields[i];
// 解析注解信息
ChainDropDownFields chainDropDownFields = field.getAnnotation(ChainDropDownFields.class);
if (null != chainDropDownFields) {
// List<ChainDropDown> sources = ResolveAnnotation.resolve(chainDropDownFields);
ChainDropDown resolve = ResolveAnnotation.resolve(chainDropDownFields);
long collect = resolve.getDataMap().keySet().size();
System.out.println("collect="+collect);
if(resolve.isRootFlag()){
resolve.setRowIndex(rowIndex);
rowIndex+=1;
}else {
resolve.setRowIndex(rowIndex);
rowIndex+=collect;
}
if (!ObjectUtils.isEmpty(resolve)) {
map.put(i, resolve);
}
}
}
return map;
}

/**
* 获取Excel列的号码A-Z - AA-ZZ - AAA-ZZZ 。。。。
* @param num
* @return
*/
public static String getColNum(int num) {
int MAX_NUM = 26;
char initChar = 'A';
if(num == 0){
return initChar+"";
}else if(num > 0 && num < MAX_NUM){
int result = num % MAX_NUM;
return (char) (initChar + result) + "";
}else if(num >= MAX_NUM){
int result = num / MAX_NUM;
int mod = num % MAX_NUM;
String starNum = getColNum(result-1);
String endNum = getColNum(mod);
return starNum+endNum;
}
return "";
}
}
  • 导出浏览器,主要看:exportBrowser()方法即可
  • SpringBoot请求接口如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Controller
@RequestMapping("/dropDown")
public class DropDownController {

@Autowired
private IUserService userService;

/**
* 下载下拉模板
* @param response
* @throws Exception
*/
@GetMapping("/downloadTemplate")
public void downloadTemplate(HttpServletResponse response) throws Exception {
List<UserTemplate> export = new ArrayList<>(Arrays.asList(new UserTemplate()));
EasyExcelUtils.exportBrowser(export, UserTemplate.class, export.size(), "模板", response);
}

}

六、级联下拉框

  • 能导出下拉框,是不是也能做成级联下拉框呢
  • 答案是肯定的。但是就是比下拉框麻烦一点点
  • 因为不只要设置数据有效性,还有设置名称管理器,和级联有效性等。
  • 我们也定义一个自定义注解:ChainDropDownFields

1、自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* excel的级联下拉框注解
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ChainDropDownFields {

/**
* 动态下拉内容,可查数据库返回等,其他操作
* @return class[]
*/
Class[] sourceClass() default {};

/**
* 是不是第一级下拉
* @return
*/
boolean isRoot() default false;

/**
* 获取下拉的参数,可以有1个到多个
* @return string
*/
String[] params() default {};

/**
* 级联下拉类型
* @return type
*/
ChainDropDownType type() default ChainDropDownType.NONE;

}
  • 上面字段都有注释解析

2、定义实体类

  • 定义一个实体类,下拉和级联下拉,复合使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Data
// 内容行高度
@ContentRowHeight(20)
// 头部行高度
@HeadRowHeight(25)
// 列宽,可在类或属性中使用
@ColumnWidth(25)
@Data
// 内容行高度
@ContentRowHeight(20)
// 头部行高度
@HeadRowHeight(25)
// 列宽,可在类或属性中使用
@ColumnWidth(25)
public class ChainTestTemplate {

@ExcelProperty("用户名称")
private String name;

@ExcelProperty("年龄")
private Integer age;


@ExcelProperty("国家")
@ChainDropDownFields(isRoot = true,sourceClass = TestChainDropDownService.class,type = ChainDropDownType.TEST)
private String country;

@ExcelProperty("省份")
@ChainDropDownFields(sourceClass = TestChainDropDownService.class,type = ChainDropDownType.TEST,params = {"2"})
private String province;

@ExcelProperty("城市")
@ChainDropDownFields(sourceClass = TestChainDropDownService.class,type = ChainDropDownType.TEST,params = {"3"})
private String city;

@ExcelProperty("区域")
@ChainDropDownFields(sourceClass = TestChainDropDownService.class,type = ChainDropDownType.TEST,params = {"4"})
private String zone;
}
  • 上面可以看到有:国家 省 市 区 4个级联下拉,可以一直级联…

3、定义注解实现类

  • 内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 区域级联下拉 实现类
*/
public class TestChainDropDownService implements IChainDropDownService{

/**
* 第一层,key=root,value=可选数组
*/
@Override
public List<String> getRoot(String... params){
return Arrays.asList(new String[]{"中国", "美国"});
}

/**
* 获取子类的Map
*/
@Override
public Map<String,List<String>> getParentBindSubMap(String... params){
int level = Integer.parseInt(params[0]);
// key 是父级,value 是父级的子类
Map<String,List<String>> dataMap = new HashMap<>();
if(level==2){
dataMap.put("中国",Arrays.asList(new String[]{"北京2", "广东2"}));
dataMap.put("美国",Arrays.asList(new String[]{"阿拉斯加州", "阿拉巴马州"}));
}else if(level == 3){
dataMap.put("北京2",Arrays.asList(new String[]{"北京市2"}));
dataMap.put("广东2",Arrays.asList(new String[]{"广州2","深圳2"}));
dataMap.put("阿拉斯加州",Arrays.asList(new String[]{"阿拉斯加","雅库塔特"}));
dataMap.put("阿拉巴马州",Arrays.asList(new String[]{"马伦戈县"}));
}else if(level == 4){
dataMap.put("北京市2",Arrays.asList(new String[]{"朝阳区2","密云区2"}));
dataMap.put("广州2",Arrays.asList(new String[]{"天河区2","白云区2"}));
dataMap.put("深圳2",Arrays.asList(new String[]{"福田区2","南山区2"}));
dataMap.put("阿拉斯加",Arrays.asList(new String[]{"瞎编区","编不下去了"}));
dataMap.put("雅库塔特",Arrays.asList(new String[]{"瞎编区","编不下去了"}));
dataMap.put("马伦戈县",Arrays.asList(new String[]{"马勒戈壁"}));
}
return dataMap;
}
}
  • 这里的getRoot() 返回的是第一层下拉,因为没有父级,所以直接返回一个集合下拉列表即可。
  • getParentBindSubMap()这里返回的是第二级及之后的下拉列表,是一个Map,key是上一级的所有值,value是key的子集。
  • 我也不知道还有没有好一点的方法来搞这个级联下拉。

4、级联下拉Handler

  • 这个才是重点,有些重要的地方都写了注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
* excel 级联下拉框 handler
*/
public class ChainDropDownWriteHandler implements SheetWriteHandler {

private final Map<Integer, ChainDropDown> map;

public ChainDropDownWriteHandler(Map<Integer, ChainDropDown> map) {
this.map = map;
}

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

}

@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
// 这里可以对cell进行任何操作
Sheet sheet = writeSheetHolder.getSheet();
DataValidationHelper helper = sheet.getDataValidationHelper();
Workbook workbook = writeWorkbookHolder.getWorkbook();
for(Map.Entry<Integer, ChainDropDown> e:map.entrySet()){
// k 为存在下拉数据集的单元格下表 v为下拉数据集
Integer k = e.getKey();
ChainDropDown v = e.getValue();
CellRangeAddressList rangeList = new CellRangeAddressList(1, 65536, k, k);
Sheet hideSheet = getSheet(workbook, v.getTypeName());
if(v.isRootFlag()){
Row firstRow = hideSheet.createRow(v.getRowIndex());
List<String> values = v.getDataMap().get(ChainDropDown.ROOT_KEY);
for(int i = 0; i < values.size(); i ++){
Cell rowCell = firstRow.createCell(i);
rowCell.setCellValue(values.get(i));
}

// v 就是下拉列表的具体数据,下拉列表约束数据
DataValidationConstraint constraint = helper.createExplicitListConstraint(values.toArray(new String[values.size()]));
// 设置下拉约束
DataValidation validation = helper.createValidation(constraint, rangeList);
// 阻止输入非下拉选项的值
validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
validation.setShowErrorBox(true);
validation.setSuppressDropDownArrow(true);
validation.createErrorBox("提示", "此值与单元格定义格式不一致");
sheet.addValidationData(validation);

}else {
Integer rowIndex = v.getRowIndex();
Map<String, List<String>> dataMap = v.getDataMap();

for(Map.Entry<String, List<String>> entry:dataMap.entrySet()){
String parentValue = entry.getKey();
List<String> childValues = entry.getValue();
Row row = hideSheet.createRow(rowIndex++);
row.createCell(0).setCellValue(parentValue);
for(int j = 0; j < childValues.size(); j ++){
Cell cell = row.createCell(j + 1);
cell.setCellValue(childValues.get(j));
}
// 添加名称管理器
String range = getRange(1, rowIndex, childValues.size());
Name name = workbook.createName();
//key不可重复
name.setNameName(parentValue);
String formula = v.getTypeName()+"!" + range;
name.setRefersToFormula(formula);
}

}
// 从第二行开始,第一行是标题
int beginRow = 2;
// 设置级联有效性
String listFormula = "INDIRECT($" + EasyExcelUtils.getColNum(k-1) + beginRow + ")";
DataValidationConstraint formulaListConstraint = helper.createFormulaListConstraint(listFormula);
// 设置下拉约束
DataValidation validation = helper.createValidation(formulaListConstraint, rangeList);
validation.setEmptyCellAllowed(false);
validation.setSuppressDropDownArrow(true);
validation.setShowErrorBox(true);
// 设置输入信息提示信息
validation.createPromptBox("下拉选择提示", "请使用下拉方式选择合适的值!");
sheet.addValidationData(validation);
}
}

public Sheet getSheet(Workbook workbook,String sheetName){
Sheet sheet = workbook.getSheet(sheetName);
if(!ObjectUtils.isEmpty(sheet)) {
return sheet;
}
return workbook.createSheet(sheetName);
}

/**
* 计算formula
* @param offset 偏移量,如果给0,表示从A列开始,1,就是从B列
* @param rowNum 第几行
* @param colCount 一共多少列
* @return 如果给入参 1,1,10. 表示从B1-K1。最终返回 $B$1:$K$1
*
*/
public static String getRange(int offset, int rowNum, int colCount) {
String start = EasyExcelUtils.getColNum(offset);
String end = EasyExcelUtils.getColNum(colCount);
String format= "$%s$%s:$%s$%s";
return String.format(format, start,rowNum,end,rowNum);
}

}
  • 这里其实复杂的就是设置名称管理器和它的级联关联有效性,我不知道还没有更好的方法。

5、导出级联下拉模板

  • 也是使用springboot
1
2
3
4
5
6
7
8
9
10
11
@Controller
@RequestMapping("/chainDropDown")
public class ChainDropDownController {

@GetMapping("/downloadTemplate")
public void downloadTemplate2(HttpServletResponse response) throws Exception {
List<ChainTestTemplate> export = new ArrayList<>(Arrays.asList(new ChainTestTemplate()));
EasyExcelUtils.exportBrowser(export, ChainTestTemplate.class, export.size(), "测试级联模板", response);
}

}

6、本文完整代码

您的打赏,是我创作的动力!不给钱?那我只能靠想象力充饥了。