1. 导入功能
1.1 前端主导(工作大量在前端)
- 上传excel文件,把excel文件的内容读出来,还原成最基本的行列结构,按后端的接口要求回传过去。
- 前端读excel文件,调接口
1.2 后端主导(工作大量在后端)
- 前端上传excel文件
1.3 实现
1. 安装必要插件
这个插件叫做
xlsx
npm install xlsx -S
//或者
yarn add xlsx -S
2. 引入UploadExcel组件并注册全局
- UploadExcel组件
UploadExcel/index.vue
<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="mini" type="primary" @click="handleUpload">浏览</el-button></div></div>
</template><script>
import XLSX from 'xlsx'export default {props: {beforeUpload: Function, // eslint-disable-lineonSuccess: Function// eslint-disable-line},data() {return {loading: false,excelData: {header: null,results: null}}},methods: {generateData({ header, results }) {this.excelData.header = headerthis.excelData.results = resultsthis.onSuccess && this.onSuccess(this.excelData)},handleDrop(e) {e.stopPropagation()e.preventDefault()if (this.loading) returnconst files = e.dataTransfer.filesif (files.length !== 1) {this.$message.error('Only support uploading one file!')return}const rawFile = files[0] // only use files[0]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'},handleUpload() {this.$refs['excel-upload-input'].click()},handleClick(e) {const files = e.target.filesconst rawFile = files[0] // only use files[0]if (!rawFile) returnthis.upload(rawFile)},upload(rawFile) {this.$refs['excel-upload-input'].value = null // fix can't select the same excelif (!this.beforeUpload) {this.readerData(rawFile)return}const before = this.beforeUpload(rawFile)if (before) {this.readerData(rawFile)}},readerData(rawFile) {this.loading = truereturn new Promise((resolve, reject) => {const reader = new FileReader()reader.onload = e => {const data = e.target.resultconst workbook = XLSX.read(data, { type: 'array' })const firstSheetName = workbook.SheetNames[0]const worksheet = workbook.Sheets[firstSheetName]const header = this.getHeaderRow(worksheet)const results = XLSX.utils.sheet_to_json(worksheet)this.generateData({ header, results })this.loading = falseresolve()}reader.readAsArrayBuffer(rawFile)})},getHeaderRow(sheet) {const headers = []const range = XLSX.utils.decode_range(sheet['!ref'])let Cconst 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 defaultif (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>
3. 注册
import UploadExcel from './UploadExcel'export default {// 插件的初始化, 插件给你提供的全局的功能, 都可以在这里配置install(Vue) {// 进行组件的全局注册Vue.component('UploadExcel', UploadExcel) // 注册导入excel组件}
}
4. 新建一个公共的导入页面
<template><upload-excel :on-success="handleSuccess" />
</template><script>
export default {name: 'Import',methods: {handleSuccess({ header, results }) {console.log(header, results)}}
}
</script>
- excel导入插件本质:把excel经过分析转换成js能够识别的常规数据,拿到数据我们可以进行任何操作
数据处理
- 数据格式转换:将excel解析好的数据经过处理后,转成可以传给接口调用的数据
调用接口进行excel上传的重点其实是数据的处理,我们需要按照接口的要求,把excel表格中经过插件处理好的数据处理成后端接口要求的格式
下面是后端接口要求的示例格式
- 按接口要求,处理excel导入的数据
处理内容包含:
- 字段中文转英文。excel中读入的是
姓名
,而后端需要的是username
- 日期处理。从excel中读入的时间是一个number值,而后端需要的是标准日期。
5. 我们单独封装一个方法来实现这个转换的功能
/*** results excel表格的内容// [ {'姓名':'小张', '手机号': '13712345678'}, {.....} ]// 目标// [ {'username':'小张', 'mobile': '13712345678'}, {.....} ]*/transExcel(results) {const userRelations = {'入职日期': 'timeOfEntry','手机号': 'mobile','姓名': 'username','转正日期': 'correctionTime','工号': 'workNumber','部门': 'departmentName','聘用形式': 'formOfEmployment'}return results.map(item => {const obj = {}// 1. 取出这个对象所有的属性名: ['姓名', ‘手机号’]// 2. 遍历这个数组,通过 中文名去 userRelations 找对应英文名, 保存值const zhKeys = Object.keys(item)zhKeys.forEach(zhKey => {const enKey = userRelations[zhKey]// 如果是时间格式,就要做转换if (enKey === 'correctionTime' || enKey === 'timeOfEntry') {obj[enKey] = new Date(formatExcelDate(item[zhKey]))} else {obj[enKey] = item[zhKey]}})return obj})}handleSuccess({ results, header }) {console.log('从当前excel文件中读出的内容是', results)// results: [{入职日期: 44502, 姓名:xxxx}]// 目标:// results: [{timeOfEntry: 44502, username:xxxx}]// 处理从excel中读入的格式const arr = this.transExcel(results)console.log('转换之后的格式是', arr)
})
6. 日期处理函数
// 把excel文件中的日期格式的内容转回成标准时间
// https://blog.csdn.net/qq_15054679/article/details/107712966
export 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)
}
下面我们就可以调接口了
- 例子
-
接口:
/*** @description: 导入excel* @param {*} data* @return {*}*/
export function importEmployee(data) {return request({url: '/sys/user/batch',method: 'post',data})
}
-
- 在页面中使用
import { importEmployee } from '@/api/employees'
export default {name: 'Import',methods: {async handleSuccess({ results, header }) {try {console.log('从当前excel文件中读出的内容是', results)// results: [{入职日期: 44502, 姓名:xxxx}]// 目标:// results: [{timeOfEntry: 44502, username:xxxx}]const arr = this.transExcel(results)console.log('转换之后的格式是', arr)// 调用上传的接口,const rs = await importEmployee(arr)console.log('调用上传的接口', rs)// 上传成功之后,回去刚才的页面this.$router.back()this.$message.success('操作成功')} catch (err) {this.$message.error(err.message)}}
}
2. 导出功能
在表格中查询到了我们需要的数据,希望用他们生成excel文件,保存在本地。
2.1 前端主导(工作大量在前端)
- 取回数据,保存excel文件
2.2 后端主导(工作大量在后端)
前端调用接口
2.3 实现
1. 安装依赖
npm install file-saver script-loader --save
2. 导入文件
- src/vendor/export2Excel.js
/* eslint-disable */
import { saveAs } from 'file-saver'
import XLSX from 'xlsx'function generateArray(table) {var out = [];var rows = table.querySelectorAll('tr');var ranges = [];for (var R = 0; R < rows.length; ++R) {var outRow = [];var row = rows[R];var columns = row.querySelectorAll('td');for (var C = 0; C < columns.length; ++C) {var cell = columns[C];var colspan = cell.getAttribute('colspan');var rowspan = cell.getAttribute('rowspan');var cellValue = cell.innerText;if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue;//Skip rangesranges.forEach(function (range) {if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);}});//Handle Row Spanif (rowspan || colspan) {rowspan = rowspan || 1;colspan = colspan || 1;ranges.push({s: {r: R,c: outRow.length},e: {r: R + rowspan - 1,c: outRow.length + colspan - 1}});};//Handle ValueoutRow.push(cellValue !== "" ? cellValue : null);//Handle Colspanif (colspan)for (var k = 0; k < colspan - 1; ++k) outRow.push(null);}out.push(outRow);}return [out, ranges];
};function datenum(v, date1904) {if (date1904) v += 1462;var epoch = Date.parse(v);return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);
}function sheet_from_array_of_arrays(data, opts) {var ws = {};var range = {s: {c: 10000000,r: 10000000},e: {c: 0,r: 0}};for (var R = 0; R != data.length; ++R) {for (var C = 0; C != data[R].length; ++C) {if (range.s.r > R) range.s.r = R;if (range.s.c > C) range.s.c = C;if (range.e.r < R) range.e.r = R;if (range.e.c < C) range.e.c = C;var cell = {v: data[R][C]};if (cell.v == null) continue;var cell_ref = XLSX.utils.encode_cell({c: C,r: R});if (typeof cell.v === 'number') cell.t = 'n';else if (typeof cell.v === 'boolean') cell.t = 'b';else if (cell.v instanceof Date) {cell.t = 'n';cell.z = XLSX.SSF._table[14];cell.v = datenum(cell.v);} else cell.t = 's';ws[cell_ref] = cell;}}if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);return ws;
}function Workbook() {if (!(this instanceof Workbook)) return new Workbook();this.SheetNames = [];this.Sheets = {};
}function s2ab(s) {var buf = new ArrayBuffer(s.length);var view = new Uint8Array(buf);for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;return buf;
}export function export_table_to_excel(id) {var theTable = document.getElementById(id);var oo = generateArray(theTable);var ranges = oo[1];/* original data */var data = oo[0];var ws_name = "SheetJS";var wb = new Workbook(),ws = sheet_from_array_of_arrays(data);/* add ranges to worksheet */// ws['!cols'] = ['apple', 'banan'];ws['!merges'] = ranges;/* add worksheet to workbook */wb.SheetNames.push(ws_name);wb.Sheets[ws_name] = ws;var wbout = XLSX.write(wb, {bookType: 'xlsx',bookSST: false,type: 'binary'});saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), "test.xlsx")
}export function export_json_to_excel({multiHeader = [],header,data,filename,merges = [],autoWidth = true,bookType = 'xlsx'
} = {}) {/* original data */filename = filename || 'excel-list'data = [...data]data.unshift(header);for (let i = multiHeader.length - 1; i > -1; i--) {data.unshift(multiHeader[i])}var ws_name = "SheetJS";var wb = new Workbook(),ws = sheet_from_array_of_arrays(data);if (merges.length > 0) {if (!ws['!merges']) ws['!merges'] = [];merges.forEach(item => {ws['!merges'].push(XLSX.utils.decode_range(item))})}if (autoWidth) {/*设置worksheet每列的最大宽度*/const colWidth = data.map(row => row.map(val => {/*先判断是否为null/undefined*/if (val == null) {return {'wch': 10};}/*再判断是否为中文*/else if (val.toString().charCodeAt(0) > 255) {return {'wch': val.toString().length * 2};} else {return {'wch': val.toString().length};}}))/*以第一行为初始值*/let result = colWidth[0];for (let i = 1; i < colWidth.length; i++) {for (let j = 0; j < colWidth[i].length; j++) {if (result[j]['wch'] < colWidth[i][j]['wch']) {result[j]['wch'] = colWidth[i][j]['wch'];}}}ws['!cols'] = result;}/* add worksheet to workbook */wb.SheetNames.push(ws_name);wb.Sheets[ws_name] = ws;var wbout = XLSX.write(wb, {bookType: bookType,bookSST: false,type: 'binary'});saveAs(new Blob([s2ab(wbout)], {type: "application/octet-stream"}), `${filename}.${bookType}`);
}
3. 给导出按钮添加点击事件
import('@/vendor/Export2Excel').then(excel => {// excel表示导入的模块对象console.log(excel)excel.export_json_to_excel({header: ['姓名', '工资'], // 表头 必填data: [['刘备', 100],['关羽', 500]], // 具体数据 必填filename: 'excel-list', // 文件名称autoWidth: true, // 宽度是否自适应bookType: 'xlsx' // 生成的文件类型})
})以上代码表示:1. 当我们正式点击导出按钮之后,才去加载vendor文件夹中的Export2Excel模块
2. import方法执行完毕返回的是一个promise对象,在then方法中我们可以拿到使用的模块对象
3. 重点关注data的配置部分,我们发现它需要一个严格的二维数组
Excel导出参数说明
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
header | 导出数据的表头 | Array | / | [] |
data | 导出的具体数据 | Array | / | [[]] |
filename | 导出文件名 | String | / | excel-list |
autoWidth | 单元格是否要自适应宽度 | Boolean | true / false | true |
bookType | 导出文件类型 | String | xlsx, csv, txt, more | xlsx |
真实数据实现导出功能
- 思路
- 从后台重新获取数据(这样才能确保是最新的)
- 对数据的格式进行转换(后端给的数据字段名都是英文的),以用来做导出
-
核心在于把后端接口返回的数据转成Export2Excel这个插件需要的格式
4. 准备表头header数据
因为接口中返回的数据中的key是英文,而我们期望导出的表头是中文,所以提前准备中文和英文的映射关系
const map = {'id': '编号','password': '密码','mobile': '手机号','username': '姓名','timeOfEntry': '入职日期','formOfEmployment': '聘用形式','correctionTime': '转正日期','workNumber': '工号','departmentName': '部门','staffPhoto': '头像地址'
}
具体的表格数据我们需要通过接口从后端获取回来,难点在于如何把后端返回的数据处理成Export2Excel插件需求的二维数组格式。
下面是一个示例:
const dataArr =
[["13600000001", "吕勇锐", "1992-08-04", "正式", "2020-01-01", "0001", "总裁办"]["13600000002", "袁永安", "1993-08-04", "正式", "2020-01-01", "0002", "总裁办"]
]
5. 补充一个用来处理数据的函数
transData(rows) {// 写代码const map = {'id': '编号','password': '密码','mobile': '手机号','username': '姓名','timeOfEntry': '入职日期','formOfEmployment': '聘用形式','correctionTime': '转正日期','workNumber': '工号','departmentName': '部门','staffPhoto': '头像地址'}// 写代码// header => ['id', '手机号', '用户名', ... ]// data => [// ["604f764971f93f3ac8f365c2", "13800000002", "管理员",.... ]// ]const enKeys = Object.keys(rows[0])const header = enKeys.map(enKey => {return map[enKey]})const data = rows.map(obj => {return Object.values(obj)})return { header, data }}
最终的代码
// 导出excelasync hExportExcel() {// 1. 获取数据const res = await getEmployeeList(this.pageParams)console.log('有效数据:', res.data.rows)// 2. 处理数据const obj = this.transData(res.data.rows)// 3. 导出import('@/vendor/Export2Excel').then(excel => {excel.export_json_to_excel({header: obj.header, // ['姓名', '工资'], // 表头 必填data: obj.data, // [// ['刘备', 800],// ['关羽', 500]// ], // 具体数据 必填filename: 'employee-list', // 文件名称autoWidth: true, // 宽度是否自适应bookType: 'xlsx' // 生成的文件类型})})},transData(rows) {// 写代码const map = {'id': '编号','password': '密码','mobile': '手机号','username': '姓名','timeOfEntry': '入职日期','formOfEmployment': '聘用形式','correctionTime': '转正日期','workNumber': '工号','departmentName': '部门','staffPhoto': '头像地址'}// 写代码// header => ['id', '手机号', '用户名', ... ]// data => [// ["604f764971f93f3ac8f365c2", "13800000002", "管理员",.... ]// ]const enKeys = Object.keys(rows[0])const header = enKeys.map(enKey => {return map[enKey]})const data = rows.map(obj => {return Object.values(obj)})return { header, data }}