【10分钟系列】手把手带你封装一个Excel文件上传&下载组件
组件亮点
支持Excel文件的选择上传、拖拽上传(导入Excel);
支持Excel文件的下载(导出为Excel);
需求与导入Excel
需求:导入下图的Excel文件,实现数据的批量添加
导入的本质:利用插件将Excel文件解析为两个数组,再将数组按照一定规则转换为后端需要的格式传给后端即可
第一步
下包 xlsx 是关键,解析、读取、生成Excel文件都靠它
npm i xlsx
第二步
准备上传专用页面,记得配置路由规则
<template>
<div>
<el-row type="flex" justify="center" align="middle" style="height:80px">
<el-col :span="19" style="margin:20px ">
<p>点击“导入Excel”按钮,切换到本组件(import.vue,即我们的上传页面。</p>
<p>但是需要配置路由规则,本组件是二级路由),然后在本组件中导入公共组件src/index.vue,这个就是上传用的组件</p>
</el-col>
</el-row>
<!-- 全局注册的上传Excel组件 -->
<UploadExcel :on-success="handleSuccess" />
<el-row type="flex" justify="center" align="middle" style="height:80px">
<el-button type="primary" @click="$router.push({path:'/employees'})">返回</el-button>
</el-row>
</div>
</template>
{
path: '/import',
name: 'import',
component: () => import('@/views/employees/components/import.vue'),
hidden: true,
meta: { title: '导入Excel' }
}
第三步
上传使用组件UploadExcel.vue,它来自vue-element-admin
让我们一起看看PanJiaChen大佬写了些啥
结构:
UploadExcel.vue的HTML结构其实很简单:一个隐藏的上传控件;一个包含文字提示和按钮的div
<template>
<div>
<input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
<div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
将Excel文件拖拽到此,或者
<el-button :loading="loading" style="margin-left:16px;" size="middle" type="primary" @click="handleUpload">
点我浏览文件
</el-button>
</div>
</div>
</template>
行为:
import XLSX from 'xlsx'
export default {
name: 'UploadExcel',
props: {
// 简写,接收父组件传入的函数
beforeUpload: Function,
onSuccess: Function
},
data() {
return {
loading: false,
// Excel中的数据
excelData: {
header: null, // 表头
results: null // 表头对应的单元格内容形成的数组
}
}
},
methods: {
// generateData()函数,用来提取表头和每行的数据
generateData({ header, results }) {
this.excelData.header = header
this.excelData.results = results
// 如果传来的onSuccess函数存在(不传函数就不存在),就将excelData变量的值传入函数并执行函数。
// onSuccess函数定义在父组件中,传过来的是函数地址,所以相当于在子组件中调用父组件中的函数
this.onSuccess && this.onSuccess(this.excelData)
// 等价于:
// if (this.onSuccess) {
// this.onSuccess(this.excelData)
// }
},
handleDrop(e) {
e.stopPropagation()
e.preventDefault()
if (this.loading) return
const files = e.dataTransfer.files
// 校验上传Excel文件的数量
if (files.length !== 1) {
this.$message.error('Only support uploading one file!')
return
}
const rawFile = files[0]
// 调用 isExcel 函数判断是否为正确格式
if (!this.isExcel(rawFile)) {
this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
return false
}
// 调用上传函数
this.upload(rawFile)
e.stopPropagation()
e.preventDefault()
},
// 在元素正在拖动到放置目标时触发该函数 或者 在拖动的元素进入到放置目标时触发该函数
handleDragover(e) {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
},
/* 点击<el-button>的回调 */
// 作用:触发隐藏<input>的点击事件
handleUpload() {
this.$refs['excel-upload-input'].click()
},
/* 点击隐藏的<input>的回调 */
// 作用:通过事件对象拿到 files 文件对象,这是个类数组,然后将0号位置的文件传入 upload 函数
handleClick(e) {
const files = e.target.files
const rawFile = files[0] // 仅使用0号位置的文件
if (!rawFile) return
this.upload(rawFile)
},
// 上传函数
upload(rawFile) {
this.$refs['excel-upload-input'].value = null // 修复无法选择同一Ecxel
if (!this.beforeUpload) {
// 如果父组件没有传入beforeUpload函数,就调用readerData()函数,读取文件
this.readerData(rawFile)
return
}
const before = this.beforeUpload(rawFile)
if (before) {
this.readerData(rawFile)
}
},
// 读取Excel文件中的数据
readerData(rawFile) {
this.loading = true
return new Promise((resolve, reject) => {
// 创建原生JS中的读取文件对象
const reader = new FileReader()
// 监听onload事件,并做一系列的操作
reader.onload = e => {
const data = e.target.result
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
// 调用 getHeaderRow 函数,获取Excel文件中的表头
const header = this.getHeaderRow(worksheet)
// 调用 XLSX插件的sheet_to_json 函数,获取Excel文件的表体
const results = XLSX.utils.sheet_to_json(worksheet)
// 调用generateData()函数,生成header数组和results数组
this.generateData({ header, results })
this.loading = false
resolve()
}
reader.readAsArrayBuffer(rawFile)
})
},
getHeaderRow(sheet) {
const headers = []
const range = XLSX.utils.decode_range(sheet['!ref'])
let C
const R = range.s.r
/* start in the first row */
for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
/* find the cell in the first row */
let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
headers.push(hdr)
}
return headers
},
isExcel(file) {
return /\.(xlsx|xls|csv)$/.test(file.name)
}
}
}
</script>
样式
<style scoped>
.excel-upload-input{
display: none;
z-index: -9999;
}
.drop{
border: 2px dashed #bbb;
width: 600px;
height: 160px;
line-height: 160px;
margin: 0 auto;
font-size: 24px;
border-radius: 5px;
text-align: center;
color: #bbb;
position: relative;
}
</style>
vue-element-admin的仓库地址:github.com/PanJiaChen/…
随后将该组件注册为全局公共组件,并到上传专用页面使用组件
提供一种基于webpack语法的自动批量注册全局组件的方法,注意该方法注册的组件必须有name属性,详见我的这篇文章
第四步
上一步,已将Excel的表头和单元格内容转换为了两个数组
header数组:['姓名', '手机号', '入职日期', '转正日期', '工号', '部门']
results数组中的成员:
{
姓名: "小张",
手机号: "13800000252",
入职日期: 44505,
转正日期: 44892,
工号: 9002
部门:"总裁办"
}
由于后端接口的字段都是英文,所以要将中文键转换为后端指定的英文键,然后正常发请求
export default {
name: 'Import',
data() {
return {
tableHeader: [],
tableResults: []
}
},
methods: {
async handleSuccess(data) {
this.tableHeader = data.header
this.tableResults = data.results
// 映射关系是mapInfo对象,它是转换的桥梁
const mapInfo = {
姓名: 'username',
手机号: 'mobile',
入职日期: 'timeOfEntry',
转正日期: 'correctionTime',
工号: 'workNumber',
聘用形式: 'formOfEmployment'
}
/* 第 1 步:将tableResults数组成员中的中文属性名转换为英文属性名,属性保持不变 */
// 获取所有属性名
var chiArr = Object.keys(mapInfo)
// 1.1 map遍历tableResults数组,item是它的成员---小对象(每一行的数据)
// 遍历目的:将其中的成员---小对象(每一行的数据),转换成---‘ 英文属性名:原属性值 ’
var newArr = this.tableResults.map((item) => {
// 1.2 空对象,之后向里面循环添加新属性(属性名:属性值)
var emptyObj = {}
// eachChi就是chiArr数组成员---中文字符串
// 1.3 遍历数组chiArr,将chiArr的成员作为属性名,向空对象emptyObj中添加新属性
chiArr.forEach((eachChi) => {
// 1.4 关联数组写法 拿到mapInfo对象中的属性值, 即mapInfo对象的属性值----英文
var eng = mapInfo[eachChi] // 获取英文属性值
if (eng === 'timeOfEntry' || eng === 'correctionTime') {
// 将Excel的时间转换为正确格式:使用new Date()将时间转换为标准时间,后端需要的日期格式是标准时间
emptyObj[eng] = new Date(formatExcelDate(item[eachChi]))
} else {
// item[key]是:this.tableResults数组成员的属性值,即"小张", "13800000252", "总裁办"等
// emptyObj[eng]是:eng是英文属性名,emptyObj是空对象
// 1.5 所以这一步就是向emptyObj空对象中添加新属性---对象名.新属性名=新属性值
emptyObj[eng] = item[eachChi]
}
})
// return的就是map方法的映射规则,即让每个数组成员(小对象)的属性名从 中文 变成 英文
return emptyObj
})
/* 第 2 步:发请求,将表格中的数据批量发送给服务器 */
const res = await importEmployeesByExcelApi(newArr)
// console.log(res)
if (res.success !== true) return this.$notify.error('上传失败')
// 上传后提示成功
this.$notify.success('批量导入员工成功')
// 退回员工管理页面
this.$router.push('/employees')
}
}
// 把excel文件中的日期格式的内容转回成标准时间
function formatExcelDate(numb, format = '/') {
const time = new Date((numb - 25567) * 24 * 3600000 - 5 * 60 * 1000 - 43 * 1000 - 24 * 3600000 - 8 * 3600000)
time.setYear(time.getFullYear())
const year = time.getFullYear() + ''
const month = time.getMonth() + 1 + ''
const date = time.getDate() + ''
if (format && format.length === 1) {
return year + format + month + format + date
}
return year + (month < 10 ? '0' + month : month) + (date < 10 ? '0' + date : date)
}
}
打印查看转换后的结果:
需求与导出Excel
需求:
点击不同按钮,导出以下Excel文件:
- 将本页数据导出为一个Excel文件
- 将所有数据导出为一个Excel文件
- 多表头导出;自动合并单元格的导出
本质: 借助xlxs插件,将请求得到的数据转换为Excel文件
第一种导出
handleDownload() {
// excel是.then方法的形式参数,接收异步操作成功的结果
import('@/vendor/Export2Excel').then(excel => {
// list是员工列表数组--employeesList
const list = this.employeesList
// formatJson函数用于处理数据(将一维数组转换为二维数组),data变量是处理完成的数据,即要导出的具体数据。例如每个员工的姓名、电话、工号等
const data = this.formatJson(list)
// console.log(data)
/*
查看处理后的数据是怎样的:
发现data是二维数组(数组成员还是数组)
上一步的employeesList数组是一维数组
所以formatJson函数的作用是将一维数组转为二维数组
为何必须是二维数组?
因为`Export2Excel`插件要求的数据格式是二维数组。
二维数组与excel表格的关系?外层数组中的内层数组,它们的第 1 个成员全部变成excel表格的第 1 列;它们的第2个成员全部变成excel表格的第2列;依次类推
*/
// 将上一步处理得到的data数据转换成excel文件(必须处理,否则无法导出)
// export_json_to_excel 是 Export2Excel插件中的一个函数
excel.export_json_to_excel({
header: this.tHeader, // excel表头,数组,数组成员是字符串
data: data, // 上一步处理得到的二维数组
filename: this.filename, // 生成的excel的文件名,字符串。没有指定时,默认为excel-list
autoWidth: this.autoWidth, // 单元格是否要自适应宽度,布尔值,默认为true
bookType: this.bookType // 生成的excel文件的后缀名,字符串,默认为xlsx
})
})
},
// 将一维数组转化二维数组
formatJson(list) {
// 1. 准备英文-中文的映射关系数组。
// 后端响应给表格的数据(employeesList数组)有很多,需要放入二维数组导出的数据不需要这么多, 所以用 mapping 数组过滤出到导出所需要的数据
const mapping = {
id: '编号',
password: '密码',
mobile: '手机号',
username: '姓名',
timeOfEntry: '入职日期',
formOfEmployment: '聘用形式',
correctionTime: '转正日期',
workNumber: '工号',
departmentName: '部门',
staffPhoto: '头像地址'
}
// 2. 遍历mapping数组,将其属性值作为导出表格的头部
this.tHeader = Object.values(mapping)
// 3. 将一维数组employeesList转换为符合要求的二维数组,list就是employeesList
// 所有list的成员---item,都是包含员工详细信息的对象
const twoDimensionArr = list.map((item) => {
// 声明空对象,向该空对象中添加新属性
const emptyObj = {}
// 获取mapping数组中的所有属性名---id、password等
const allEngKey = Object.keys(mapping)
// 遍历 allEngKey 数组,填充空对象emptyObj。
// eachEngKey是数组中的成员,即英文键
allEngKey.forEach((eachEngKey) => {
// 向emptyObj空对象添加新属性,属性名是mapping数组的属性值(中文),属性值是employeesList数组的属性值
emptyObj[mapping[eachEngKey]] = item[eachEngKey]
// emptyObj对象被填充成这样:
/*
{
'编号': '10637056....',
'密码': 'c877722...',
'手机号':'13800000002',
'姓名':'管理员',
'入职日期': '2018-11-02....',
'聘用形式':'1',
'转正日期': '2018...',
'工号':'9002'
'部门':'总裁办'
}
*/
})
// 遍历已经填充满成员的emptyObj,拿到所有的属性值
// ['10637056....','c877722...','13800000002','2018-11-02....','管理员','2018-11-02....',...,'9002','总裁办']
const allValue = Object.values(emptyObj)
return allValue
// map()方法的作用就是对数组成员应用某种映射关系,然后返回这个新的数组。return后接的就是该映射关系
// return allValue 表示:将上一步拿到的allValue数组作为map()方法的映射关系,作用到list上。 这样一来,list中的每个成员,即员工详细信息对象,都会被抽取出表头所要求的信息,这些信息形成allValue数组。也就是说,list中有多少个成员,就生成多少个allValue数组
// 最后,map方法遍历结束后,形成一个新数组,存入twoDimensionArr变量,而新数组的成员就是每个allValue数组
})
// 将twoDimensionArr作为formatJson()函数的返回值,
return twoDimensionArr
}
第二种导出
async handleDownload2() {
const res = await getEmployeeListApi({
// 这里的page和size不能存入data()函数的q对象中,因为点击按钮会导致翻页
page: 1,
size: this.total // 将所有后端所有数据的条数赋值为size的大小
})
const allEmployeesInfo = res.data.rows
import('@/vendor/Export2Excel').then(excel => {
// list是员工列表数组--employeesList
const list = allEmployeesInfo
// formatJson函数用于将一维数组转换为二维数组
const data = this.formatJson(list)
excel.export_json_to_excel({
header: this.tHeader, // excel表头,数组,数组成员是字符串
data: data, // 上一步处理得到的二维数组
filename: this.filename, // 生成的excel的文件名,字符串。没有指定时,默认为excel-list
autoWidth: this.autoWidth, // 单元格是否要自适应宽度,布尔值,默认为true
bookType: this.bookType // 生成的excel文件的后缀名,字符串,默认为xlsx
})
})
}
第三种导出
async handleDownload3() {
const res = await getEmployeeListApi({
// 这里的page和size不能存入data()函数的q对象中,因为点击按钮会导致翻页
page: 1,
size: this.total // 将所有后端所有数据的条数赋值为size的大小
})
const allEmployeesInfo = res.data.rows
import('@/vendor/Export2Excel').then(excel => {
// list是员工列表数组--employeesList
const list = allEmployeesInfo
// formatJson函数用于将一维数组转换为二维数组
const data = this.formatJson(list)
excel.export_json_to_excel({
/* === 多级表头 ===
0. 在默认表头的基础上,额外添加的表头,该属性可以为空数组 或 不写该属性
1. 要求也是一个二维数组
2. multiHeader数组中有多少个小数组,就有几个表头
3. mapping对象有多少个属性,每个小数组就必须有多少个数组成员。因为一一对应,mapping对象的属性决定了excel表格有多少列。所以即使有些列无其他表头,小数组的成员写空字符串也不能省略,否则报格式错误
*/
multiHeader: [
['热干面', '肠粉', '烤鸭脖', '烧鹅', '糖水', '罗宋汤', '炖牛肉', '卤鸡爪', '生煎', '小麻虾'],
['荔枝', '水蜜桃', '橙子', '苹果', '火龙果', '猕猴桃', '哈密瓜', '西瓜', '葡萄', '樱桃']
],
/* 像日常合并excel表格一样自动合并指定单元格:
0. 该属性可以为空数组 或 不写该属性
1. 合并后,单元格的内容默认为前者--A1
2. 合并后,单元格的内容默认为前者--C1
*/
merges: ['A1:B1', 'C1:C2'],
header: this.tHeader, // excel表头,数组,数组成员是字符串
data: data, // 上一步处理得到的二维数组
filename: this.filename, // 生成的excel的文件名,字符串。没有指定时,默认为excel-list
autoWidth: this.autoWidth, // 单元格是否要自适应宽度,布尔值,默认为true
bookType: this.bookType // 生成的excel文件的后缀名,字符串,默认为xlsx
})
})
}
转载自:https://juejin.cn/post/7062699973615288328