- 写在前面
- 正文
- 1. springboot后端引入easyexcel及使用
- 1.1 引入依赖
- 1.2 接口serviceImpl方法
- 1.3 提供一个对list集合去重的方法(根据相同key,去除重复,合并value值)
- 1.4 BizMergeStrategy合并策略类
- 1.5 自定义ExcelUtil工具类
- 2. vue前端调用后台下载excel接口实现点击按钮完成下载
- 2.1 上图对应vue代码
- 2.2 export_excel() 方法
- 3. vue多种方式实现调用后台接口下载excel (本小节借鉴他人总结)
- 4. 总结碰到的一些问题,避免小伙伴踩坑
仅作记录,如能帮助尚在迷途的小伙伴,不胜荣幸。
这两三天好好搞了一下这个利用easyexcel导出并下载excel表格的事情。也是感受颇多,本着前人栽树后人乘凉的人道主义精神,赶紧码下这一篇文章,顺带加深自己的记忆。
1.2 接口serviceImpl方法com.alibaba easyexcel
xxxServiceImpl.java
public void exportExcelData(String wo_id, HttpServletResponse response) throws BusinessException { // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman try { // 模板文件 //此处getResourceAsStream 用于获取服务器打包后的Excel模板文件流; //如果采用getPath方法获取文件地址本地ieda环境可以获取到,上传到服务器后会失效。采用流可以都生效,具体原因暂未仔细查看。有兴趣的童鞋可以自己去尝试! //InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("templates/excelTemplate.xls"); // 获取表头数据 if (wo_id == null || wo_id.isEmpty()) { throw new BusinessException(BusinessCodeEnum.PARAMETER_ERROR, "工单id获取为空!"); } // 下面2行这是我自己的业务,不用管 CkdPoInfo ckdPoInfo = ckdPoInfoMapper.selectByPrimaryKey(wo_id); String work_order = ckdPoInfo.getWork_order(); ListexcelDataList = getData(wo_id); // 从数据库获取excel表体数据 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"); // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 String fileName = URLEncoder.encode(work_order+"报表导出测试", "UTF-8").replaceAll("\+", "%20"); // response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); response.addHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); response.addHeader("Access-Control-Expose-Headers", "Content-disposition"); // 自定义的合并策略,重要 Map > srategyMap = ExcelUtil.addMergeStrategy(excelDataList); // 这里需要设置不关闭流; EasyExcel.write(response.getOutputStream(), ExportExcelData.class).autoCloseStream(Boolean.FALSE) // 注册合并策略 .registerWriteHandler(new BizMergeStrategy(srategyMap)) // 这里调用自定义的合并策略 .registerWriteHandler(ExcelUtil.CellStyleStrategy()) // 调用自定义的工具类 .sheet("xxx信息表") .doWrite(excelDataList); // 写入获取的list数据集合 } catch (Exception e) { // 重置response response.reset(); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); // Map map = MapUtils.newHashMap(); // map.put("status", "failure"); // map.put("message", "文件下载失败" + e.getMessage()); // try { // response.getWriter().println(JSON.toJSonString(map)); // } catch (IOException ex) { // ex.printStackTrace(); // } e.printStackTrace(); throw new BusinessException(BusinessCodeEnum.PARAMETER_ERROR, "导出excel失败!"); } }
- 以上try_catch语句调用easyexcel写文件流我写在了serviceImpl的类方法里,也可以把这段直接写在controller里。个人所有业务层代码都放在service里,controller层只负责调用service层。
- wo_id是我需要传的参数,这里根据个人实际情况,可传其他也可不传。
- excelDataList就是我封装的获取我数据库数据的list集合,getData()是具体实现方法;
- getData()不同场景实现都不同,这里就不再贴出有关它的实现
- 自定义合并策略、自定义ExcelUtil工具类下面会放,不急。
public ListgetNewList(List oldList) { List newList = new ArrayList<>(); HashMap tempMap = new HashMap (); // 去掉重复key 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(); // 作为map的key值 // 给属性赋值,以下3个整体构成的类对象作为key newCkdMtrPackage.setOdm_pn(odm_pn); newCkdMtrPackage.setExporter_pn(exporter_pn); newCkdMtrPackage.setimporter_pn(importer_pn); // if (tempMap.containsKey(newCkdMtrPackage)) { // 合并相同料号的value ckdMaterialPackage.setQuantity(tempMap.get(newCkdMtrPackage).getQuantity() + ckdMaterialPackage.getQuantity()); // hashmap不允许key重复,当有key重复时,前面key对应的value值会被覆盖 tempMap.put(newCkdMtrPackage, ckdMaterialPackage); } else { tempMap.put(newCkdMtrPackage, ckdMaterialPackage); } } for (Map.Entry entry : tempMap.entrySet()) { newList.add(entry.getValue()); } return newList; }
- 该方法就是传入一个原始的数据list,然后返回一个去重合并后的list
- List
oldList中 CkdMaterialPackage是我自己的数据库表实体类,换成你自己的 - NewCkdMtrPackage这个是专门定义的实体类,可以理解为里面的属性整体充当key;举个例子,一个list集合里包含 name,sex,height,score几个元素,我想要合并重复的name,sex,height,且将score求和。就是说我合并的列不止一列。那么把这些重复的封装成一个类,整体作为key来执行。
public class BizMergeStrategy extends AbstractMergeStrategy { private Map> strategyMap; // RowRangeDto 行起始结束范围类 private Sheet sheet; public BizMergeStrategy(Map > 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 > entry : strategyMap.entrySet()) { Integer columnIndex = Integer.valueOf(entry.getKey()); entry.getValue().forEach(rowRange -> { //添加一个合并请求 sheet.addMergedRegionUnsafe(new CellRangeAddress(rowRange.getStart(), rowRange.getEnd(), columnIndex, columnIndex)); }); } } } }
- RowRangeDto
@Data @AllArgsConstructor @NoArgsConstructor @ToString public class RowRangeDto { private int start; private int end; }1.5 自定义ExcelUtil工具类
public class ExcelUtil{ public static Map> addMergeStrategy(List excelDataList) { Map > 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())) { //如果栈板号一样,则可合并栈板号、栈板毛重、栈板尺寸 3列 fillStrategyMap(strategyMap, "0", i); fillStrategyMap(strategyMap, "1", i); fillStrategyMap(strategyMap, "2", i); //如果栈板号一样,并且卡通箱号一样,则可合并卡通箱号、单箱毛重、单箱尺寸 3列 if (currExcelData.getCarton_number().equals(preExcelData.getCarton_number())) { fillStrategyMap(strategyMap, "3", i); fillStrategyMap(strategyMap, "4", i); fillStrategyMap(strategyMap, "5", i); // //如果栈板号、卡通箱号一样,并且物料料号也一样,则可合并物料料号一列 物料信息查询时已合并 // if (currExcelData.getCoopOrg().equals(preExcelDto.getCoopOrg())) { // fillStrategyMap(strategyMap, "2", i); // } } } } preExcelData = currExcelData; } return strategyMap; } private static void fillStrategyMap(Map > strategyMap, String key, int index) { List rowRangeDtoList = strategyMap.get(key) == null ? new ArrayList<>() : strategyMap.get(key); boolean flag = false; for (RowRangeDto dto : rowRangeDtoList) { //分段list中是否有end索引是上一行索引的,如果有,则索引+1 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代码2.2 export_excel() 方法导出报关单
export_excel(wo_id){ //scope.row.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']); // 从response的headers中获取filename, 后端response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx") 设置的文件名; //以=分割取数组[1]元素为文件名 let filename = window.decodeURI(res.headers['content-disposition'].split('=')[1]) let url = window.URL.createObjectURL(blob); // 创建下载链接 let alink = document.createElement("a"); // 赋值给a标签的href属性 alink.style.display = "none"; alink.href = url; alink.setAttribute("download", filename); document.body.appendChild(alink); // 将a标签挂载上去 alink.click(); // a标签click事件 document.body.removeChild(alink); // 移除a标签 window.URL.revokeObjectURL(url); // 销毁下载链接 return this.$message.success("导出报关单成功"); }).catch(function (error) { console.log(error); }) }3. vue多种方式实现调用后台接口下载excel (本小节借鉴他人总结)
方式一:直接通过a标签
优点:简单方便。
缺点:这种下载方式只支持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
结果d出下载好的文件并保存框,证明后端接口可用;测试时可以前后端分开编写和测试。
② 我定义下载逻辑用的是easyexcel,但是上传文件逻辑前人用的是 excelkit,然后poi版本是3.1,但是我easyexcel内包含4.0版本的poi,所以导致上传文件逻辑一些方法报错。
解决办法:pom.xml依赖里手动添加发生冲突的jar包更高版本即可。
③ 后端设置好header里定义好了下载的excel表格名称,但是vue前端并没有获取到:
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 String fileName = URLEncoder.encode(work_order+"报表导出测试", "UTF-8").replaceAll("\+", "%20"); // response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); response.addHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); //response.addHeader("Access-Control-Expose-Headers", "Content-disposition"); 此时还没有哦
console.log(res.headers['content-disposition']); // 这里控制台输出是undefined
查资料得知必须后台在设置请求头时将 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表格导出功能
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)