前端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;
// 创建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"><slot name="custom" v-if="isCustom"></slot><a-button ghost type="primary" @click="startExport" v-else>导出excel</a-button><a-modalv-if="visible"v-model="visible":title="modelTitle":maskClosable="false":closable="false"><template #footer><a-buttontype="primary"ghostv-if="isAbnormal":loading="btnLoading"@click="startExport">重新导出</a-button><a-buttontype="primary"ghostv-if="isAbnormal":loading="btnLoading"@click="getTableData">继续导出</a-button><a-button :loading="btnLoading" @click="handleClose"> 关闭 </a-button></template><a-progress:percent="percent":status="progressStatus"class="progress"/></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为默认buttonisCustom: {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;});},//导出数据表的表头的codetableCode() {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,});},//导出所有数据到一个excelexportExcel(fileName, sheetName) {// 文件名称const filename = fileName;//Excel第一个sheet的名称const ws_name = sheetName;// 创建wokbookconst 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":asyncDataApi="asyncDataApi":isCustom="isCustom":listQuery="listQuery"ref="export":filterFunction="handleDateFilter"><template #custom><!-- <a-button type="primary" @click="handleClick">导出excel</a-button> --><a-dropdown-button>Dropdown<a-menu slot="overlay" @click="handleMenuClick"><a-menu-item key="1"><a-icon type="user" />1st menu item</a-menu-item><a-menu-item key="2"><a-icon type="user" />2nd menu item</a-menu-item><a-menu-item key="3"><a-icon type="user" />3rd item</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":tableTitleData="titles"ref="export"><a-button type="primary" @click="handleClick">导出excel</a-button></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。Booleanfalse
modelTitle模态框标题String"导出excel"

multiFileSize

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

FAQ

filterFunction怎么使用

<template><ExportExcelComponent...:isCustom = "true":asyncDataApi="asyncDataApi":tableTitleData="titles"ref="export":filterFunction="handleDateFilter"><a-button type="primary" @click="handleClick">导出excel</a-button></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}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/207516.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于锁的粒度问题——面试

锁的粒度划分主要有三种&#xff1a;表级锁、页级锁和行锁 1.表级锁&#xff1a; 对整张表加锁&#xff0c;粒度最大&#xff0c;加锁的并发度最低&#xff0c;会导致其他事务无法访问该表&#xff0c;只有当前事务提交或者回滚后才能释放锁。表级锁适用于对表进行全表操作的场…

DeepIn,UOS统信专业版安装运行Java,JavaFx程序

因为要适配国产统信UOS系统&#xff0c;要求JavaFx程序能简便双击运行&#xff0c;由于网上UOS开发相关文章少&#xff0c;多数文章没用&#xff0c;因此花了不少时间&#xff0c;踩了不少坑&#xff0c;下面记录一些遇到的问题&#xff0c;我的程序环境是jdk1.8&#xff0c;为…

【K8s】Kubernetes CRD 介绍(控制器)

文章目录 CRD 概述1. 操作CRD1.1 创建 CRD1.2 操作 CRD 2. 其他笔记2.1 Kubectl 发现机制2.2 校验 CR2.3 简称和属性 3. 架构设计3.1 控制器概览 参考 CRD 概述 CR&#xff08;Custom Resource&#xff09;其实就是在 Kubernetes 中定义一个自己的资源类型&#xff0c;是一个具…

如何为 3D 模型制作纹理的最佳方法

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 您可以通过不同的方式为 3D 模型创建 3D 纹理。下面我们将介绍为 3D …

《opencv实用探索·十四》VideoCapture播放视频和视像头调用

1、VideoCapture播放视频 #include <opencv2/opencv.hpp> #include <iostream>using namespace std; using namespace cv;int main() {// 定义相关VideoCapture对象VideoCapture capture;// 打开视频文件capture.open("1.avi");// 判断视频流读取是否正…

Python os模块及用法

os 模块代表了程序所在的操作系统&#xff0c;主要用于获取程序运行所在操作系统的相关信息。 在 Python 的交互式解释器中先导入 os 模块&#xff0c;然后输入 os.__all__ 命令&#xff08;__all__ 变量代表了该模块开放的公开接口&#xff09;&#xff0c;即可看到该模块所包…

Linux DataEase数据可视化分析工具本地部署与远程访问

文章目录 前言1. 安装DataEase2. 本地访问测试3. 安装 cpolar内网穿透软件4. 配置DataEase公网访问地址5. 公网远程访问Data Ease6. 固定Data Ease公网地址 前言 DataEase 是开源的数据可视化分析工具&#xff0c;帮助用户快速分析数据并洞察业务趋势&#xff0c;从而实现业务…

ExecutorService介绍

参考&#xff1a;https://blog.csdn.net/fwt336/article/details/81530581 前言 在开发中为了提高系统的响应速度和处理能力会使用到多线程&#xff0c;但线程的创建和释放&#xff0c;需要占用不小的内存和资源。如果每次需要使用线程时&#xff0c;都new 一个Thread的话&…

【LeetCode】2723. 两个 Promise 对象相加

两个 Promise 对象相加 题目题解 题目 给定两个 promise 对象 promise1 和 promise2&#xff0c;返回一个新的 promise。promise1 和 promise2 都会被解析为一个数字。返回的 Promise 应该解析为这两个数字的和。 示例 1&#xff1a; 输入&#xff1a; promise1 new Promise…

geoserver根据属性字段值设置不同的颜色

<?xml version"1.0" encoding"UTF-8"?> <StyledLayerDescriptor xmlns"http://www.opengis.net/sld" xmlns:xlink"http://www.w3.org/1999/xlink" xmlns:ogc"http://www.opengis.net/ogc" xmlns:xsi"http:/…

中国聚羟基脂肪酸酯(PHA)市场评估与投资战略报告(2024版)

内容简介&#xff1a; PHA英文名称为 Polyhydroxyalkanoates&#xff0c;是近20多年迅速发展起来的&#xff0c;很多天然原料合成的一种聚酯。PHA是生物基生物降解材料&#xff0c;具有良好的生物相容性和可加工性。防止水汽的穿透是保鲜包装中的重要指标&#xff0c;PHA 有良…

os.walk()遍历文件夹/文件

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

P3 Qt 控件 —— pushButton

前言 &#x1f3ac; 个人主页&#xff1a;ChenPi &#x1f43b;推荐专栏1: 《C_ChenPi的博客-CSDN博客》✨✨✨ &#x1f525; 推荐专栏2: 《Linux C应用编程&#xff08;概念类&#xff09;_ChenPi的博客-CSDN博客》✨✨✨ &#x1f33a;本篇简介 &#xff1a;这一章我们学一…

Python evalml 库:自动化机器学习的新前景

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在机器学习领域&#xff0c;evalml 库崭露头角&#xff0c;为开发者提供了一个强大而高效的自动化机器学习框架。本文将深入介绍 evalml 的核心功能、使用方法以及在实际项目中的应用。通过详实的示例代码&#…

前端高频面试题大全-面试必看

内容较多&#xff0c;建议查看目录&#xff0c;方便食用 高频 React和Vue的区别 通常解法&#xff1a;vue是采用指令结合vue-loader实现构件用户界面的渐进式框架&#xff0c;React是采用JSX构件用户界面的组件化开发 详细解法&#xff1a;在渲染界面的时候DOM操作是昂贵的&…

【Linux系统编程】项目自动化构建工具make/Makefile

介绍&#xff1a; make和Makefile是用于编译和构建C/C程序的工具和文件。Makefile是一个文本文件&#xff0c;其中包含了编译和构建程序所需的规则和指令。它告诉make工具如何根据源代码文件生成可执行文件&#xff0c;里面保存的是依赖关系和依赖方法。make是一个命令行工具&a…

智汇恒星科技|控乐屋.全宅智能冠军代言来啦, 智慧家居千亿蓝海

随着5G、大数据、云计算、物联网等技术的发展&#xff0c;智能化正覆盖人们生活的方方面面&#xff0c;全屋智能的出现为“一键式”智能家居生活享受提供无限可能。近年来智能家居行业总体规模增长迅速&#xff0c;数据显示&#xff0c;2022年中国智能家居行业市场规模约为6200…

Java内部类

文章目录 什么是 Java 中的内部类&#xff1f;有哪些类型的内部类&#xff1f;匿名内部类局部内部类&#xff08;定义在方法中的类&#xff09;局部内部类静态内部类 Java 类中不仅可以定义变量和方法&#xff0c;还可以定义类&#xff0c;这样定义在类内部的类就被称为内部类。…

Java期末复习题之封装

点击返回标题->23年Java期末复习-CSDN博客 第1题. 定义一个类Person,定义name和age私有属性&#xff0c;定义有参的构造方法对name和age进行初始化。在测试类中创建该类的2个对象&#xff0c;姓名、年龄分别为lili、19和lucy、20&#xff0c;在屏幕打印出2个对象的姓名和年龄…

2024年江苏省职业院校技能大赛信息安全管理与评估 理论题(样卷)

2024年江苏省职业院校技能大赛信息安全管理与评估 理论题&#xff08;样卷&#xff09; 理论技能与职业素养&#xff08;100分&#xff09; 2024年江苏省职业院校技能大赛&#xff08;高职学生组&#xff09; 模块三“信息安全管理与评估”理论技能 【注意事项】 Geek极安云…