【Vue】HutoolExcel导出
最近写Excel导出功能,发现需求有点复杂,这里整理一下思路和解决方案
一、需求背景:
老系统改造,功能和代码是现成的,预览了一下内容:
第一个是有特定样式,比如首行标题,以及红色的列名称
第二个,导出多个Sheet页
第三个,最后多一行放置导出时间
二、技术选型 :
我非常喜欢用Hutool的工具处理,然后看了下最新的文档案例,推测是可以满足上述需求的
1 | http: //hutool.cn/docs/#/poi/Excel生成-ExcelWriter |
重点是关于如何多Sheet页导出的支持,Hutool这里没有细说,看看有没有现成的案例
经过简单测试发现是可行的
1 | https: //blog.csdn.net/ZLK1142/article/details/106531246/ |
三、落地实现:
1、前后交互问题:
本来是打算使用前端导出的,后端接口提供数据即可,但是前端导出怎么设置具体样式并不熟悉,加上自定义样式需求多,就放弃这个方案了
使用后端导出的基本办法是使用get请求方式的接口,然后前端使用window.open()打开新链接,这样跳转下载一个文件
- 这样好处是不用编写交互处理,用户等待新页面弹出下载提示即可
- 但是请求参数,令牌信息都要通过url携带,不安全的,也会暴露信息
再加上现有系统无法从url上获取参数,所以改用axios请求实现
axios请求实现的问题在于响应的处理,要在前端声明特定的blob类型、重新封装文件内容、和下载事件处理
2、Hutool基于业务需求的封装:
之前写的导出就是导出数据就行,这里参考多sheet自己实现的一个逻辑
Hutool支持了用Map配置别名映射,为了更方便实现更符合业务逻辑方式的开发,可以自定义映射注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package cn.anmte.wd.excel.annotation; import java.lang.annotation.*; /** * @description Excel 字段信息 * @author OnCloud9 * @date 2024/3/6 16:26 */ @Documented @Retention (RetentionPolicy.RUNTIME) @Target (ElementType.FIELD) public @interface ExportAlias { String value(); } |
通过注解的导出实体类,可以得到映射信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /** * @description 获取导出数据别名映射 * @author OnCloud9 * @date 2024/3/6 16:16 * @params * @return */ public static <ExportDTO> Map<String, String> getExportDataAlias(Class<ExportDTO> exportClass) { Map<String, String> aliasMap = new HashMap<>(); Field[] declaredFields = exportClass.getDeclaredFields(); for (Field field : declaredFields) { String name = field.getName(); ExportAlias annotation = field.getAnnotation(ExportAlias. class ); if (Objects.isNull(annotation)) continue ; aliasMap.put(name, annotation.value()); } return aliasMap; } |
因为是多sheet,所以要声明一个类封装sheet信息
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 | package cn.anmte.wd.excel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @Data @AllArgsConstructor @NoArgsConstructor @Builder public class SheetInfo<ExportData> { private String sheetName; private String sheetTitle; /* 导出实体的class对象 */ private Class<ExportData> exportDataClass; /* 导出的数据 */ private List<ExportData> exportDataList; /* 别名映射过滤方法 默认不处理 */ private Function<Map<String, String>, Map<String, String>> aliasFilter; /* 数据写入完成后的操作 -> 默认执行内容 */ private Consumer<WdImcExcelUtil.AfterWrite> awConsumer; } |
存在动态列名导出的场景,这里基于解析映射信息的基础上,追加了调整映射信息的方法:
提供一个Function方法接口,投入解析好的映射Map,具体调整方法交给外部调用实现
1 2 3 4 5 6 7 8 9 | private static Map<String, String> aliasConfig(ExcelWriter writer, SheetInfo<?> sheetInfo) { Map<String, String> aliasMap = getExportDataAlias(sheetInfo.getExportDataClass()); Function<Map<String, String>, Map<String, String>> aliasFilter = sheetInfo.getAliasFilter(); if (Objects.nonNull(aliasFilter)) aliasMap = aliasFilter.apply(aliasMap); writer.clearHeaderAlias(); writer.setHeaderAlias(aliasMap); writer.setOnlyAlias( true ); return aliasMap; } |
WebServlet下载逻辑部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private static void exportForDownload(HttpServletResponse response, ExcelWriter writer, String workBookName) { ServletOutputStream out = null ; try { response.setContentType( "application/vnd.ms-excel;charset=utf-8" ); response.setHeader( "Content-Disposition" , "attachment;filename=" + URLEncoder.encode(workBookName + ".xlsx" , "UTF-8" )); out = response.getOutputStream(); writer.flush(out, true ); } catch (IOException e) { e.printStackTrace(); } finally { // 关闭writer,释放内存 writer.close(); } // 关闭输出Servlet流 IoUtil.close(out); } |
导出整装的逻辑:
后置处理和上面的动态映射也是同一个逻辑,外部实现
因为我想把那些样式处理放到这里统一执行,逻辑层次是清晰的,易于维护
在下面导出可以再补充其他下载方式,开发时间有限就写到这个程度了
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 | /** * @description * @author Cloud9 * @date 2024/3/6 17:02 * @params * @return */ public static void writeWdMultiSheetWorkBook(HttpServletResponse response, String workBookName, List<SheetInfo<?>> sheetInfoList) { if (CollectionUtils.isEmpty(sheetInfoList)) return ; ExcelWriter writer = ExcelUtil.getWriter(); /* 开启多sheet页支持方法 */ writer.renameSheet( 0 , sheetInfoList.get( 0 ).getSheetName()); sheetInfoList.forEach(sheetInfo -> { /* sheet名称设置 */ writer.setSheet(sheetInfo.getSheetName()); /* sheet别名映射设置 */ Map<String, String> aliasMap = aliasConfig(writer, sheetInfo); /* 设置标头内容 */ if (StringUtils.isNotBlank(sheetInfo.getSheetTitle())) writer.merge(aliasMap.size() - 1 , sheetInfo.getSheetTitle()); /* 写入数据 */ writer.write(sheetInfo.getExportDataList(), true ); /* 后置处理 */ Consumer<AfterWrite> awConsumer = sheetInfo.getAwConsumer(); if (Objects.nonNull(awConsumer)) awConsumer.accept(AfterWrite.builder().writer(writer).aliasMap(aliasMap).build()); }); exportForDownload(response, writer, workBookName); } |
完整工具类代码(WdImcExcelUtil):
这里样式设置的代码没完全写好,可以提供参考
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 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | package cn.anmte.wd.excel; import cn.anmte.wd.excel.annotation.ExportAlias; import cn.hutool.core.io.IoUtil; import cn.hutool.poi.excel.ExcelUtil; import cn.hutool.poi.excel.ExcelWriter; import cn.hutool.poi.excel.style.StyleUtil; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.poi.ss.usermodel.*; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Field; import java.net.URLEncoder; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; /** * @description * https://www.hutool.cn/docs/#/poi/Excel%E7%94%9F%E6%88%90-ExcelWriter * @author OnCloud9 * @date 2024/3/6 16:11 */ public class WdImcExcelUtil { /** * @description 获取当前时间线后缀 * @author OnCloud9 * @date 2024/3/6 17:13 * @params * @return */ public static String getCurrentTimeSuffix(String format) { if (StringUtils.isBlank(format)) format = "yyyyMMddHHmmss" ; DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); return LocalDateTime.now().format(formatter); } /** * @description * @author OnCloud9 * @date 2024/3/6 17:33 * @params * @return */ public static void writeWdSheetWorkBook(HttpServletResponse response, String workBookName, SheetInfo<?> sheetInfo) { ExcelWriter writer = ExcelUtil.getWriter(); writer.renameSheet( 0 , sheetInfo.getSheetName()); writer.setSheet(sheetInfo.getSheetName()); /* sheet别名映射设置 */ Map<String, String> aliasMap = aliasConfig(writer, sheetInfo); /* 设置标头内容 */ if (StringUtils.isNotBlank(sheetInfo.getSheetTitle())) writer.merge(aliasMap.size() - 1 , sheetInfo.getSheetTitle()); /* 写入数据 */ writer.write(sheetInfo.getExportDataList(), true ); /* 后置处理 */ Consumer<AfterWrite> awConsumer = sheetInfo.getAwConsumer(); if (Objects.nonNull(awConsumer)) awConsumer.accept(AfterWrite.builder().writer(writer).aliasMap(aliasMap).build()); exportForDownload(response, writer, workBookName); } /** * @description * @author OnCloud9 * @date 2024/3/6 17:02 * @params * @return */ public static void writeWdMultiSheetWorkBook(HttpServletResponse response, String workBookName, List<SheetInfo<?>> sheetInfoList) { if (CollectionUtils.isEmpty(sheetInfoList)) return ; ExcelWriter writer = ExcelUtil.getWriter(); /* 开启多sheet页支持方法 */ writer.renameSheet( 0 , sheetInfoList.get( 0 ).getSheetName()); sheetInfoList.forEach(sheetInfo -> { /* sheet名称设置 */ writer.setSheet(sheetInfo.getSheetName()); /* sheet别名映射设置 */ Map<String, String> aliasMap = aliasConfig(writer, sheetInfo); /* 设置标头内容 */ if (StringUtils.isNotBlank(sheetInfo.getSheetTitle())) writer.merge(aliasMap.size() - 1 , sheetInfo.getSheetTitle()); /* 写入数据 */ writer.write(sheetInfo.getExportDataList(), true ); /* 后置处理 */ Consumer<AfterWrite> awConsumer = sheetInfo.getAwConsumer(); if (Objects.nonNull(awConsumer)) awConsumer.accept(AfterWrite.builder().writer(writer).aliasMap(aliasMap).build()); }); exportForDownload(response, writer, workBookName); } private static Map<String, String> aliasConfig(ExcelWriter writer, SheetInfo<?> sheetInfo) { Map<String, String> aliasMap = getExportDataAlias(sheetInfo.getExportDataClass()); Function<Map<String, String>, Map<String, String>> aliasFilter = sheetInfo.getAliasFilter(); if (Objects.nonNull(aliasFilter)) aliasMap = aliasFilter.apply(aliasMap); writer.clearHeaderAlias(); writer.setHeaderAlias(aliasMap); writer.setOnlyAlias( true ); return aliasMap; } private static void exportForDownload(HttpServletResponse response, ExcelWriter writer, String workBookName) { ServletOutputStream out = null ; try { response.setContentType( "application/vnd.ms-excel;charset=utf-8" ); response.setHeader( "Content-Disposition" , "attachment;filename=" + URLEncoder.encode(workBookName + ".xlsx" , "UTF-8" )); out = response.getOutputStream(); writer.flush(out, true ); } catch (IOException e) { e.printStackTrace(); } finally { // 关闭writer,释放内存 writer.close(); } // 关闭输出Servlet流 IoUtil.close(out); } /** * @description 获取导出数据别名映射 * @author OnCloud9 * @date 2024/3/6 16:16 * @params * @return */ public static <ExportDTO> Map<String, String> getExportDataAlias(ExportDTO dto) { Map<String, String> aliasMap = new HashMap<>(); Class<?> exportClass = dto.getClass(); Field[] declaredFields = exportClass.getDeclaredFields(); for (Field field : declaredFields) { String name = field.getName(); ExportAlias annotation = field.getAnnotation(ExportAlias. class ); if (Objects.isNull(annotation)) continue ; aliasMap.put(name, annotation.value()); } return aliasMap; } /** * @description 获取导出数据别名映射 * @author OnCloud9 * @date 2024/3/6 16:16 * @params * @return */ public static <ExportDTO> Map<String, String> getExportDataAlias(Class<ExportDTO> exportClass) { Map<String, String> aliasMap = new HashMap<>(); Field[] declaredFields = exportClass.getDeclaredFields(); for (Field field : declaredFields) { String name = field.getName(); ExportAlias annotation = field.getAnnotation(ExportAlias. class ); if (Objects.isNull(annotation)) continue ; aliasMap.put(name, annotation.value()); } return aliasMap; } /** * @description 头部样式设置 * @author OnCloud9 * @date 2024/3/6 16:08 * @params * @return */ public static void headerStyleSetting(ExcelWriter excelWriter) { CellStyle cellStyle = StyleUtil.createCellStyle(excelWriter.getWorkbook()); Sheet sheet = excelWriter.getSheet(); Row row = sheet.getRow( 0 ); /* 设置单元行高度为50磅 */ row.setHeight(( short ) 1000 ); /* 创建头部样式的自定义字体 */ Font font = excelWriter.createFont(); font.setFontName( "Arial" ); font.setBold( true ); font.setFontHeightInPoints(( short ) 24 ); /* 设置默认的背景色 */ cellStyle.setFont(font); cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); cellStyle.setAlignment(HorizontalAlignment.CENTER); // 设置水平居中 cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 设置垂直居中 /* 放置样式 */ Cell cell = row.getCell( 0 ); cell.setCellStyle(cellStyle); } /** * @description 列名样式设置 * @author OnCloud9 * @date 2024/3/6 16:08 * @params * @return */ public static void columnNameStyleSetting(ExcelWriter excelWriter, int colSize) { CellStyle cellStyle = StyleUtil.createCellStyle(excelWriter.getWorkbook()); Sheet sheet = excelWriter.getSheet(); Row row = sheet.getRow( 1 ); /* 创建头部样式的自定义字体 */ Font font = excelWriter.createFont(); font.setFontName( "Arial" ); font.setBold( true ); font.setColor(Font.COLOR_RED); font.setFontHeightInPoints(( short ) 10 ); /* 设置样式 */ cellStyle.setFont(font); cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); /* 边框样式 */ // 创建边框对象,并设置边框样式 BorderStyle borderStyle = BorderStyle.THIN; // 细边框 short blackIndex = IndexedColors.BLACK.getIndex(); cellStyle.setBorderTop(borderStyle); // 设置上边框 cellStyle.setTopBorderColor(blackIndex); // 设置边框颜色为黑色 cellStyle.setBorderBottom(borderStyle); // 设置下边框 cellStyle.setBottomBorderColor(blackIndex); cellStyle.setBorderLeft(borderStyle); // 设置左边框 cellStyle.setLeftBorderColor(blackIndex); cellStyle.setBorderRight(borderStyle); // 设置右边框 cellStyle.setRightBorderColor(blackIndex); for ( int i = 0 ; i < colSize; i++) { Cell cell = row.getCell(i); cell.setCellStyle(cellStyle); } } /** * @description * @author OnCloud9 * @date 2024/3/7 17:37 * @params * @return */ public static void timelineMark(ExcelWriter excelWriter, int rowIdx, int colIdx) { CellStyle cellStyle = StyleUtil.createCellStyle(excelWriter.getWorkbook()); String currentTime = getCurrentTimeSuffix( "yyyy-MM-dd HH:mm:ss" ); excelWriter.setCurrentRow(rowIdx); excelWriter.merge(colIdx, "时间:" + currentTime); Cell cell = excelWriter.getOrCreateCell(rowIdx, 0 ); /* 设置右居中 */ cellStyle.setAlignment(HorizontalAlignment.RIGHT); // 设置右居中 cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 设置垂直居中 cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); cell.setCellStyle(cellStyle); } @Data @AllArgsConstructor @NoArgsConstructor @Builder public static class AfterWrite { private ExcelWriter writer; private Map<String, String> aliasMap; } } |
3、前端JS部分:
封装好axios处理
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 | import { CUSTOMAPIURl } from '@/utils/define' import axios from 'axios' /** * @param apiPath 请求路径 * @param postData 请求参数 * @param token 令牌 * @param fileName 文件名 * @param whenDone 完成时回调 * @param whenErr 异常时回调 */ export function requestExcelExport({ apiPath, postData, token, fileName, whenDone, whenErr }) { axios({ method: 'post' , url: CUSTOMAPIURl + apiPath, data: postData, responseType: 'blob' , headers: { 'Content-Type' : 'application/json' , // 示例的 header,你可以根据需要添加更多 'Authorization' : token // 示例的授权 header } }).then(function (response) { // 创建一个 blob URL const blobUrl = window.URL.createObjectURL( new Blob([response.data])) const link = document.createElement( 'a' ) link.href = blobUrl; link.setAttribute( 'download' , fileName); // 设置下载文件的名称 document.body.appendChild(link) // 触发点击事件来下载文件 link.click(); // 清理 window.URL.revokeObjectURL(blobUrl); document.body.removeChild(link); whenDone() }). catch (function (error) { // 请求失败后的处理 console.error( 'Error downloading Excel file:' , error); whenErr() }) } |
方法使用:
四、实现效果:
这里还没处理映射顺序,看起来有点乱,等后面我再追加补充吧....
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
2021-03-07 【ActiveJdbc】01 入门
2021-03-07 【MUI】工作总结