写在前面
仅作记录,如能帮助尚在迷途的小伙伴,不胜荣幸。 这两三天好好搞了一下这个利用easyexcel导出并下载excel表格的事情。也是感受颇多,本着前人栽树后人乘凉的人道主义精神,赶紧码下这一篇文章,顺带加深自己的记忆。
正文
1. springboot后端引入easyexcel及使用
1.1 引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
1.2 接口serviceImpl方法
xxxServiceImpl.java
public void exportExcelData(String wo_id, HttpServletResponse response) throws BusinessException {
try {
if (wo_id == null || wo_id.isEmpty()) {
throw new BusinessException(BusinessCodeEnum.PARAMETER_ERROR, "工单id获取为空!");
}
CkdPoInfo ckdPoInfo = ckdPoInfoMapper.selectByPrimaryKey(wo_id);
String work_order = ckdPoInfo.getWork_order();
List<ExportExcelData> excelDataList = getData(wo_id);
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode(work_order+"报表导出测试", "UTF-8").replaceAll("\\+", "%20");
response.addHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
response.addHeader("Access-Control-Expose-Headers", "Content-disposition");
Map<String, List<RowRangeDto>> srategyMap = ExcelUtil.addMergeStrategy(excelDataList);
EasyExcel.write(response.getOutputStream(), ExportExcelData.class).autoCloseStream(Boolean.FALSE)
.registerWriteHandler(new BizMergeStrategy(srategyMap))
.registerWriteHandler(ExcelUtil.CellStyleStrategy())
.sheet("xxx信息表")
.doWrite(excelDataList);
} catch (Exception e) {
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
e.printStackTrace();
throw new BusinessException(BusinessCodeEnum.PARAMETER_ERROR, "导出excel失败!");
}
}
- 以上try_catch语句调用easyexcel写文件流我写在了serviceImpl的类方法里,也可以把这段直接写在controller里。个人所有业务层代码都放在service里,controller层只负责调用service层。
- wo_id是我需要传的参数,这里根据个人实际情况,可传其他也可不传。
- excelDataList就是我封装的获取我数据库数据的list集合,getData()是具体实现方法;
- getData()不同场景实现都不同,这里就不再贴出有关它的实现
- 自定义合并策略、自定义ExcelUtil工具类下面会放,不急。
1.3 提供一个对list集合去重的方法(根据相同key,去除重复,合并value值)
public List<CkdMaterialPackage> getNewList(List<CkdMaterialPackage> oldList) {
List<CkdMaterialPackage> newList = new ArrayList<>();
HashMap<NewCkdMtrPackage, CkdMaterialPackage> tempMap = new HashMap<NewCkdMtrPackage, CkdMaterialPackage>();
for (CkdMaterialPackage ckdMaterialPackage : oldList) {
String odm_pn = ckdMaterialPackage.getOdm_pn();
String exporter_pn = ckdMaterialPackage.getExporter_pn();
String importer_pn = ckdMaterialPackage.getImporter_pn();
NewCkdMtrPackage newCkdMtrPackage = new NewCkdMtrPackage();
newCkdMtrPackage.setOdm_pn(odm_pn);
newCkdMtrPackage.setExporter_pn(exporter_pn);
newCkdMtrPackage.setImporter_pn(importer_pn);
if (tempMap.containsKey(newCkdMtrPackage)) {
ckdMaterialPackage.setQuantity(tempMap.get(newCkdMtrPackage).getQuantity() + ckdMaterialPackage.getQuantity());
tempMap.put(newCkdMtrPackage, ckdMaterialPackage);
} else {
tempMap.put(newCkdMtrPackage, ckdMaterialPackage);
}
}
for (Map.Entry<NewCkdMtrPackage, CkdMaterialPackage> entry : tempMap.entrySet()) {
newList.add(entry.getValue());
}
return newList;
}
- 该方法就是传入一个原始的数据list,然后返回一个去重合并后的list
- List<CkdMaterialPackage> oldList中 CkdMaterialPackage是我自己的数据库表实体类,换成你自己的
- NewCkdMtrPackage这个是专门定义的实体类,可以理解为里面的属性整体充当key;举个例子,一个list集合里包含 name,sex,height,score几个元素,我想要合并重复的name,sex,height,且将score求和。就是说我合并的列不止一列。那么把这些重复的封装成一个类,整体作为key来执行。
1.4 BizMergeStrategy合并策略类
public class BizMergeStrategy extends AbstractMergeStrategy {
private Map<String, List<RowRangeDto>> strategyMap;
private Sheet sheet;
public BizMergeStrategy(Map<String, List<RowRangeDto>> strategyMap) {
this.strategyMap = strategyMap;
}
@Override
protected void merge(org.apache.poi.ss.usermodel.Sheet sheet, Cell cell, Head head, Integer integer) {
this.sheet = sheet;
if (cell.getRowIndex() == 1 && cell.getColumnIndex() == 0) {
for (Map.Entry<String, List<RowRangeDto>> entry : strategyMap.entrySet()) {
Integer columnIndex = Integer.valueOf(entry.getKey());
entry.getValue().forEach(rowRange -> {
sheet.addMergedRegionUnsafe(new CellRangeAddress(rowRange.getStart(),
rowRange.getEnd(), columnIndex, columnIndex));
});
}
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class RowRangeDto {
private int start;
private int end;
}
1.5 自定义ExcelUtil工具类
public class ExcelUtil{
public static Map<String, List<RowRangeDto>> addMergeStrategy(List<ExportExcelData> excelDataList) {
Map<String, List<RowRangeDto>> strategyMap = new HashMap<>();
ExportExcelData preExcelData = null;
for (int i = 0; i < excelDataList.size(); i++) {
ExportExcelData currExcelData = excelDataList.get(i);
if (preExcelData != null) {
if (currExcelData.getPallet_number().equals(preExcelData.getPallet_number())) {
fillStrategyMap(strategyMap, "0", i);
fillStrategyMap(strategyMap, "1", i);
fillStrategyMap(strategyMap, "2", i);
if (currExcelData.getCarton_number().equals(preExcelData.getCarton_number())) {
fillStrategyMap(strategyMap, "3", i);
fillStrategyMap(strategyMap, "4", i);
fillStrategyMap(strategyMap, "5", i);
}
}
}
preExcelData = currExcelData;
}
return strategyMap;
}
private static void fillStrategyMap(Map<String, List<RowRangeDto>> strategyMap, String key, int index) {
List<RowRangeDto> rowRangeDtoList = strategyMap.get(key) == null ? new ArrayList<>() : strategyMap.get(key);
boolean flag = false;
for (RowRangeDto dto : rowRangeDtoList) {
if (dto.getEnd() == index) {
dto.setEnd(index + 1);
flag = true;
}
}
if (!flag) {
rowRangeDtoList.add(new RowRangeDto(index, index + 1));
}
strategyMap.put(key, rowRangeDtoList);
}
public static HorizontalCellStyleStrategy CellStyleStrategy(){
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short)13);
headWriteFont.setBold(true);
headWriteCellStyle.setWriteFont(headWriteFont);
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
return horizontalCellStyleStrategy;
}
}
Controller方法:
@GetMapping("/downloadExcel")
public void exportExcelData(
@RequestParam(value = "wo_id") String wo_id,
HttpServletResponse response
) throws BusinessException {
ckdPoInfoService.exportExcelData(wo_id, response);
}
以上是全部的后端接口及方法。
2. vue前端调用后台下载excel接口实现点击按钮完成下载
2.1 上图对应vue代码
<span v-if="(scope.row.status&63)===60">
<el-popconfirm
style="margin-left:20px;"
@confirm="export_excel(scope.row.wo_id)"
title="导出报关单?"
>
<el-button type="text" slot="reference" size="small" icon="el-icon-document-copy">导出报关单</el-button>
</el-popconfirm>
</span>
2.2 export_excel() 方法
export_excel(wo_id){
console.log("wo_id = "+wo_id);
const url = this.BASE_API_URL + 'springbootApi/downloadExcel?wo_id='+wo_id;
axios.get(url,
{
responseType: 'blob' <!--响应类型必须设为二进制文件流-->
})
.then((res) => {
if (!res) return
console.log("res data = "+res.data);
let blob = new Blob([res.data], {type: 'application/vnd.ms-excel;charset=utf-8'})
console.log(res.headers['content-disposition']);
let filename = window.decodeURI(res.headers['content-disposition'].split('=')[1])
let url = window.URL.createObjectURL(blob);
let aLink = document.createElement("a");
aLink.style.display = "none";
aLink.href = url;
aLink.setAttribute("download", filename);
document.body.appendChild(aLink);
aLink.click();
document.body.removeChild(aLink);
window.URL.revokeObjectURL(url);
return this.$message.success("导出报关单成功");
}).catch(function (error) {
console.log(error);
})
}
3. vue多种方式实现调用后台接口下载excel (本小节借鉴他人总结)
方式一:直接通过a标签
<a href="/images/logo.jpg" download="logo" />
优点:简单方便。 缺点:这种下载方式只支持Firefox和Chrome不支持IE和Safari,兼容性不够好。
方式二: 通过window.location
window.location = 'http://127.0.0.1:8080/api/download?name=xxx&type=xxx'
其实就是类似我直接在浏览器url地址栏输入接口地址,回车下载。 优点:简单方便。 缺点:只能进行get请求,当有token校验的时候不方便。
方式三:axios请求后台接口 (目录2就是基于此)
4. 总结碰到的一些问题,避免小伙伴踩坑
① 首先后端接口写好后,我直接浏览器输入api接口去调用测试:
http://localhost:8989/xxx/downloadExcel?wo_id=33b948460598420eb533d62930c9
结果弹出下载好的文件并保存框,证明后端接口可用;测试时可以前后端分开编写和测试。
② 我定义下载逻辑用的是easyexcel,但是上传文件逻辑前人用的是 excelkit,然后poi版本是3.1,但是我easyexcel内包含4.0版本的poi,所以导致上传文件逻辑一些方法报错。 解决办法:pom.xml依赖里手动添加发生冲突的jar包更高版本即可。 ③ 后端设置好header里定义好了下载的excel表格名称,但是vue前端并没有获取到:
String fileName = URLEncoder.encode(work_order+"报表导出测试", "UTF-8").replaceAll("\\+", "%20");
response.addHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
console.log(res.headers['content-disposition']);
查资料得知必须后台在设置请求头时将 content-disposition 加入到 Access-Control-Expose-Headers里,前端才能获取到。 即后台加一段代码:
response.addHeader("Access-Control-Expose-Headers", "Content-disposition");
然后vue就能获取到文件名了。
参考文章: 1. Vue项目利用axios请求接口下载excel(附前后端代码) 2. vue中axios实现二进制流文件下载 3. 老哥写的很棒 4. 这位大佬设置模板填充的,可以看看,还有打压缩包下载,留个传送门 5. 这个帮我最大的忙 6. github源码easyexcel地址 7. 使用easy excel导出复杂表头的excel 8. 感觉有用的就放在这里了
分割线====
- 这里乱入一个easypoi的官方文档
- 使用EasyPoi完成复杂一对多excel表格导出功能
|