前端Excel导出实用方案(完整源码,可直接应用)

零凌林 2024-08-01 08:03:01 阅读 67

目录

前言:

技术选型:

主要功能点:

核心代码:

完整代码:

安装sheetjs

全局导出组件代码:

调用示例:

mock数据:

开发文档

调用方式:

API

FAQ


前言:

在前后端分离开发为主流的时代,很多时候,excel导出已不再由后端主导,而是把导出的操作移交到了前端。本文在全局导出组件封装上,保持了高度的扩展性,无论大家用的是element组件库还是antd vue的组件库或者其他的组件库,都容易进行更换。

技术选型:

vue + antd vue + sheetjs

前端导出excel导出,需借助第三方插件,目前两款导出最为主流。

一款是sheetjs,优点支持多种excel格式,但是官方文档全是英文

SheetJS Community Edition | SheetJS Community Edition

一款是exceljs,优点是中文文档很全,缺点是导出格式受限,仅支持部分格式

https://github.com/exceljs/exceljs/blob/master/README_zh.md

因公司业务需要,用户需支持多种excel的格式,所以本文笔者主要针对sheetjs进行封装调用。

主要功能点:

自定义dom拆分成多张表导出(默认超过1万条数据自动拆分)自定义过滤函数各种标题自定义数据排序支持大数据量导出

核心代码:

<code>// 文件名称

const filename = fileName;

//Excel第一个sheet的名称

const ws_name = sheetName;

// 创建sheet

const ws = XLSX.utils.aoa_to_sheet([this.tableTitle]);

//添加数据

XLSX.utils.sheet_add_json(ws, apiData, {

skipHeader: true,

origin:origin

});

// 创建wokbook

const wb = XLSX.utils.book_new();

// 将数据添加到工作薄

XLSX.utils.book_append_sheet(wb, ws, ws_name);

// 导出文件

XLSX.writeFile(wb, filename);

完整代码:

安装sheetjs

npm i xlsx

全局导出组件代码:

ExportExcelComponent.vue

<template>

<div id="excel-export">code>

<slot name="custom" v-if="isCustom"></slot>code>

<a-button ghost type="primary" @click="startExport" v-else>code>

导出excel

</a-button>

<a-modal

v-if="visible"code>

v-model="visible"code>

:title="modelTitle"code>

:maskClosable="false"code>

:closable="false"code>

>

<template #footer>

<a-button

type="primary"code>

ghost

v-if="isAbnormal"code>

:loading="btnLoading"code>

@click="startExport"code>

>

重新导出

</a-button>

<a-button

type="primary"code>

ghost

v-if="isAbnormal"code>

:loading="btnLoading"code>

@click="getTableData"code>

>

继续导出

</a-button>

<a-button :loading="btnLoading" @click="handleClose"> 关闭 </a-button>code>

</template>

<a-progress

:percent="percent"code>

:status="progressStatus"code>

class="progress"code>

/>

</a-modal>

</div>

</template>

<script>

import * as XLSX from "xlsx";

export default {

props: {

//自定义过滤函数

filterFunction: {

type: Function,

default: null,

},

//sheet名

ws_name: {

type: String,

default: "Sheet",

},

//导出的excel的表名

filename: {

type: String,

default: "Excel" + new Date().getTime(),

},

//拆分成每个表多少条数据,需要搭配isSplit属性一起使用

multiFileSize: {

type: Number,

default: 10e3,

},

//模态框标题

modelTitle: {

type: String,

default: "导出excel",

},

//是否自定义dom,如果采用插槽,需要开启该属性,否则dom为默认button

isCustom: {

type: Boolean,

default: false,

},

// 导出的数据表的表头

tableTitleData: {

type: Array,

required: true,

default: () => [],

},

//请求数据的api函数

asyncDataApi: {

type: Function,

default: () => {},

},

//请求参数

listQuery: {

type: Object,

default: () => ({}),

},

},

data() {

return {

ws: null,

isAbnormal: false,

btnLoading: false,

progressStatus: "active",

visible: false,

percent: 0,

tableData: [],

currentPage: 1,

multiFileNum: 0,

};

},

computed: {

// 导出的数据表的表头

tableTitle() {

return this.tableTitleData.map((item) => {

return item.title;

});

},

//导出数据表的表头的code

tableCode() {

return this.tableTitleData.map((item) => {

return item.code;

});

},

},

watch: {

percent: {

handler(newVal) {

if (newVal > 100) {

this.progressStatus = "success";

setTimeout(() => {

this.handleClose();

}, 500);

}

},

},

},

methods: {

//按照指定的title顺序映射排序数组对象

sortData(data, tit_code) {

const newData = [];

data.forEach((item) => {

const newObj = {};

tit_code.forEach((v) => {

newObj[v] = item[v] || "";

});

newData.push(newObj);

});

return newData;

},

handleClose() {

console.log("close");

this.resetExport();

this.visible = false;

},

resetExport() {

this.percent = 0;

this.progressStatus = "active";

this.isAbnormal = false;

this.tableData = [];

this.currentPage = 1;

this.multiFileNum = 0;

this.ws = XLSX.utils.aoa_to_sheet([this.tableTitle]);

},

//获取进度条百分比

getPersent(res) {

const persent_num =

((res.paginator.currentPage * res.paginator.size) /

res.paginator.total) *

100;

this.percent = parseInt(persent_num) - 1;

},

//异常处理

handleAbnormal() {

this.btnLoading = false;

this.progressStatus = "exception";

this.isAbnormal = true;

},

async startExport() {

if (!this.asyncDataApi) {

return new Promise(new Error("asyncDataApi is required"));

}

this.resetExport();

await this.getTableData();

},

//请求导出的数据和标题

async getTableData() {

this.visible = true;

this.btnLoading = true;

this.isAbnormal = false;

try {

const res = await this.asyncDataApi({

...this.listQuery,

page: this.currentPage,

});

if (res.code !== 200) {

this.handleAbnormal();

this.$message.error(res.message || this.t("requestException"));

return;

}

let apiData = res.data;

apiData = this.sortData(apiData, this.tableCode);

if (this.filterFunction) {

apiData = this.filterFunction(apiData);

}

apiData = apiData.map((item) => Object.values(item));

this.addSheetData(apiData, res);

this.currentPage = res.paginator.currentPage + 1;

console.log("res", res);

this.getPersent(res);

const isSplit =

res.paginator.currentPage * res.paginator.size >=

this.multiFileSize * (this.multiFileNum + 1);

if (isSplit) {

this.splitExport();

}

if (res.paginator.currentPage < res.paginator.page) {

this.getTableData();

return;

}

//当数据不满足拆分数量时触发

this.hadnleOneExport(res);

this.percent += 2;

this.btnLoading = false;

this.$message.success("导出成功");

} catch (error) {

console.log(error);

this.$message.error("网络错误,请稍后再试");

this.handleAbnormal();

}

},

//当数据不满足拆分数量时触发

hadnleOneExport(res) {

if (

this.multiFileNum &&

res.paginator.total > this.multiFileNum * this.multiFileSize

) {

this.multiFileNum += 1;

this.exportExcel(

this.filename + this.multiFileNum + ".xlsx",

this.ws_name + this.multiFileNum

);

} else if (!this.multiFileNum) {

this.exportExcel(this.filename + ".xlsx", this.ws_name);

}

},

//拆分成多个excel导出

splitExport() {

this.multiFileNum += 1;

this.exportExcel(

this.filename + this.multiFileNum + ".xlsx",

this.ws_name + this.multiFileNum

);

//重置表格

this.ws = XLSX.utils.aoa_to_sheet([this.tableTitle]);

},

addSheetData(apiData, res) {

//添加数据到表格 origin为每次添加数据从第几行开始

XLSX.utils.sheet_add_json(this.ws, apiData, {

skipHeader: true,

origin:

(this.currentPage - 1) * res.paginator.size -

this.multiFileSize * this.multiFileNum +

1,

});

},

//导出所有数据到一个excel

exportExcel(fileName, sheetName) {

// 文件名称

const filename = fileName;

//Excel第一个sheet的名称

const ws_name = sheetName;

// 创建wokbook

const wb = XLSX.utils.book_new();

// 将数据添加到工作薄

XLSX.utils.book_append_sheet(wb, this.ws, ws_name);

// 导出文件

XLSX.writeFile(wb, filename);

},

},

};

</script>

调用示例:

App.vue

<template>

<div>

<h1>测试表格导出</h1>

<div>

<ExportExcelComponent

:tableTitleData="title"code>

:asyncDataApi="asyncDataApi"code>

:isCustom="isCustom"code>

:listQuery="listQuery"code>

ref="export"code>

:filterFunction="handleDateFilter"code>

>

<template #custom>

<!-- <a-button type="primary" @click="handleClick">导出excel</a-button> -->code>

<a-dropdown-button>

Dropdown

<a-menu slot="overlay" @click="handleMenuClick">code>

<a-menu-item key="1">code>

<a-icon type="user" />1st menu itemcode>

</a-menu-item>

<a-menu-item key="2">code>

<a-icon type="user" />2nd menu itemcode>

</a-menu-item>

<a-menu-item key="3">code>

<a-icon type="user" />3rd itemcode>

</a-menu-item>

</a-menu>

</a-dropdown-button>

</template>

</ExportExcelComponent>

</div>

</div>

</template>

<script>

import ExportExcelComponent from "./ExportExcelComponent/ExportExcelComponent.vue";

import { asyncDataApi } from "./request";

import dayjs from "dayjs";

export default {

data() {

return {

listQuery: {

name: "yyy",

age: 18,

},

isCustom: true,

asyncDataApi: null,

title: [

{ code: "id", title: "序号" },

{ code: "hobby", title: "爱好" },

{ code: "name", title: "姓名" },

{ code: "age", title: "年龄" },

// { code: "hobby", title: "爱好" },

{ code: "sex", title: "性别" },

{ code: "address", title: "地址" },

{ code: "birthday", title: "生日" },

{ code: "createTime", title: "创建时间" },

{ code: "updateTime", title: "更新时间" },

{ code: "remark", title: "备注" },

{ code: "status", title: "状态" },

],

};

},

methods: {

handleDateFilter(data) {

const res = data.reduce((pre, cur) => {

for (let i in cur) {

if (i === "createTime") {

cur[i] = dayjs(cur[i] * 1000).format("YYYY-MM-DD HH:mm:ss");

}

}

pre.push(cur);

return pre;

}, []);

return res;

},

async handleMenuClick(val) {

// const titleNewData = [];

// for (let i = 1; i < 500; i++) {

// this.title.forEach((item) => {

// titleNewData.push({ code: item.code + i, title: item.title + i });

// });

// }

// this.title = titleNewData;

console.log("点击了导出excel", val);

await (this.asyncDataApi = asyncDataApi);

this.$refs.export.startExport();

},

// async handleClick() {

// console.log("点击了导出excel");

// await (this.asyncDataApi = asyncDataApi);

// this.$refs.export.startExport();

// },

},

components: {

ExportExcelComponent,

},

};

</script>

mock数据:

request.js

const asyncDataApi = (listquery) => {

console.log("params", listquery);

// 模拟异步请求接口

return new Promise((resolve, reject) => {

setTimeout(() => {

const data = [];

for (let i = listquery.page * 100; i < (listquery.page + 1) * 100; i++) {

const obj = {

id: i - 99,

name: "姓名" + i,

age: 20 + i,

hobby:

"赵客缦胡缨,吴钩霜雪明。银鞍照白马,飒沓如流星。十步杀一人,千里不留行。事了拂衣去,深藏身与名。闲过信陵饮,脱剑膝前横。将炙啖朱亥,持觞劝侯嬴。" +

i,

sex: "男" + i,

birthday: "2020-01-01",

createTime: "1701155392",

updateTime: "2020-01-01",

remark: "备注" + i,

status: "1" + i,

};

// let newObj = {};

// for (var a = 1; a < 500; a++) {

// for (let k in obj) {

// newObj[k + a] = obj[k];

// }

// }

// data.push(newObj);

data.push(obj);

}

resolve({

data,

code: 200,

msg: "请求成功",

paginator: {

page: 1000,

size: 100,

total: 100000,

currentPage: listquery.page,

},

});

}, 100);

});

};

export { asyncDataApi };

开发文档

调用方式:

如果不采用自定义dom的话,直接点击默认的按钮可直接导出表格数据; 如果采用自定义dom的话,通过ref实例调用子组件内的startExport方法,执行导出操作

<template>

<ExportExcelComponent

...

:isCustom = "true"

:asyncDataApi="asyncDataApi"code>

:tableTitleData="titles"code>

ref="export"code>

>

<template #custom>

<a-button type="primary" @click="handleClick">导出excel</a-button>code>

</template>

</ExportExcelComponent>

</template>

<script>

import { asyncDataApi } from '@/api/member'

export default{

data(){

return:{

titles:[]

asyncDataApi,

}

}

methods:{

handleClick(){

this.$refs.export.startExport();

}

}

}

</script>

API

属性如下

参数 说明 类型 默认值
listQuery 请求参数 Object {}
asyncDataApi 请求数据的api函数 Function 必传
tableTitleData 导出的数据表的表头 Array 必传
isCustom 是否自定义dom,如果采用插槽,需要开启该属性,否则dom为默认button;可以传递 v-slot:custom 来自定义 dom。 Boolean false
modelTitle 模态框标题 String "导出excel"

multiFileSize

拆分成每个表多少条数据,需要搭配isSplit属性一起使用 Number 10e3
filename 导出的excel的表名 String "Excel" + new Date().getTime()
ws_name sheet名 String "Sheet"
filterFunction 自定义过滤函数;可在业务层处理数据格式,如时间格式化等 Function(data) null

FAQ

filterFunction怎么使用

<template>

<ExportExcelComponent

...

:isCustom = "true"

:asyncDataApi="asyncDataApi"code>

:tableTitleData="titles"code>

ref="export"code>

   :filterFunction="handleDateFilter"code>

>

<template #custom>

<a-button type="primary" @click="handleClick">导出excel</a-button>code>

</template>

</ExportExcelComponent>

</template>

<script>

import { asyncDataApi } from '@/api/member'

export default{

data(){

return:{

titles:[]

asyncDataApi,

}

}

methods:{

     handleDateFilter(data) {

      const res = data.reduce((pre, cur) => {

       for (let i in cur) {

         if (i === "createTime") {

           cur[i] = dayjs(cur[i] * 1000).format("YYYY-MM-DD HH:mm:ss");

        }

      }

       pre.push(cur);

      return pre;

    }, []);

    return res;

    },

handleClick(){

this.$refs.export.startExport();

}

}

}

</script>

导出表格数据为空是什么情况?

因为导出的表格数据的顺序和标题的顺序并不一定是一致的,所以在组件内部做了映射排序,一定要确保传入的标题数据在调用导出接口之前执行。如果传递的标题有误,在进行映射的时候,这时标题和表格数据并不匹配,那么就会出现数据映射为空的情况

Promise Error:"asyncDataApi is required"

当传递给组件的后端api需要在点击dom后赋值再传递的时候,一定要确保在导入后端api之后再调用组件内的导出方法,否则因为后端api还没传递过去就调用,然后抛错或者导出异常

正确示例:

async handleClick() {

 await (this.asyncDataApi = asyncDataApi);

 this.$refs.export.getTableData();

},

后端导出表格api数据返回格式

因该组件为全局组件,方便以后复用,需与后端协商规定好数据导出的格式。以下为笔者的公司,与后端同事协商的数据格式。大家可根据自己公司需要,更改以上源码中后端返回值字段。

//后端返回数据结构

{

   "status": true,

   "data": [

      {...},

    {...},

  ],

   "paginator": {

       "currentPage": 1,

       "total": 200,

       "size": 20,

       "page": 10

  }

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。