SpringBoot + Vue + Axios 操作Excel文档

0X01 需求描述

实现前后端分离项目中的数据以Excel文档格式进行导入导出。

0X02后端实现

在这里我选择使用了EasyPOI第三方工具来进行实现,有关其相关内容,可以去它官方文档阅读:EasyPOI官方文档。在这里示例一个基于SpringBoot项目中使用的Demo。

1.导入依赖坐标

<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-base</artifactId>
    <version>4.1.0</version>
</dependency>
<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-web</artifactId>
    <version>4.1.0</version>
</dependency>
<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-annotation</artifactId>
    <version>4.1.0</version>
</dependency>

我这里使用SpringBoot来搭建项目,可以进行如下引入:

<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-spring-boot-starter</artifactId>
    <version>4.1.0</version>
</dependency>

2.编写实体类

package cn.imyjs.vo;

import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;


/**
 * @Classname RecordVO
 * @Description TODO
 * @Date 2022/4/16 17:05
 * @Created by YJS
 * @WebSite www.imyjs.cn
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class RecordVO implements Serializable {
    @Excel(name = "任务编号")
    private int cid; // 任务卡片唯一标识
    @Excel(name = "任务名称")
    private String name; //任务名称
    @Excel(name = "是否完成", replace = {"已完成_1","未完成_0"})
    private int flag; // 今日任务是否完成
    @Excel(name = "点赞数")
    private int likes; //点赞数
    @Excel(name = "学习时长")
    private String time; // 学习时长
    @Excel(name = "打卡日期")
    private String completedate; //打卡日期
    @Excel(name = "打卡标题")
    private String title;
    @Excel(name = "打卡内容")
//    @ExcelIgnore
    private String  reason; // 原因
}

有关常用注解有关说明,具体详细使用见官方文档。

3.编写ExcelUtils工具类

package cn.imyjs.controller.utils;

import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

/**
 * @Classname ExcelUtil
 * @Description TODO
 * @Date 2022/4/26 17:27
 * @Created by YJS
 * @WebSite www.imyjs.cn
 */

@Slf4j
public class ExcelUtils {
    /**
     * excel 导出
     *
     * @param list           数据
     * @param title          标题
     * @param sheetName      sheet名称
     * @param pojoClass      pojo类型
     * @param fileName       文件名称
     * @param isCreateHeader 是否创建表头
     * @param response
     */
    public static void exportExcel(List<?> list, String title, String sheetName, Class<?> pojoClass, String fileName, boolean isCreateHeader, HttpServletResponse response) throws IOException {
        ExportParams exportParams = new ExportParams(title, sheetName, ExcelType.XSSF);
        exportParams.setCreateHeadRows(isCreateHeader);
        defaultExport(list, pojoClass, fileName, response, exportParams);
    }

    /**
     * excel 导出
     *
     * @param list      数据
     * @param title     标题
     * @param sheetName sheet名称
     * @param pojoClass pojo类型
     * @param fileName  文件名称
     * @param response
     */
    public static void exportExcel(List<?> list, String title, String sheetName, Class<?> pojoClass, String fileName, HttpServletResponse response) throws IOException {
        defaultExport(list, pojoClass, fileName, response, new ExportParams(title, sheetName, ExcelType.XSSF));
        log.error(fileName + "导出....");
    }

    /**
     * excel 导出
     *
     * @param list         数据
     * @param pojoClass    pojo类型
     * @param fileName     文件名称
     * @param response
     * @param exportParams 导出参数
     */
    public static void exportExcel(List<?> list, Class<?> pojoClass, String fileName, ExportParams exportParams, HttpServletResponse response) throws IOException {
        defaultExport(list, pojoClass, fileName, response, exportParams);
    }

    /**
     * excel 导出
     *
     * @param list     数据
     * @param fileName 文件名称
     * @param response
     */
    public static void exportExcel(List<Map<String, Object>> list, String fileName, HttpServletResponse response) throws IOException {
        defaultExport(list, fileName, response);
    }

    /**
     * 默认的 excel 导出
     *
     * @param list         数据
     * @param pojoClass    pojo类型
     * @param fileName     文件名称
     * @param response
     * @param exportParams 导出参数
     */
    private static void defaultExport(List<?> list, Class<?> pojoClass, String fileName, HttpServletResponse response, ExportParams exportParams) throws IOException {
        Workbook workbook = ExcelExportUtil.exportExcel(exportParams, pojoClass, list);
        downLoadExcel(fileName, response, workbook);
    }

    /**
     * 默认的 excel 导出
     *
     * @param list     数据
     * @param fileName 文件名称
     * @param response
     */
    private static void defaultExport(List<Map<String, Object>> list, String fileName, HttpServletResponse response) throws IOException {
        Workbook workbook = ExcelExportUtil.exportExcel(list, ExcelType.HSSF);
        downLoadExcel(fileName, response, workbook);
    }

    /**
     * 下载
     *
     * @param fileName 文件名称
     * @param response
     * @param workbook excel数据
     */
    private static void downLoadExcel(String fileName, HttpServletResponse response, Workbook workbook) throws IOException {
        try {
            response.setCharacterEncoding("UTF-8");
            response.setHeader("content-Type", "application/vnd.ms-excel");
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName + "." + ExcelTypeEnum.XLSX.getValue(), "UTF-8"));
            workbook.write(response.getOutputStream());
        } catch (Exception e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * excel 导入
     *
     * @param filePath   excel文件路径
     * @param titleRows  标题行
     * @param headerRows 表头行
     * @param pojoClass  pojo类型
     * @param <T>
     * @return
     */
    public static <T> List<T> importExcel(String filePath, Integer titleRows, Integer headerRows, Class<T> pojoClass) throws IOException {
        if (StringUtils.isBlank(filePath)) {
            return null;
        }
        ImportParams params = new ImportParams();
        params.setTitleRows(titleRows);
        params.setHeadRows(headerRows);
        params.setNeedSave(true);
        params.setSaveUrl("/excel/");
        try {
            return ExcelImportUtil.importExcel(new File(filePath), pojoClass, params);
        } catch (NoSuchElementException e) {
            throw new IOException("模板不能为空");
        } catch (Exception e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * excel 导入
     *
     * @param file      excel文件
     * @param pojoClass pojo类型
     * @param <T>
     * @return
     */
    public static <T> List<T> importExcel(MultipartFile file, Class<T> pojoClass) throws IOException {
        return importExcel(file, 1, 1, pojoClass);
    }

    /**
     * excel 导入
     *
     * @param file       excel文件
     * @param titleRows  标题行
     * @param headerRows 表头行
     * @param pojoClass  pojo类型
     * @param <T>
     * @return
     */
    public static <T> List<T> importExcel(MultipartFile file, Integer titleRows, Integer headerRows, Class<T> pojoClass) throws IOException {
        return importExcel(file, titleRows, headerRows, false, pojoClass);
    }

    /**
     * excel 导入
     *
     * @param file       上传的文件
     * @param titleRows  标题行
     * @param headerRows 表头行
     * @param needVerfiy 是否检验excel内容
     * @param pojoClass  pojo类型
     * @param <T>
     * @return
     */
    public static <T> List<T> importExcel(MultipartFile file, Integer titleRows, Integer headerRows, boolean needVerfiy, Class<T> pojoClass) throws IOException {
        if (file == null) {
            return null;
        }
        try {
            return importExcel(file.getInputStream(), titleRows, headerRows, needVerfiy, pojoClass);
        } catch (Exception e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * excel 导入
     *
     * @param inputStream 文件输入流
     * @param titleRows   标题行
     * @param headerRows  表头行
     * @param needVerfiy  是否检验excel内容
     * @param pojoClass   pojo类型
     * @param <T>
     * @return
     */
    public static <T> List<T> importExcel(InputStream inputStream, Integer titleRows, Integer headerRows, boolean needVerfiy, Class<T> pojoClass) throws IOException {
        if (inputStream == null) {
            return null;
        }
        ImportParams params = new ImportParams();
        params.setTitleRows(titleRows);
        params.setHeadRows(headerRows);
        params.setSaveUrl("/excel/");
        params.setNeedSave(true);
        // params.setNeedVerfiy(needVerfiy);
        try {
            return ExcelImportUtil.importExcel(inputStream, pojoClass, params);
        } catch (NoSuchElementException e) {
            throw new IOException("excel文件不能为空");
        } catch (Exception e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * Excel 类型枚举
     */
    enum ExcelTypeEnum {
        XLS("xls"), XLSX("xlsx");
        private String value;

        ExcelTypeEnum(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }
}

此工具类一般都是可以在网上直接找到的。

4.编写控制类

@RestController
@RequestMapping("/admin/records")
@Slf4j
public class RecordController {
    @Autowired
    private RecordVOMapper recordVOMapper;

    @GetMapping("/export")
    public void exportRecords(HttpServletResponse response) throws IOException {
        List<RecordVO> recordVOS = recordVOMapper.selectAll();
        ExcelUtils.exportExcel(recordVOS, "任务打卡记录", "打卡信息", RecordVO.class, "打卡信息", response);
    }

}

到此,后端相关代码的编写就完成了,下面再进行前端开发时,却出现了一个问题!具体往下看。

0X03前端实现

1.问题描述

起初以为前端开发就非常简单了,只需要再具体页面某位置添加一个导出按钮,并给其绑定单击事件,并使用Axios以GET方式发送请求到指定API接口,就可以实现该功能了,其实也就是这么回事,但是需要注意一点的是,使用Axios发送GET请求时需要进行一定的处理才可以。

起初这样写:

// 导出打卡数据
exportData() {
    this.axios.get(this.$api + "/admin/records/export");
}

结果并没有看到下载文件,结果我一直以为是后端的问题,结果就去仔细检查后端相关代码,从ExcelUtils相关代码、控制接口类到实体类.....然而结果并无效果,紧接着就去考虑是不是项目的过滤器?拦截器?还用了AOP.......然后进行一番修改和测试,最终!以失败告终。

2.问题分析

这是怎么回事??

结果正在无望之时,无意之间使用浏览器的地址栏直接向接口发起了GET请求,结果竟然下载成功了.....

我去,瞬时又体会到了山重水复疑无路,柳暗花明又一村真切感受。这次可以保证不会是后端代码的问题了,那么肯定是前端,而且是发送的GET请求有问题。下边就是解决他了.....怎么解决呢?肯定是面向百度编程啦!

3.问题解决

因为像这样的问题一般都是会有固定的解决方案的,于是改正了有关代码:

 // 导出打卡数据
    exportData() {
      this.axios({
        method: "get",
        url: "http://localhost/admin/records/export",
        responseType: "blob"  // 这里需要指定响应类型为流式文件即二进制
      }).then((resp) => {
        // 这里resp.data是返回的blob对象
        let blob = new Blob([resp.data], 
        {type: 'application/vnd.openxmlformats-	officedocument.spreadsheetml.sheet;charset=utf-8'});
        //application/vnd.openxmlformats-officedocument.spreadsheetml.sheet这里表示xlsx类型
        let downloadElement = document.createElement('a');
        let href = window.URL.createObjectURL(blob); //创建下载的链接
        downloadElement.href = href;
        downloadElement.download = "打卡记录.xlsx"; //下载后文件名
        document.body.appendChild(downloadElement);
        downloadElement.click(); //点击下载
        document.body.removeChild(downloadElement); //下载完成移除元素
        window.URL.revokeObjectURL(href); //释放掉blob对象
      }).catch((r) => {
        console.log(r)
        this.$message.error("文件导出异常!")
      })

到这里,前面所提到的需求就简单给实现了。

0X04EasyPOI常用注解

  • @Excel 作用到filed上面,是对Excel一列的一个描述
  • @ExcelCollection 表示一个集合,主要针对一对多的导出,比如一个老师对应多个科目,科目就可以用集合表示
  • @ExcelEntity 表示一个继续深入导出的实体,但他没有太多的实际意义,只是告诉系统这个对象里面同样有导出的字段
  • @ExcelIgnore 和名字一样表示这个字段被忽略跳过这个导导出
  • @ExcelTarget 这个是作用于最外层的对象,描述这个对象的id,以便支持一个对象可以针对不同导出做出不同处理

1.@Excel

这个是必须使用的注解,如果需求简单只使用这一个注解也是可以的,涵盖了常用的Excel需求,需要大家熟悉这个功能,主要分为基础,图片处理,时间处理,合并处理几块,name_id是上面讲的id用法,这里就不累言了

属性 类型 默认值 功能
name String null 列名,支持name_id
needMerge boolean fasle 是否需要纵向合并单元格(用于含有list中,单个的单元格,合并list创建的多个row)
orderNum String “0” 列的排序,支持name_id
replace String[] {} 值得替换 导出是{a_id,b_id} 导入反过来
savePath String “upload” 导入文件保存路径,如果是图片可以填写,默认是upload/className/ IconEntity这个类对应的就是upload/Icon/
type int 1 导出类型 1 是文本 2 是图片,3 是函数,10 是数字 默认是文本
width double 10 列宽
height double 10 列高,后期打算统一使用@ExcelTarget的height,这个会被废弃,注意
isStatistics boolean fasle 自动统计数据,在追加一行统计,把所有数据都和输出[这个处理会吞没异常,请注意这一点]
isHyperlink boolean false 超链接,如果是需要实现接口返回对象
isImportField boolean true 校验字段,看看这个字段是不是导入的Excel中有,如果没有说明是错误的Excel,读取失败,支持name_id
exportFormat String “” 导出的时间格式,以这个是否为空来判断是否需要格式化日期
importFormat String “” 导入的时间格式,以这个是否为空来判断是否需要格式化日期
format String “” 时间格式,相当于同时设置了exportFormat 和 importFormat
databaseFormat String “yyyyMMddHHmmss” 导出时间设置,如果字段是Date类型则不需要设置 数据库如果是string 类型,这个需要设置这个数据库格式,用以转换时间格式输出
numFormat String “” 数字格式化,参数是Pattern,使用的对象是DecimalFormat
imageType int 1 导出类型 1 从file读取 2 是从数据库中读取 默认是文件 同样导入也是一样的
suffix String “” 文字后缀,如% 90 变成90%
isWrap boolean true 是否换行 即支持\n
mergeRely int[] {} 合并单元格依赖关系,比如第二列合并是基于第一列 则{0}就可以了
mergeVertical boolean fasle 纵向合并内容相同的单元格
fixedIndex int -1 对应excel的列,忽略名字
isColumnHidden boolean false 导出隐藏列

2.@ExcelCollection

一对多的集合注解,用以标记集合是否被数据以及集合的整体排序

属性 类型 默认值 功能
id String null 定义ID
name String null 定义集合列名,支持nanm_id
orderNum int 0 排序,支持name_id
type Class<?> ArrayList.class 导入时创建对象使用

3.@ExcelEntity

标记是不是导出excel 标记为实体类,一遍是一个内部属性类,标记是否继续穿透,可以自定义内部id

属性 类型 默认值 功能
id String null 定义ID

4.@ExcelIgnore

忽略这个属性,多使用需循环引用中,无需多解释吧^^

5.@ExcelTarget

限定一个到处实体的注解,以及一些通用设置,作用于最外面的实体@ExcelCollection

一对多的集合注解,用以标记集合是否被数据以及集合的整体排序

属性 类型 默认值 功能
id String null 定义ID
name String null 定义集合列名,支持nanm_id
orderNum int 0 排序,支持name_id
type Class<?> ArrayList.class 导入时创建对象使用

有关这些注解的使用实例具体可以参考官方文档:EasyPOI官方文档

微信关注

阅读剩余
THE END