js中导入excel表转json格式和利用配置下载生成excel模板文件
页面中经常会遇到【导入和模板下载】的需求。下面是针对此类问题的一种实现方式
主要使用的库是xlsx
库
安装包
npm i xlsx
构造一个 excel.js
的配置文件
import XLSX from 'xlsx'
class ColumnDef {
/** * * @param {string} name 列的标题 * @param {string} field 列的字段 * @param {object} [options] 列选项 * @param {bool} [options.required=true] 列是否必填的。当其为 true 时,其值为能为空 * @param {string} [options.type='string'] 列的类型,可选值见 ColumnDef.types */
constructor(name, field, options) {
this.name = name
this.field = field
this.options = Object.assign(
{
required: true,
type: ColumnDef.types.STRING
},
options
)
}
parseValue(value) {
if (this.options.required) {
if (value === undefined || value === null || value === '') {
throw new Error('值不能为空')
}
}
switch (this.options.type) {
case ColumnDef.types.DATE:
return this._parseDate(value)
case ColumnDef.types.NUMBER:
return parseInt(value)
default:
return value
}
}
_parseDate(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())}`
if (tp === '00:00:00') {
return dp
}
return `${ dp} ${ tp}`
}
_pad(num) {
return num.toString().padStart(2, '0')
}
}
// 如果表中有字段为 时间 类型,需要指定为 date 类型
ColumnDef.types = {
STRING: 'string',
NUMBER: 'number',
DATE: 'date'
}
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.parseValue(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
}
buildTemplate() {
const row = []
const data = []
this.columns.forEach((col, colName) => {
row.push(colName)
data.push('')
})
return XLSX.utils.aoa_to_sheet([row, data])
}
}
class WorkbookDef {
/** * * @param {SheetDef[]} defs 此表格中要使用的表定义 */
constructor(defs) {
this.defs = new Map()
defs.forEach(def => {
this.defs.set(def.name, def)
})
}
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)
}
})
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.defs.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
}
// 构造 excel 文件
buildTemplate() {
const workbook = {
SheetNames: [],
Sheets: Object.create(null)
}
this.defs.forEach(sheetDef => {
workbook.SheetNames.push(sheetDef.name)
workbook.Sheets[sheetDef.name] = sheetDef.buildTemplate()
})
const wbout = XLSX.write(workbook, {
bookType: 'xlsx', // 要生成的文件类型
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
})
// 字符串转ArrayBuffer
function s2ab(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
}
return new Blob([s2ab(wbout)], { type: 'application/octet-stream'})
}
// 下载模板文件
downloadTemplate(name) {
const data = this.buildTemplate()
const link = document.createElement('a')
document.body.appendChild(link)
link.href = URL.createObjectURL(data)
link.download = name + '.xlsx'
link.click()
setTimeout(() => {
document.body.removeChild(link)
}, 1000)
}
}
export { WorkbookDef, SheetDef, ColumnDef}
构造一个 import.js
文件
import { WorkbookDef, SheetDef, ColumnDef } from './excel'
/** * @returns WorkbookDef * data 是一个数组对象,如 [{ label: '编号', field: 'no', required: true }, {...}],label 用于 excel 的表头字段名,field 用于 传给后台的字段名,required 表示是否一定为空。 * dataName 是生成 excel 表名的一部分 */
function getDef(data, dataName, rowHandler) {
return new WorkbookDef([
new SheetDef(
dataName,
data.map(item => {
return new ColumnDef(item.label, item.field, {
required: item.required
})
}),
rowHandler
)
])
}
/** * 资产数据的导入和导入模板下载 */
export default {
/** * 下载导入的模板文件 * @param {string} dataName 多级名称写法: 服务器/国产化服务器 */
downloadTemplate(data, dataName) {
getDef(data, dataName).downloadTemplate(dataName.replace('/', '_') + '-数据导入模板')
},
/** * * @param {string} dataName 多级名称写法: 服务器/国产化服务器 * @param {File} file 选择的文件对象 */
async readData(data, dataName, file, rowHandler) {
return getDef(data, dataName, rowHandler).read(file)
}
}
如何在 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,
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() {
InfoImport.downloadTemplate(this.tableData, '人员信息')
},
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.tableData, '人员信息', 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
}
}
还没有评论,来说两句吧...