行业知识
前端Excel导出实用方案(完整源码,可直接应用)
目录
前言:
技术选型:
主要功能点:
核心代码:
完整代码:
开发文档
前言:
在前后端分离开发为主流的时代,很多时候,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万条数据自动拆分)
- 自定义过滤函数
- 各种标题自定义
- 数据排序
- 支持大数据量导出
核心代码:
// 文件名称const filename = fileName;//Excel第一个sheet的名称const ws_name = sheetName;// 创建sheetconst ws = XLSX.utils.aoa_to_sheet([this.tableTitle]);//添加数据XLSX.utils.sheet_add_json(ws, apiData, { skipHeader: true, origin:origin});// 创建wokbookconst wb = XLSX.utils.book_new();// 将数据添加到工作薄XLSX.utils.book_append_sheet(wb, ws, ws_name);// 导出文件XLSX.writeFile(wb, filename);完整代码:
安装sheetjs
npm i xlsx
全局导出组件代码:
ExportExcelComponent.vue
导出excel 重新导出 继续导出 关闭 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 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); }, },};调用示例:
App.vue
测试表格导出
Dropdown 1st menu item 2nd menu item 3rd item 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 { // 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, },};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开发文档
调用方式:
如果不采用自定义dom的话,直接点击默认的按钮可直接导出表格数据; 如果采用自定义dom的话,通过ref实例调用子组件内的startExport方法,执行导出操作
导出excel import { asyncDataApi } from '@/api/member' export default{ data(){ return:{ titles:[] asyncDataApi, } } methods:{ handleClick(){ this.$refs.export.startExport(); } } }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怎么使用
导出excel 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(); } } }导出表格数据为空是什么情况?
因为导出的表格数据的顺序和标题的顺序并不一定是一致的,所以在组件内部做了映射排序,一定要确保传入的标题数据在调用导出接口之前执行。如果传递的标题有误,在进行映射的时候,这时标题和表格数据并不匹配,那么就会出现数据映射为空的情况
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 }}
上一篇:cs怎么选服务器2?