后端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});
}
},