上一篇博客中筆者提到了自己在工作中使用axios的post請求類型調用excel的導出接口時遇到了一個坑,也提到過在下篇文章中再寫一篇關於java使用apache poi組件實現帶按鈕下載的導出excel文件功能的博客。筆者當然要儘量說道做到,因此在這一邊博客中本人繼續帶來乾貨,不僅帶領讀者實現帶按鈕的導出功能,還要在利用vue整合element-ui庫在前端實現美觀的接口數據的展示。話不多說,下面展示乾貨!
1 vue整合element-ui庫實現帶表單查詢的數據展示和導出excel文件界面
這篇博客的本地開發項目是在我的上一遍博客dom4j解析XML配置,Java反射與POI API實現Excel文件導出導入模板工具類(下)的基礎之上,所以關於前後端項目的架構和前端路由的配置請讀者參考本人這篇博客。
1.1 參考element-ui官網demo實現帶有表單查詢和數據導出、導入功能的模板組件
About.vue 模板部分代碼如下:
<template>
<div class="excel-example">
<el-form name="searchForm" ref="searchForm" :model="form" size="mini" >
<div class="row">
<el-form-item label="用戶賬號:">
<el-input v-model="form.userAccount"></el-input>
</el-form-item>
<el-form-item label="用戶名:">
<el-input v-model="form.nickName"></el-input>
</el-form-item>
</div>
<div class="row">
<el-form-item label="部門:">
<el-select v-model="form.deptNo" clearable placeholder="請選擇部門">
<el-option v-for="item in depts" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="創建時間:" class="datetime-range">
<el-date-picker placeholder="起始查詢時間" class="startTime"
v-model="form.startDate" @change="startTimeChange"
clearable size="small" editable align="left" type="datetime" value-format="yyyy-MM-dd HH:mm:ss">
</el-date-picker>
<span style="display:block;width:20px;float:left">~</span>
<el-date-picker placeholder="結束查詢時間"
v-model="form.endDate" class="endTime" @change="endTimeChange"
clearable size="small" editable align="left" type="datetime" value-format="yyyy-MM-dd HH:mm:ss">
</el-date-picker>
</el-form-item>
</div>
</el-form>
<form id="hiddenForm" hidden="true" action="http://localhost:8081/springboot/export/searchExcel" method="POST">
<input name="userAccount" :value="form.userAccount"/>
<input name="nickName" :value="form.nickName"/>
<input name="deptNo" :value="form.deptNo"/>
<input name="startDate" :value="form.startDate"/>
<input name="endDate" :value="form.endDate"/>
</form>
<el-row class="btn-group">
<el-button type="primary" icon="el-icon-search" @click="searchUsers">查詢</el-button>
<el-button>重置</el-button>
<el-button type="primary" icon="el-icon-download" @click="exportExcel">導出</el-button>
<el-upload class="uploadDemo" ref="upload" name="uploadFile"
action="http://localhost:8081/springboot/importExcel"
:before-upload="beforeUpload"
:on-progress="onProgress">
<el-button size="small" type="primary" icon="el-icon-upload" >導入excel文件數據</el-button>
<div slot="tip" class="el-upload__tip">只能上傳xls/xlsx文件,且不超過1MB</div>
</el-upload>
</el-row>
<el-table :data="tableData" border style="width: 100%" id="userData">
<el-table-column v-for="item in columns" :prop="item.prop" :label="item.label" :key="item.key">
</el-table-column>
</el-table>
</div>
</template>
以上主要用到了element-ui組件中的Form表單、Input輸入框、Select選擇器和DatePicker日期選擇器等組件,在element-ui官網https://element.eleme.cn/#/zh-CN/component/installation
的“組件菜”單都能找到相應vue實現的相應demo,開發人員需要更具自己的需求作適當修改即可。最新的element-ui版本爲2.13版本,筆者項目中用的爲2.12版本
1.2 數據model與按鈕邦定的函數實現
<script>
export default {
name: 'about',
data(){
return {
form:{
userAccount: '',
nickName: '',
deptNo: -1,
startDate: '',
endDate: ''
},
depts:[
{value:1001,label: '生產部'},
{value:1002,label:'測試部'},
{value:1003,label:'銷售部'},
{value: 1004,label: '研發部'},
{value: 1005,label: '採購部'},
{value: 1006,label: '運維部'},
{value: 1007,label: '售後支持部'},
{value:-1,label:'所有部門'}
],
tableData: [],
columns: [{prop: 'userAccount',label: '用戶賬號',width: '120'},
{prop: 'nickName',label: '用戶名', width: '120'},
{prop: 'deptNo',label: '部門編號',width: '120'},
{prop: 'deptName',label: '部門名稱', width: '120'},
{prop: 'birthDay',label: '出生日期', width: '120'},
{prop: 'updatedTime',label: '更新時間',width: '150'}]
}
},
methods:{
searchUsers(){
let params = this.form;
let searchUrl = '/user/queryUsers?';
for(let key in params){
let val = params[key]?encodeURIComponent(params[key]):'';
searchUrl += encodeURIComponent(key) +'=' + val + '&';
}
this.$getRequest(searchUrl).then(response=>{
this.tableData = response.data.data;
});
},
exportExcel(){
let hiddenForm = document.getElementById("hiddenForm");
hiddenForm.submit();
},
beforeUpload(file){
console.log(file);
const name = file.name+'';
const flag = name.endsWith(".xlsx") || name.endsWith("xls");
if(!flag){
this.$message.error({message:'文件格式不合要求,只能上傳xls/xlsx文件!'});
return false;
}
},
onProgress(event,file){
console.log(event);
console.log(file);
},
startTimeChange(val){
console.log('startTime='+val);
console.log(this.form.startDate);
},
endTimeChange(val){
console.log('endTime='+val);
console.log(this.form.endDate);
}
}
}
</script>
注意:以上導出form表單查詢數據的excel文件的前端按鈕實現使用了一個隱藏的form表單,以及javascript中form節點對象自帶的submit函數提交請求。而使用axios的Post請求發送導出excel文件的請求反而會因爲Content-Type不是application/octet-stream類型而導致接口報錯,而一旦把請求頭的Content-Type屬性值改爲application/octet-stream,form表單中的參數又無法傳遞到服務器,同樣會導致接口報錯。而直接調用form.submit()方法卻能完美解決上述問題!
爲了方便組件對不通請求類型接口的調用,本人蔘考同行的經驗對axios的接口進行了封裝處理,並將axios調用接口的函數下掛到了vue組件原型方法中,讓每一個實例化的vue組件都自帶發送get個post http請求的屬性方法
1.3 axios調用後臺接口函數的封裝
在項目的src目錄下新建request文件夾,並在該文件夾下新建api.js文件,編輯代碼如下:
import axios from 'axios';
axios.defaults.baseURL = 'http://localhost:8081/springboot';
//get請求
export const getRequest = (url)=>{
return axios({
method: 'GET',
url: `${url}`
});
}
//post請求
export const postRequest=(url,params)=>{
return axios({
method: 'POST',
url: `${url}`,
data: params,
transformRequest: [function(data){
let ret = '';
for(let key in data){
ret += encodeURIComponent(key) + '=' + encodeURIComponent(data[key])+'&';
}
return ret;
}],
headers:{
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
//put請求
export const putRequest = (url, params) => {
return axios({
method: 'PUT',
url: `${url}`,
data: params,
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}
//delete請求
export const deleteRequest = (url) => {
return axios({
method: 'delete',
url: `${url}`
});
}
//上傳文件請求
export const uploadFileRequest = (url, params) => {
return axios({
method: 'post',
url: `${url}`,
data: params,
headers: {
'Content-Type': 'multipart/form-data'
}
});
}
然後在main.js文件中引入api.js中的請求方法並添加到vue實例的原型方法中去,實現代碼如下
import {getRequest,postRequest,putRequest,deleteRequest,uploadFileRequest} from './request/api.js';
Vue.prototype.$getRequest = getRequest;
Vue.prototype.$postRequest = postRequest;
Vue.prototype.$putRequest = putRequest;
Vue.prototype.$deleteRequest = deleteRequest;
Vue.prototype.$uploadFileRequest = uploadFileRequest;
Vue.config.productionTip = false;
// Vue.config.devtools=true;
Vue.prototype.$message = Message;
1.4 定義頁面樣式
組件頁面的樣式是根據啓動項目後利用開發者工具調試出來的,最終的about.vue文件中的樣式代碼如下:
<style>
div.excel-example{
margin: 20px 20px;
}
form[name='searchForm']{
width: 80%;
margin-left: 100px;
height: 150px;
border: 2px solid #ccc;
border-radius: 5px;
}
div.row{
width: 100%;
height: 40px;
}
.el-row.btn-group{
margin-top: 16px;
flex-flow: row;
display: flex;
width: 80%;
margin-left: 25%;
}
.uploadDemo{
margin-left: 5%;
height: 26px;
}
.el-form-item.el-form-item--mini{
width: 35%;
min-width: 280px;
float: left;
height: 35px;
margin-top: 20px;
margin-left:10px;
}
.el-form-item.el-form-item--mini>.el-form-item__label{
min-width:60px;
float:left;
}
.el-form-item__content>.el-input.el-input--mini{
width: 40%;
float:left;
}
.el-form-item__content>.el-select{
float: left;
margin-left: 6%;
width: 40%;
}
.el-form-item.datetime-range{
width: 60%;
float: left;
}
.el-form-item__content>.el-date-editor.el-date-editor--daterange{
float: left;
margin-left: 0px;
width: 35%;
}
.el-date-editor.el-input{
width: 195px;
float: left;
}
.el-row{
margin-top: 16px;
}
.el-table{
width: 80%;
margin-top: 16px;
margin-left: 100px;
}
.el-table th>.cell{
text-align: center;
}
.el-table th{
background-color: #F1F2F3;
}
.el-table__body-wrapper tbody tr td{
background-color:#fff;
}
.el-table__body-wrapper tbody tr td>.cell{
text-align: center;
}
</style>
注意樣式代碼中的
保存所有修改過的文件後,在項目根目錄下使用git bash 打開一個命令控制檯,輸入 npm run serve
命令回車後啓動本地開發環境服務器,開發環境啓動成功後可以看到如下信息
App running at:
- Local: http://localhost:3000/
- Network: http://192.168.1.102:3000/
在谷歌瀏覽器中輸入 http://localhost:3000 回車進入主頁面後點擊About菜單後界面效果圖如下所示:
2 後端接口實現
2.1 form表單查詢接口實現
控制層代碼
MybatisController.java
@GetMapping("/queryUsers")
public ServiceResponse<List<UserTO>> searchUsers(@RequestParam("userAccount") String userAccount,
@RequestParam("nickName") String nickName,
@RequestParam("deptNo") Integer deptNo,
@RequestParam("startDate") String startDate,
@RequestParam("endDate") String endDate){
UserForm userForm = new UserForm(userAccount,null,nickName,deptNo,null,startDate,endDate);
return userService.searchUsers(userForm);
}
service層代碼
IUserService.java
ServiceResponse<List<UserTO>> searchUsers(UserForm userForm);
UserService.java
@Override
public ServiceResponse<List<UserTO>> searchUsers(UserForm userForm) {
logger.info("userForm={}", JSONArray.toJSONString(userForm));
ServiceResponse<List<UserTO>> response = new ServiceResponse<>();
try {
List<UserTO> users = userBusiness.searchUsers(userForm);
response.setStatus(200);
response.setMessage("ok");
response.setData(users);
} catch (Exception e) {
logger.error("searchUsers failed",e);
response.setStatus(500);
response.setMessage("inner server error, caused by: "+e.getMessage());
}
return response;
}
Business層代碼:
IUserBusiness.java
List<UserTO> searchUsers(UserForm userForm) throws Exception;
UserBusiness.java
@Override`在這裏插入代碼片`
public List<UserTO> searchUsers(UserForm userForm) throws Exception {
List<UserTO> users = userDao.searchUsers(userForm);
return users;
}
Dao層代碼:
IUserDao.java
List<UserTO> searchUsers(UserForm userForm);
IUserDao.xml
<select id="searchUsers" parameterType="com.example.mybatis.form.UserForm" resultType="com.example.mybatis.model.UserTO">
select t.id,t.user_account,t.password,t.nick_name,t.dept_no,
d.dept_name,t.phone_num, t.birth_day,t.updated_by, t.updated_time
from userinfo t
inner join dept d on t.dept_no=d.dept_no
<if test="deptNo!=null and deptNo!=-1">
and d.dept_no = #{deptNo,jdbcType=INTEGER}
</if>
<where>
<if test="userAccount!=null and userAccount!='' ">
and t.user_account like #{userAccount,jdbcType=VARCHAR} || '%'
</if>
<if test="nickName!=null and nickName!=''">
and t.nick_name like #{nickName,jdbcType=VARCHAR} || '%'
</if>
<if test="startDate!=null and startDate!=''">
and t.updated_time <![CDATA[>=]]> #{startDate,jdbcType=VARCHAR}
</if>
<if test="endDate!=null and endDate!=''">
and t.updated_time <![CDATA[<=]]> #{endDate,jdbcType=VARCHAR}
</if>
</where>
</select>
2.2 帶表單參數查詢的Excel導出接口實現
上兩篇博客中關於導出導入文件的配置信息都是從xml文件中讀取的,本次採用在實體類中添加註解實現讀取列頭和映射的實體類等excel導出接口的元數據信息
(1) 新建兩個註解類ExcelSheet和ExcelColumn,它們分別作用在實體類和實體類的屬性上,具體代碼如下:
ExcelSheet.java
package com.example.mybatis.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcelSheet {
@AliasFor(attribute = "value")
String name() default "";
@AliasFor(attribute = "name")
String value() default "";
//全類路徑名
String voClass() default "";
}
ExcelColumn.java
package com.example.mybatis.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcelColumn {
@AliasFor(attribute = "value")
String name() default "";
@AliasFor(attribute = "name")
String value() default "";
String displayName() default "";
String type() default "";
String format() default "";
int width() default 0;
int order() default 0;
}
(2) 將以上兩個註解添加到UserTO實體類上, 添加註解後的代碼如下:
package com.example.mybatis.model;
import com.example.mybatis.annotation.ExcelColumn;
import com.example.mybatis.annotation.ExcelSheet;
import java.io.Serializable;
@ExcelSheet(value="userInfo",voClass = "com.example.mybatis.model.UserTO")
public class UserTO implements Serializable {
@ExcelColumn(name = "id", displayName = "ID",type = "Long", width = 50, order = 0)
private Long id;
@ExcelColumn(name="deptNo", displayName = "部門編號", type="Integer", width=80, order=4)
private Integer deptNo;
@ExcelColumn(name="deptName", displayName = "部門名稱", type="String", width=120, order=5)
private String deptName;
@ExcelColumn(name="userAccount", displayName = "用戶賬號", type="String", width=120, order=1)
private String userAccount;
@ExcelColumn(name="password", displayName = "用戶密碼", type="String", width=120, order=2)
private String password;
@ExcelColumn(name="nickName", displayName = "用戶名稱", type="String", width=150, order=3 )
private String nickName;
@ExcelColumn(name="emailAddress",displayName = "郵箱地址", type="String", width=180, order=6)
private String emailAddress;
@ExcelColumn(name="birthDay",displayName="出生日期", type="Date", width=150, order=7 )
private String birthDay;
@ExcelColumn(name="phoneNum", displayName = "手機號碼", type="String", width=150, order = 8)
private String phoneNum;
@ExcelColumn(name="updatedBy", displayName = "更新人", type="String", width=120, order = 9)
private String updatedBy;
@ExcelColumn(name="updatedTime", displayName = "更新時間", type="Date", width = 180, order = 10)
private String updatedTime;
//此處省略setter和getter方法
}
(3) 編輯讀取註解配置的元數據工具類
AnnotationSheetUtil.java
package com.example.mybatis.utils;
import com.example.mybatis.annotation.ExcelColumn;
import com.example.mybatis.annotation.ExcelSheet;
import com.example.mybatis.model.ColumnInfo;
import com.example.mybatis.model.SheetInfo;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.List;
public class AnnotationSheetUtil {
public static SheetInfo initSheetInfo(Class clazz){
SheetInfo sheetInfo=null;
if(clazz.isAnnotationPresent(ExcelSheet.class)){
Annotation annotation = clazz.getDeclaredAnnotation(ExcelSheet.class);
ExcelSheet sheetAnnotation = (ExcelSheet) annotation;
String sheetName = sheetAnnotation.name();
if("".equals(sheetName)){
sheetName = sheetAnnotation.value();
}
if("".equals(sheetName)){
sheetName = clazz.getSimpleName();
}
String voClass = sheetAnnotation.voClass();
if("".equals(voClass)){
voClass = clazz.getName();
}
sheetInfo = new SheetInfo(sheetName,voClass);
List<ColumnInfo> columnInfos = sheetInfo.getColumns();
Field[] fields = clazz.getDeclaredFields();
for(Field field: fields){
if(field.isAnnotationPresent(ExcelColumn.class)){
Annotation columnAnnotation = field.getDeclaredAnnotation(ExcelColumn.class);
ExcelColumn excelColumnAnnotation = (ExcelColumn) columnAnnotation;
String propName = excelColumnAnnotation.name();
if("".equals(propName)){
propName = field.getName();
}
String displayName = excelColumnAnnotation.displayName();
String type = excelColumnAnnotation.type();
Integer width = excelColumnAnnotation.width();
ColumnInfo columnInfo = new ColumnInfo(propName,displayName,type,width);
String format = excelColumnAnnotation.format();
if(!"".equals(format)){
columnInfo.setFormat(format);
}
Integer order = excelColumnAnnotation.order();
columnInfo.setOrder(order);
columnInfos.add(columnInfo);
}
}
columnInfos.sort(Comparator.comparing(item->{
return item.getOrder();
}));
}
return sheetInfo;
}
}
(4) Controller層實現
ExcelController.java
@PostMapping("export/searchExcel")
public void exportSearchExcel(UserForm form, HttpServletResponse response){
logger.info("form={}", JSONArray.toJSONString(form));
// response.setHeader("Content-Type","application/octet-stream");
if(searchUserSheet==null){
searchUserSheet = AnnotationSheetUtil.initSheetInfo(UserTO.class);
}
excelService.exportSearchSheet(form,searchUserSheet,response);
}
(5) Service層實現
IExcelService.java
void exportSearchSheet(UserForm form, SheetInfo sheetInfo,HttpServletResponse response);
ExcelService.java
@Override
public void exportSearchSheet(UserForm form, SheetInfo sheetInfo, HttpServletResponse response) {
String sheetName = sheetInfo.getName();
Date now = new Date();
String dateTime = sdf.format(now);
String fileName = sheetName+dateTime+".xlsx";
try {
response.setHeader("Content-Disposition","attachment;filename="+URLEncoder.encode(fileName,"UTF-8"));
List<UserTO> userList = userBusiness.searchUsers(form);
OutputStream os = response.getOutputStream();
//調用工具類寫Excel
ExcelReadWriteUtil.writeSingleExcel(sheetInfo,userList,os);
logger.info("export single excel sheet success!");
} catch (Exception e) {
logger.error("export single excel sheet failed!",e);
throw new RuntimeException("export single excel sheet failed,caused by: "+e.getMessage());
}
}
(6) ExcelReadWriteUtil.writeSingleExcel(SheetInfo sheetInfo,List<?> dataList,OutputStream os)靜態方法的實現
ExcelReadWriteUtil.java
public static void writeSingleExcel(SheetInfo sheetInfo,List<?> dataList,OutputStream os) throws Exception{
XSSFWorkbook workbook = new XSSFWorkbook();
String sheetName = sheetInfo.getName();
String voClass = sheetInfo.getVoClass();
XSSFSheet sheet = workbook.createSheet(sheetName);
Row headRow = sheet.createRow(0);
//設置行高
// headRow.setHeightInPoints(256*30);
//設置列頭
List<ColumnInfo> columnInfos = sheetInfo.getColumns();
for(int i=0;i<columnInfos.size();i++){
Cell cell = headRow.createCell(i,CellType.STRING);
ColumnInfo columnInfo = columnInfos.get(i);
// sheet.setColumnWidth(i,columnInfo.getWidth());
cell.setCellValue(columnInfo.getDisplayName());
}
//設置列主體
Class clazz = Class.forName(voClass);
for(int i=0;i<dataList.size();i++){
Object item = dataList.get(i);
Row contentRow = sheet.createRow(i+1);
// contentRow.setHeightInPoints(256*26);
for(int j=0;j<columnInfos.size();j++){
ColumnInfo columnInfo = columnInfos.get(j);
// Integer width = columnInfo.getWidth();
String name = columnInfo.getName();
String getFieldName = "get"+upCaseFirstChar(name);
Method method = clazz.getDeclaredMethod(getFieldName,null);
Object value = method.invoke(item,null);
String type = columnInfo.getType();
switch (type){
case "Integer":
Integer intVal = (Integer) value;
Cell cell = contentRow.createCell(j,CellType.NUMERIC);
double val = Double.parseDouble(String.valueOf(intVal));
cell.setCellValue(val);
break;
case "Long":
Long longVal = (Long) value;
Cell cell1 = contentRow.createCell(j,CellType.NUMERIC);
double val1 = Double.parseDouble(String.valueOf(longVal));
cell1.setCellValue(val1);
break;
case "Date":
String dateStr = (String) value;
Cell cell2 = contentRow.createCell(j,CellType.STRING);
cell2.setCellValue(dateStr);
default:
String strVal = (String) value;
Cell cell3 = contentRow.createCell(j,CellType.STRING);
cell3.setCellValue(strVal);
break;
}
}
}
//將工作簿寫到輸出流中
workbook.write(os);
workbook.close();
}
保存所有修改過的代碼後,在IDEA中以Debug模式啓動spring-boot項目,內嵌的tomcat啓動成後在控制檯中顯示如下日誌信息時表示項目啓動成功:
main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path '/springboot'
2020-01-08 22:13:18.352 INFO 916 --- [ main] com.example.mybatis.MybatisApplication : Started MybatisApplication in 5.954 seconds (JVM running for 8.894)
3 功能測試
此時可以返回前端界面點擊查詢按鈕和導出按鈕測試web頁面功能了
(1)當form表單中創建時間的開始時間和結束時間分別爲2019-12-20 00:00:00和2019-12-24 00:00:00,部門選項爲所有部門,用戶賬號和中文名爲空時點擊查詢按鈕查詢符合條件的數據,結果如下圖所示:
(2)點擊導出按鈕,瀏覽器左下方會顯示下載成功的excel文件,下載成功後打開文件部門內容如下圖所示:
4 技術棧總結
本文利用springboot作爲後端架構,vue-cli 3腳手架工具和element-ui庫開發了一個可視化的
頁面,實現了數據查詢和excel文件導出導入功能。
- 後端利用自定義註解實現讀取excel文件導出數據轉換實現類元數據信息;
- 利用apache poi api和java 反射知識自定義Excel讀寫工具類方便開發人員簡化Excel導入和導出功能的開發;
- 前端利用vue整合element-ui實現了簡單的按鈕查詢和導出功能,使用封裝後axios組件發送http請求,實現與服務端的數據交互。
由於本人平時忙於工作中的業務開發,而鮮少進行技術講解,總感覺自己總結知識和技術的水平有限,很多時候也是深感自己的表達和寫作水平有限。因而有錯誤和缺失的地方還請同行不吝指教!本博客相關的前後端代碼本人已悉數上傳到自己的個人碼雲庫,以下是gitee地址
後端springboot項目地址: https://gitee.com/heshengfu1211/mybatisProjectDemo/tree/master
前端vue-cli項目地址:https://gitee.com/heshengfu1211/vue-cli-project/tree/master
對本人這篇博客技術感興趣的讀者可以克隆下來學習參考。