欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

后端springboot、mybatisplus,前端vue-cli3、elementUI、axios,使用阿里巴巴提供的easyExcel导入导出excel表格

程序员文章站 2022-03-15 10:17:52
...

1. 导入Excel表格

Java后端代码

一、导入easyExcel的jar包依赖

 <!-- excel 导入导出 -->
 <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>easyexcel</artifactId>
     <version>2.2.3</version>
 </dependency>

二、创建对应的数据模型,字段需求相同时,可复用

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;

/**
 * @ApiNote : 用于接收 解析 Excel 的行数据
 * 在类上的样式注解代表本类全局作用
 */
@ContentRowHeight(26) // 内容的行高
@HeadRowHeight(30) //表头的行高
@Data
public class DrugsDTO {

    @ColumnWidth(16) //具体字段导出后表格的列宽
    @ExcelProperty(value = "药品编码")
    private String drugsCode;

    @ColumnWidth(26)
    @ExcelProperty(value = "药品名称")
    private String drugsName;

    @ColumnWidth(18)
    @ExcelProperty(value = "药品规格")
    private String drugsFormat;

    @ColumnWidth(14)
    @ExcelProperty(value = "药品剂型")
    private String drugsDosage;

    @ColumnWidth(14)
    @ExcelProperty(value = "药品类型")
    private String drugsType;

    @ColumnWidth(14)
    @ExcelProperty(value = "药品单价")
    private Double drugsPrice;

    @ColumnWidth(18)
    @ExcelProperty(value = "拼音助记码")
    private String mnemonicCode;

    @ColumnWidth(14)
    @ExcelProperty(value = "包装单位")
    private String drugsUnit;
}

三、创建读取excel表格的监听器

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.extern.slf4j.Slf4j;
import org.neuedu.entity.HisDrugs;
import org.neuedu.entity.dto.DrugsDTO;
import org.neuedu.service.IHisConstantCategoryService;
import org.neuedu.service.IHisDrugsService;
import org.springframework.beans.BeanUtils;

import java.util.ArrayList;
import java.util.List;
@Slf4j //用于日志打印
public class DrugsDataListener extends AnalysisEventListener<DrugsDTO> {

    //注意:AnalysisEventListener不支持Spring 管理,需要通过构造的形式注入
    private IHisConstantCategoryService constantCategoryService;
    private IHisDrugsService drugsService;

    //通过构造器获取所需要的执行对象
    public DrugsDataListener(IHisConstantCategoryService constantCategoryService,IHisDrugsService drugsService){
        this.constantCategoryService = constantCategoryService;
        this.drugsService = drugsService;
    }

    //每隔1000条存储数据库,实际使用中可以3000条,然后清理list,方便内存回收
    private static final Integer BATCH_COUNT = 1000;

    //存储读取的行数据,用于批量插入数据库
    List<HisDrugs> list = new ArrayList<>();

    /**
     * @ApiNote 解析 excel 每一行数据都会调用的方法
     * @param drugsDTO 把excel 中的每一行数据注入到data 对象
     * @param analysisContext 上下文
     */
    @Override
    public void invoke(DrugsDTO drugsDTO, AnalysisContext analysisContext) {
        log.debug("读取的行数据:{}",drugsDTO);
        HisDrugs drugs = new HisDrugs();

        //转换处理,通过excel 中获取的药品剂型和药品类型,到数据库中查询对应的ID,用于入库
        Integer drugsDosageId = constantCategoryService.getIdByConstantCategoryName(drugsDTO.getDrugsDosage());
        if(drugsDosageId != -1){
            drugs.setDrugsDosageId(drugsDosageId);
        }
        Integer drugsTypeId = constantCategoryService.getIdByConstantCategoryName(drugsDTO.getDrugsType());
        if(drugsTypeId != -1){
            drugs.setDrugsTypeId(drugsTypeId);
        }

        //把 DrugsDTO 中的其他数据存储到 HisDrugs 对象中,注意:名称和类型匹配才会执行,不匹配则不管
        BeanUtils.copyProperties(drugsDTO,drugs);

        list.add(drugs);

        //达到BATCH_COUNT 了,需要去存储一次数据库,防止几万条数据在内存,容易OOM
        if(list.size() >= BATCH_COUNT){
            this.saveData();
            //存储完成清理list
            list.clear();
        }
    }

    /**
     * @ApiNote 执行批量保存操作
     */
    public void saveData(){
        log.debug("开始执行批量插入~");
        drugsService.saveBatch(list);
        log.debug("批量插入执行完毕~");
    }

    /**
     * @ApiNote excel 中的所有数据解析完毕后会执行的方法
     * @param analysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        //这里也要保存数据,确保最后遗留的数据已存储到数据库
        if(list.size() > 0){
            this.saveData();
        }
        log.debug("所有数据解析完成!");
    }
}

四、Controller前端控制器代码

    /**
     * @ApiNote 非药品目录Excel文件导入
     * @param file excel文档
     * @return
     * @throws Exception
     */
    @ApiOperation("非药品目录Excel文件导入")
    @ApiImplicitParam(name = "file",value = "excel文档",required = true)
    @PostMapping("/fileUpload")
    public ResponseEntity excelImport(MultipartFile file) throws Exception{
        //使用 阿里巴巴 提供的 EasyExcel 完成 Excel 读取
        EasyExcel
                .read(file.getInputStream(), DrugsDTO.class, new DrugsDataListener(constantCategoryService,drugsService))
                .sheet() //指定读取的 sheet ,sheet -> 表格
                .doRead();
        return ResponseEntity.ok("非药品目录导入成功");
    }

vue前端代码

五、axios异步请求的统一处理httpAxios.js

在判断请求方式为post或put的基础上,增加了文件上传的判断

import axios from 'axios'  //异步请求
import qs from 'qs' //用于post请求的数据转换
import router from "@/router" //页面跳转 replace  push
//elementUI中的按需加载 :Message用于消息提示,Loading 请求加载层
import {Loading,Message} from 'element-ui'

// axios 全局配置
axios.defaults.timeout = 5000; // 5s没响应则认为该请求失败
// 配置请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
axios.defaults.baseURL = '/api';  //跨域配置可以添加上它,就不用每一个请求都以api开头了

let loadingInstance; //加载对象,设置为全局的对象
// 添加请求拦截器
axios.interceptors.request.use(request => {
        //TODO 开始请求,日志打印
        console.log("开始请求:",request.url,",参数:",JSON.stringify(request.params));

        //加载层:数据加载中。。
        loadingInstance = Loading.service({
            lock: true,
            text: '数据加载中,请稍后...',
            spinner: 'el-icon-loading',
            background: 'rgba(0, 0, 0, 0.7)'
        });

        //如果是post 或者 put 请求,需要做数据转换
        if (request.method === 'post' || request.method === 'put') {
            //如果是文件上传,则不需要进行数据转换
            //约定,如果是文件上传,则请求地址以 fileUpload 结尾
            let currentUrl = request.url.split('/')[request.url.split('/').length-1];
            if(currentUrl !== 'fileUpload'){
                //不是文件上传,才做处理
                request.data = qs.stringify(request.data)  //这里使用qs对data进行处理,转为json数据
            }
        }
        
        if(request.method === 'get'){
            //如果是下载,需要指定下载格式为 blob 或者二进制
            //约定,如果是文件下载,则请求地址以 downLoad 结尾
            let curUrl = request.url.split('/')[request.url.split('/').length - 1];
            if(curUrl === 'downLoad'){
                request.responseType = 'blob';
                return request; //这里对上传文件的api 不做传序列化处理
            }
        }

        return request;
    },err => {
        loadingInstance.close();
        Message.error('请求超时!');
        return Promise.reject(err)
    }
);
//添加响应拦截器
axios.interceptors.response.use(response => {
        //TODO 请求完毕数据输出,后续添加后端返回的对应状态判断
        console.log("请求完毕数据:",response.data);
        loadingInstance.close(); //关闭加载层

    // 这里是前后端分离,下载的时候需要通过下载链接来操作
    //把下载的 blob 数据转为 excel
    if(response.config.responseType === 'blob'){ // 下载excel类型
        let blob = new Blob([response.data],{type:'application/vnd.ms-excel;charset=utf-8'});
        let downloadElement = document.createElement('a');
        let href = window.URL.createObjectURL(blob); //创建下载的连接
        downloadElement.href = href;
        downloadElement.download = decodeURI(response.headers['content-disposition'].split('=')[1]); //处理文件名乱问题, 下载后文件
        document.body.appendChild(downloadElement);
        downloadElement.click(); //点击下载
        document.body.removeChild(downloadElement); //下载完成移除元素
        window.URL.revokeObjectURL(href); //释放掉blob对象
        return;
    }

        if (response.data.code === 200) {
            return response.data
        }else if (response.data.code === 504) {
            Message.error('服务器被吃了⊙﹏⊙∥');
        } else if(response.data.code === 404){
            Message.error('请求地址不存在!');
            router.replace({path:'/404'})
        }else if (response.data.code === 403) {
            Message.error('权限不足,请联系管理员!');
            router.replace({path:'/403'})
        } else if (response.data.code === 401) { //未登录
            // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
            router.replace({
                path: '/login',
                query: {
                    redirect: router.currentRoute.fullPath
                }
            });
        }else{
            //操作失败后的显示信息,不用这里直接弹出
            //Message.error(response.data.msg);
        }
        return response.data
    },err => {
        loadingInstance.close();
        Message.error('请求失败,请稍后再试');
        return Promise.reject(err)
    }
);

//对 axios 的所有请求进行参数统一规格
let http = {
    get(url, params = {}){
        return axios.get(url, {params})
    },
    post(url, params = {}){
        return axios.post(url, params)
    },
    del(url, params = {}){
        return axios.delete(url, {params})
    },
    put(url, params = {}){
        return axios.put(url, params)
    },
    upload(url, params = {}){
        return axios.post(url, params,{
            headers: { // 这里指定的是多文件二进制上传
                'Content-Type': 'multipart/form-data'
            }
        })
    }
};

export default http;

六、具体发请求的drugs.js

import http from './httpAxios';

export default {
    importExcel(file){ //非药品目录Excel文件导入
        return http.upload('/his-drugs/fileUpload',file);
    }
}

七、js的路由index.js

import drugs from './drugs.js';// 统一管理,后缀.js可以省略

// 统一导出
export default {
    //drugs:drugs, //取名和导入的名字一样时,可简写
    drugs,
}

八、vue部分,Drugs.vue

<template>
    <div id="drugs">
		<div class="container">
            <!-- 导入导出部分 -->
            <el-form :inline="true" :model="query" class="demo-form-inline" size="mini">
                <el-form-item>
                    <el-input v-model="query.search" clearable placeholder="药品编码/名称/拼音助记码"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="pageSearchHandler" icon="el-icon-search">查询</el-button>
					<!-- 根据查询结果导出查询出来所有的数据 -->
                    <el-button type="info" @click="exportPageHandler" icon="el-icon-download">导出</el-button>
                </el-form-item>
                <el-form-item>
                    <!--
                        action:上传的地址,这里我们通过axios 进行代理,不直接写
                        http-request:覆盖默认的上传行为,可以自定义上传的实现
                        show-file-list:是否显示已上传的文件列表
                     -->
                    <el-upload action="" :http-request="importHandler" :show-file-list="false">
                        <el-button type="info" icon="el-icon-upload2">导入</el-button>
                    </el-upload>

                </el-form-item>
            </el-form>
        </div>
    </div>
</template>

<script>
    export default {
        name: 'Drugs',
        data(){
            return {
                query:{
                    search: '', //查询条件:药品编码 或 药品名称 或 拼音助记码
                }
            }
        },
        methods:{
            // 导入,这里会自动把上传的文件带过来
            importHandler(data){
                // - 通过异步的形式上传文件,需要模拟表单的形式
                //创建表单对象
                let form = new FormData();
                //后端接收参数,可以接收多个参数
                form.append('file',data.file);
                this.$api.drugs.importExcel(form).then(res=>{
                    //提示信息
                    if(res.code === 200){
                        this.$message.success(res.msg);
                        //重新加载数据
                        this.pageSearchHandler();
                    }
                });
            }
        }
    };
</script>

<style scoped>

</style>

2.导出excel表格

后端Java部分

一、和导入excel的操作相同的部分,导入jar包相同,数据模型drugsDTO复用

二、使用MyBatisPlus 的条件构造器,在Mapper接口中编写方法

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.neuedu.entity.HisDrugs;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.neuedu.entity.dto.DrugsDTO;

import java.util.List;

/**
 * <p>
 * 非药品目录收费项目 Mapper 接口
 * </p>
 *
 * @author WHLin
 * @since 2020-03-30
 */
public interface HisDrugsMapper extends BaseMapper<HisDrugs> {

    //使用MyBatisPlus 的条件构造器
    //这里drugs_dosage_id 和drugs_type_id使用的是外键的形式完成的查询
    @Select("SELECT drugs_code,drugs_name,drugs_format,\n" +
            "(select constant_name from his_constant_category where id = his_drugs.drugs_dosage_id) drugs_dosage,\n" +
            "(select constant_name from his_constant_category where id = his_drugs.drugs_type_id) drugs_type,\n" +
            "drugs_price,mnemonic_code,drugs_unit FROM his_drugs ${ew.customSqlSegment}")
    List<DrugsDTO> excelExport(@Param(Constants.WRAPPER) Wrapper<DrugsDTO> wrapper);
}

三、在service中编写对应的方法接口

import org.neuedu.entity.HisDrugs;
import com.baomidou.mybatisplus.extension.service.IService;
import org.neuedu.entity.dto.DrugsDTO;

import java.util.List;

/**
 * <p>
 * 非药品目录收费项目 服务类
 * </p>
 *
 * @author WHLin
 * @since 2020-03-30
 */
public interface IHisDrugsService extends IService<HisDrugs> {

    List<DrugsDTO> excelExport(String search);
}

四、在service实现类中实现方法,并完成条件构造的逻辑

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.neuedu.entity.HisDrugs;
import org.neuedu.entity.dto.DrugsDTO;
import org.neuedu.mapper.HisDrugsMapper;
import org.neuedu.service.IHisDrugsService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 * 非药品目录收费项目 服务实现类
 * </p>
 *
 * @author WHLin
 * @since 2020-03-30
 */
@Service
public class HisDrugsServiceImpl extends ServiceImpl<HisDrugsMapper, HisDrugs> implements IHisDrugsService {

    @Override
    public List<DrugsDTO> excelExport(String search) {
        //查询的条件构造器
        QueryWrapper<DrugsDTO> wrapper = new QueryWrapper<>();
        wrapper.like(StringUtils.isNotBlank(search),"drugs_code",search)
                .or()
                .like(StringUtils.isNotBlank(search),"drugs_name",search)
                .or()
                .like(StringUtils.isNotBlank(search),"mnemonic_code",search)
                .eq("del_mark",0);
        return baseMapper.excelExport(wrapper);
    }
}

五、Controller前端控制器的编写

    @ApiOperation("非药品目录导出")
    @ApiImplicitParam(name = "search",value = "查询条件:药品编码 或 药品名称 或 拼音助记码")
    @GetMapping("/downLoad")
    public void excelExport(HttpServletResponse response, String search) throws Exception{
        //写入excel 的数据
        List<DrugsDTO> list = drugsService.excelExport(search);

        //这里注意 有同学反映使用swagger 会导致各种问题,请直接用浏览器或者用postman
        response.setContentType("application/vnd.ms-excel;charset=utf-8");
        //response.setCharacterEncoding("utf-8");
        //这里URLEncoder.encode 可以防止中文乱码,当然和easyexcel 没有关系
        String fileName = URLEncoder.encode("非药品目录-" + LocalDate.now().toString(), "utf-8");
        //设置为手动下载,不让浏览器自动下载
        response.setHeader("Content-disposition","attachment;filename=" + fileName + ".xlsx");

        EasyExcel.write(response.getOutputStream(),DrugsDTO.class)
                .sheet("非药品目录")
                .doWrite(list); // 这是需要写入的数据
    }

前端部分代码

六、在httpAxios.js中判断为get请求之后,再判断是否为文件下载;下载的时候需要通过创建下载链接来操作,把下载的 blob 数据转为 excel,上面代码已经展示出来了

七、具体发请求的drugs.js

import http from './httpAxios';

export default {
	excelExport(search){ // 非药品目录导出
	        return http.get('his-drugs/downLoad',search);
	 }
}

八、drugs.vue文件中js使用

methods:{
	// 导出
    exportPageHandler(){
         console.log(this.query.search);
         this.$api.drugs.excelExport({search:this.query.search});
     }
},