vue中使用xlsx实现excel表的导入和导出
页面中经常会遇到【导入和模板下载】的需求。下面是针对此类问题的一种实现方式主要使用的库是 xlsx 库安装包npm i xlsx构造一个 excel.js 的配置文件import XLSX from 'xlsx'class ColumnDef {/**** @param {string} name 列的标题* @param {string} field 列的字段* @param {object}
·
页面中经常会遇到【导入和模板下载】的需求。下面是针对此类问题的一种实现方式
主要使用的库是xlsx
库
安装包
npm i xlsx
构造一个 excel.js
的配置文件
import XLSX from 'xlsx'
function downloadData(data, filename) {
const link = document.createElement('a')
document.body.appendChild(link)
link.href = URL.createObjectURL(data)
link.download = filename
link.click()
setTimeout(() => {
document.body.removeChild(link)
}, 1000)
}
// 字符串转ArrayBuffer
function string2arraybuffer(s) {
const buf = new ArrayBuffer(s.length)
const view = new Uint8Array(buf)
for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
/**
* 列的枚举类型定义
*/
class ColumnEnum {
/**
*
* @param label
* @param {any} value
*/
constructor(label, value) {
this.label = label
this.value = value
}
}
/**
* 列定义
*/
class ColumnDef {
/**
*
* @param {string} name 列的标题
* @param {string} field 列的字段
* @param {object} [options] 列选项
* @param {bool} [options.required=true] 列是否必填的。当其为 true 时,其值不能为空
* @param {string} [options.type='string'] 列的类型,可选值见 ColumnDef.types
* @param {ColumnEnum[]} [options.enums] 列的枚举定义
* @param {function({value: any, column: ColumnDef})} [options.writeParser] 列的写处理函数
* @param {function({value: any, column: ColumnDef})} [options.readParser] 列的读处理函数
*/
constructor(name, field, options) {
this.name = name
this.field = field
this.options = Object.assign(
{
required: true,
type: ColumnDef.types.STRING,
enums: []
},
options
)
}
/**
* 处理导入时的数据
* @param value
* @returns {string|number|*}
*/
parseReadValue(value) {
if (this.options.readParser) {
value = this.options.readParser({
value,
column: this
})
}
const isEmpty = value === undefined || value === null || value === ''
if (this.options.required && isEmpty) {
throw new Error('值不能为空')
}
if (!isEmpty) {
switch (this.options.type) {
case ColumnDef.types.DATE:
case ColumnDef.types.DATETIME:
value = this._parseReadDate(value)
break
case ColumnDef.types.NUMBER:
value = parseInt(value)
break
default:
break
}
if (this.options.enums.length && [ColumnDef.types.DATE, ColumnDef.types.DATETIME].indexOf(this.options.type) === -1) {
value = this._parseReadEnum()
}
}
return value
}
/**
* 处理导出时的数据
* @param value
* @returns {string|number|*}
*/
parseWriteValue(value) {
if (this.options.writeParser) {
value = this.options.writeParser({
value,
column: this
})
}
const isEmpty = value === undefined || value === null || value === ''
if (!isEmpty) {
switch (this.options.type) {
case ColumnDef.types.DATE:
case ColumnDef.types.DATETIME:
value = this._parseWriteDate(value)
break
default:
break
}
if (this.options.enums.length && [ColumnDef.types.DATE, ColumnDef.types.DATETIME].indexOf(this.options.type) === -1) {
value = this._parseWriteEnum(value)
}
}
return value
}
/**
*
* @param {number} value
* @returns {string}
* @private
*/
_parseReadDate(value) {
const d = value - 1
const t = Math.round((d - Math.floor(d)) * 24 * 60 * 60)
const date = new Date(1900, 0, d, 8, 0, t)
const dp = `${date.getFullYear()}-${this._pad(date.getMonth() + 1)}-${this._pad(date.getDate())}`
const tp = `${this._pad(date.getHours())}:${this._pad(date.getMinutes())}:${this._pad(date.getSeconds())}`
return this.options.type === ColumnDef.types.DATETIME ? `${dp} ${tp}` : dp
}
_parseReadEnum(value) {
for (const e of this.options.enums) {
if (e.label === value) {
return e.value
}
}
throw new Error(`无效的枚举值 "${value}"`)
}
_parseWriteEnum(value) {
for (const e of this.options.enums) {
if (e.value === value) {
return e.label
}
}
throw new Error(`无效的枚举值 "${value}"`)
}
/**
*
* @param {string} value
* @returns {string}
* @private
*/
_parseWriteDate(value) {
return this.options.type === ColumnDef.types.DATETIME ? value : value.split(' ')[0]
}
_pad(num) {
return num.toString().padStart(2, '0')
}
}
/**
* 列数据的类型
* @type {{DATE: string, NUMBER: string, STRING: string}}
* 如果表中有字段为 时间 类型,需要指定为 date 类型
*/
ColumnDef.types = {
STRING: 'string',
NUMBER: 'number',
DATE: 'date',
DATETIME: 'datetime'
}
/**
* 表定义
*/
class SheetDef {
/**
*
* @param {string} name Sheet名称
* @param {ColumnDef[]} columns 列声明
* @param {function({data: {}, index: number, raw: {}}): boolean | {}} [rowHandler] 行的值处理器。返回 false 表示值无效
* @param {number} [maxRowCount] 最多读取的数据行数
*/
constructor(name, columns, rowHandler, maxRowCount) {
this.name = name
/**
*
* @type {Map<string, ColumnDef>}
*/
this.columns = new Map()
this.rowHandler = rowHandler
this.maxRowCount = maxRowCount
columns.forEach(column => {
this.columns.set(column.name, column)
})
}
/**
*
* @param {WorkSheet} sheet
*/
read(sheet) {
const rows = XLSX.utils.sheet_to_json(sheet, {
defval: null
})
const data = []
for (let index = 0; index < rows.length; index++) {
if (this.maxRowCount && index === this.maxRowCount) {
break
}
const row = rows[index]
const rowData = Object.create(null)
// 先检查是否行的所有值都为空
// 要是都为空,跳过此行
if (Object.values(row).every(value => {
return value === undefined || value === null || value === ''
})) {
continue
}
this.columns.forEach((column, name) => {
if (!row.hasOwnProperty(name)) {
throw new Error(`在表 "${this.name}" 中找不到列 "${name}"`)
}
try {
rowData[column.field] = column.parseReadValue(row[name])
} catch (e) {
throw new Error(`表 "${this.name}" 第 ${index + 2} 行 "${name}" ${e.message}`)
}
})
if (this.rowHandler) {
const result = this.rowHandler({
data: rowData,
raw: row,
index: index
})
if (result === false) {
throw new Error(`表 "${this.name}" 第 ${index + 2} 行值无效`)
}
if (result !== undefined) {
const type = /^\[object ([^[]+)]$/.exec(Object.prototype.toString.call(result))[1]
if (type !== 'Object') {
throw new Error(`表 "${this.name} 的行处理函数返回值类型 "${type}" 无效:仅支持返回 object/false 类型`)
}
}
}
data.push(rowData)
}
return data
}
/**
* 将数据写入 Sheet
* @param {object[]} data
* @returns {WorkSheet}
*/
write(data) {
const header = []
this.columns.forEach((col, colName) => {
header.push(colName)
})
const rows = [header]
data.forEach((row, index) => {
const rowData = []
this.columns.forEach(columnDef => {
try {
rowData.push(columnDef.parseWriteValue(row[columnDef.field]))
} catch (e) {
throw new Error(`表 "${this.name}" 的数据第 ${index} 行 "${name}" ${e.message}`)
}
})
rows.push(rowData)
})
return XLSX.utils.aoa_to_sheet(rows)
}
}
/**
* 工作薄定义
*/
class WorkbookDef {
/**
*
* @param {SheetDef[]} defs 此表格中要使用的表定义
*/
constructor(defs) {
/**
*
* @type {Map<string, SheetDef>}
*/
this.sheets = new Map()
defs.forEach(def => {
this.sheets.set(def.name, def)
})
}
/**
* 读取文件内容
* @param file
* @returns {Promise<unknown>}
*/
async readFile(file) {
const reader = new FileReader()
const promise = new Promise((resolve, reject) => {
reader.onload = function () {
resolve(reader.result)
}
reader.onerror = function (e) {
reader.abort()
reject(e)
}
})
console.log(file)
console.log(reader)
reader.readAsArrayBuffer(file)
return promise
}
/**
* 从 文件对象 读取数据
* @param {File} file
*/
async read(file) {
let dataBuffer = await this.readFile(file)
const workbook = XLSX.read(dataBuffer, {
type: 'array'
})
const result = []
this.sheets.forEach((def, sheetName) => {
if (workbook.SheetNames.indexOf(sheetName) === -1) {
throw new Error(`找不到名称为 "${sheetName}" 的表`)
}
const sheet = workbook.Sheets[sheetName]
result.push({
name: sheetName,
rows: def.read(sheet)
})
})
return result
}
/**
* 将数据写入文件
* @param {[{name: string, rows: array}]} [data]
* @param {string} saveAs 另存为文件名(会自动添加 xlsx 扩展名)
*/
write(data, saveAs) {
const workbook = {
SheetNames: [],
Sheets: Object.create(null)
}
const tempData = {}
data.forEach(item => {
tempData[item.name] = item.rows
})
this.sheets.forEach(sheetDef => {
const name = sheetDef.name
workbook.SheetNames.push(name)
const rows = tempData[name] || []
workbook.Sheets[name] = this.sheets.get(name).write(rows)
})
const output = XLSX.write(workbook, {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
})
const blobData = new Blob([string2arraybuffer(output)], {type: 'application/octet-stream'})
if (!saveAs) {
return blobData
}
downloadData(blobData, saveAs + '.xlsx')
}
}
export {WorkbookDef, SheetDef, ColumnDef, ColumnEnum}
构造一个 import.js
文件
import { WorkbookDef, SheetDef, ColumnDef } from './excel'
/**
* @returns WorkbookDef
* data: 数组对象,对象里面包含数据data和sheet表名dataName
* data 是一个数组对象,如 [{ label: '编号', field: 'no', required: true }, {...}],label 用于 excel 的表头字段名,field 用于 传给后台的字段名,required 表示是否一定为空。
* dataName 是生成 excel 表名的一部分
*/
function getWbDef(data, rowHandler) {
let sheet = []
data.forEach(ele => {
sheet.push(
new SheetDef(
dataName,
data.map(item => {
return new ColumnDef(item.label, item.field, {
required: item.required
})
}),
rowHandler
)
)
})
return new WorkbookDef(sheet)
}
/**
* 资产数据的导入和导入模板下载
*/
export default {
/**
* 下载导入的模板文件
* @param {string} dataName 多级名称写法: 服务器/国产化服务器
*/
downloadTemplate(data, dataName) {
getWbDef(data).write([], dataName.replace('/', '_') + '-数据导入模板')
},
/**
*
* @param {string} data 数组对象
* @param {File} file 选择的文件对象
*/
async readData(data, file, rowHandler) {
return getWbDef(data, rowHandler).read(file)
},
/**
* 导出excel
* @param {Array} data 写入excel的数据
* { name: dataName, rows: data }:数据格式
* 传入excel表的数据格式必须是 [ { name: dataName, rows: data }, { name: dataName, rows: data }, ... ],数组里面有几个对象就有几个sheet表
*/
export(data, dataName) {
getDef(dataName).write(data, dataName)
}
}
如何在 Vue 页面中使用
模板内容
<el-button type="primary" size="small" @click="downloadTpl">模板下载</el-button>
<el-button type="success" size="small" @click="importExcel">导入</el-button>
<!-- 导入 -->
<el-dialog
title="导入数据"
@close="closeUploadFile"
width="650px"
top="30vh"
:visible.sync="uploadVisible">
<el-row>
<el-col :span="6">选择要导入的文件</el-col>
<el-col :span="12">
<form ref="importForm">
<input type="file" class="el-input" @change="onFileChange" />
</form>
</el-col>
<el-col :span="6">
<button @click="doImport">开始导入</button>
</el-col>
</el-row>
<div class="tip">请选择 xlsx 格式的资产文件</div>
</el-dialog>
js 部分
import InfoImport from '@/assets/scripts/import'
export default {
name: 'TableList',
components: {DeptForm, DeptDetail},
props: {
term: {
type: Array,
default: () => []
}
},
data() {
return {
uploadVisible: false,
selectedFile: null,
data:[],
tableData: [ // 这个一般不是前端写死,从后台配置文件返回的接口中获取
{ label: '编号', field: 'no', required: true },
{ label: '负责人', field: 'name', required: true },
{ label: '联系电话', field: 'phone', required: true },
{ label: '邮箱', field: 'email', required: true },
{ label: '备注', field: 'remark', required: false }
]
}
},
methods: {
downloadTpl() {
let res = {
data: this.tableData,
dataName: '人员信息'
}
this.data.push(res)
InfoImport.downloadTemplate(this.data, '人员信息')
},
onFileChange(e) {
this.selectedFile = e.target.files[0]
},
async doImport() {
if (!this.selectedFile) {
this.$message.warning('请选择要导入的文件')
return
}
let data
try {
data = await InfoImport.readData(this.data, this.selectedFile, e => {
// 可以对表中字段做额外处理,如
// if (!nos.hasOwnProperty(row.no)) {
// throw new Error(`第 ${e.index + 1} 行数据 "编号" 值 "${row.no}" 无效`)
// }
// row.no= depts[row.no]
})
} catch (e) {
this.$error(e)
return
}
if (!data || !data.length) {
this.$message.warning('选择的文件中没有数据')
return
}
data = data[0].rows // json 格式
this.$API.post('/api/person', {
data
}).then(() => {
this.closeUploadFile()
this.$message.success('导入成功')
this.getList()
}).catch(e => {
this.$error(e)
})
},
importExcel() {
this.uploadVisible = true
},
closeUploadFile() {
this.uploadVisible = false
this.$refs.importForm.reset()
this.selectedFile = null
}
}
更多推荐
已为社区贡献31条内容
所有评论(0)