js中导入excel表转json格式和利用配置下载生成excel模板文件

今天药忘吃喽~ 2022-09-04 03:49 290阅读 0赞

页面中经常会遇到【导入和模板下载】的需求。下面是针对此类问题的一种实现方式
主要使用的库是 xlsx

安装包

  1. npm i xlsx

构造一个 excel.js 的配置文件

  1. import XLSX from 'xlsx'
  2. class ColumnDef {
  3. /** * * @param {string} name 列的标题 * @param {string} field 列的字段 * @param {object} [options] 列选项 * @param {bool} [options.required=true] 列是否必填的。当其为 true 时,其值为能为空 * @param {string} [options.type='string'] 列的类型,可选值见 ColumnDef.types */
  4. constructor(name, field, options) {
  5. this.name = name
  6. this.field = field
  7. this.options = Object.assign(
  8. {
  9. required: true,
  10. type: ColumnDef.types.STRING
  11. },
  12. options
  13. )
  14. }
  15. parseValue(value) {
  16. if (this.options.required) {
  17. if (value === undefined || value === null || value === '') {
  18. throw new Error('值不能为空')
  19. }
  20. }
  21. switch (this.options.type) {
  22. case ColumnDef.types.DATE:
  23. return this._parseDate(value)
  24. case ColumnDef.types.NUMBER:
  25. return parseInt(value)
  26. default:
  27. return value
  28. }
  29. }
  30. _parseDate(value) {
  31. const d = value - 1
  32. const t = Math.round((d - Math.floor(d)) * 24 * 60 * 60)
  33. const date = new Date(1900, 0, d, 8, 0, t)
  34. const dp = `${ date.getFullYear()}-${ this._pad(date.getMonth() + 1)}-${ this._pad(date.getDate())}`
  35. const tp = `${ this._pad(date.getHours())}:${ this._pad(date.getMinutes())}:${ this._pad(date.getSeconds())}`
  36. if (tp === '00:00:00') {
  37. return dp
  38. }
  39. return `${ dp} ${ tp}`
  40. }
  41. _pad(num) {
  42. return num.toString().padStart(2, '0')
  43. }
  44. }
  45. // 如果表中有字段为 时间 类型,需要指定为 date 类型
  46. ColumnDef.types = {
  47. STRING: 'string',
  48. NUMBER: 'number',
  49. DATE: 'date'
  50. }
  51. class SheetDef {
  52. /** * * @param {string} name Sheet名称 * @param {ColumnDef[]} columns 列声明 * @param {function({data: {}, index: number, raw: {}}): boolean | {}} [rowHandler] 行的值处理器。返回 false 表示值无效 * @param {number} [maxRowCount] 最多读取的数据行数 */
  53. constructor(name, columns, rowHandler, maxRowCount) {
  54. this.name = name
  55. /** * * @type {Map<string, ColumnDef>} */
  56. this.columns = new Map()
  57. this.rowHandler = rowHandler
  58. this.maxRowCount = maxRowCount
  59. columns.forEach(column => {
  60. this.columns.set(column.name, column)
  61. })
  62. }
  63. /** * * @param {WorkSheet} sheet */
  64. read(sheet) {
  65. const rows = XLSX.utils.sheet_to_json(sheet, {
  66. defval: null
  67. })
  68. const data = []
  69. for (let index = 0; index < rows.length; index++) {
  70. if (this.maxRowCount && index === this.maxRowCount) {
  71. break
  72. }
  73. const row = rows[index]
  74. const rowData = Object.create(null)
  75. // 先检查是否行的所有值都为空
  76. // 要是都为空,跳过此行
  77. if (Object.values(row).every(value => {
  78. return value === undefined || value === null || value === ''
  79. })) {
  80. continue
  81. }
  82. this.columns.forEach((column, name) => {
  83. if (!row.hasOwnProperty(name)) {
  84. throw new Error(`在表 "${ this.name}" 中找不到列 "${ name}"`)
  85. }
  86. try {
  87. rowData[column.field] = column.parseValue(row[name])
  88. } catch (e) {
  89. throw new Error(`表 "${ this.name}" 第 ${ index + 2} 行 "${ name}" ${ e.message}`)
  90. }
  91. })
  92. if (this.rowHandler) {
  93. const result = this.rowHandler({
  94. data: rowData,
  95. raw: row,
  96. index: index
  97. })
  98. if (result === false) {
  99. throw new Error(`表 "${ this.name}" 第 ${ index + 2} 行值无效`)
  100. }
  101. if (result !== undefined) {
  102. const type = /^\[object ([^[]+)]$/.exec(Object.prototype.toString.call(result))[1]
  103. if (type !== 'Object') {
  104. throw new Error(`表 "${ this.name} 的行处理函数返回值类型 "${ type}" 无效:仅支持返回 object/false 类型`)
  105. }
  106. }
  107. }
  108. data.push(rowData)
  109. }
  110. return data
  111. }
  112. buildTemplate() {
  113. const row = []
  114. const data = []
  115. this.columns.forEach((col, colName) => {
  116. row.push(colName)
  117. data.push('')
  118. })
  119. return XLSX.utils.aoa_to_sheet([row, data])
  120. }
  121. }
  122. class WorkbookDef {
  123. /** * * @param {SheetDef[]} defs 此表格中要使用的表定义 */
  124. constructor(defs) {
  125. this.defs = new Map()
  126. defs.forEach(def => {
  127. this.defs.set(def.name, def)
  128. })
  129. }
  130. async readFile(file) {
  131. const reader = new FileReader()
  132. const promise = new Promise((resolve, reject) => {
  133. reader.onload = function () {
  134. resolve(reader.result)
  135. }
  136. reader.onerror = function (e) {
  137. reader.abort()
  138. reject(e)
  139. }
  140. })
  141. reader.readAsArrayBuffer(file)
  142. return promise
  143. }
  144. /** * * @param {File} file */
  145. async read(file) {
  146. let dataBuffer = await this.readFile(file)
  147. const workbook = XLSX.read(dataBuffer, {
  148. type: 'array'
  149. })
  150. const result = []
  151. this.defs.forEach((def, sheetName) => {
  152. if (workbook.SheetNames.indexOf(sheetName) === -1) {
  153. throw new Error(`找不到名称为 "${ sheetName}" 的表`)
  154. }
  155. const sheet = workbook.Sheets[sheetName]
  156. result.push({
  157. name: sheetName,
  158. rows: def.read(sheet)
  159. })
  160. })
  161. return result
  162. }
  163. // 构造 excel 文件
  164. buildTemplate() {
  165. const workbook = {
  166. SheetNames: [],
  167. Sheets: Object.create(null)
  168. }
  169. this.defs.forEach(sheetDef => {
  170. workbook.SheetNames.push(sheetDef.name)
  171. workbook.Sheets[sheetDef.name] = sheetDef.buildTemplate()
  172. })
  173. const wbout = XLSX.write(workbook, {
  174. bookType: 'xlsx', // 要生成的文件类型
  175. bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
  176. type: 'binary'
  177. })
  178. // 字符串转ArrayBuffer
  179. function s2ab(s) {
  180. const buf = new ArrayBuffer(s.length)
  181. const view = new Uint8Array(buf)
  182. for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
  183. return buf
  184. }
  185. return new Blob([s2ab(wbout)], { type: 'application/octet-stream'})
  186. }
  187. // 下载模板文件
  188. downloadTemplate(name) {
  189. const data = this.buildTemplate()
  190. const link = document.createElement('a')
  191. document.body.appendChild(link)
  192. link.href = URL.createObjectURL(data)
  193. link.download = name + '.xlsx'
  194. link.click()
  195. setTimeout(() => {
  196. document.body.removeChild(link)
  197. }, 1000)
  198. }
  199. }
  200. export { WorkbookDef, SheetDef, ColumnDef}

构造一个 import.js 文件

  1. import { WorkbookDef, SheetDef, ColumnDef } from './excel'
  2. /** * @returns WorkbookDef * data 是一个数组对象,如 [{ label: '编号', field: 'no', required: true }, {...}],label 用于 excel 的表头字段名,field 用于 传给后台的字段名,required 表示是否一定为空。 * dataName 是生成 excel 表名的一部分 */
  3. function getDef(data, dataName, rowHandler) {
  4. return new WorkbookDef([
  5. new SheetDef(
  6. dataName,
  7. data.map(item => {
  8. return new ColumnDef(item.label, item.field, {
  9. required: item.required
  10. })
  11. }),
  12. rowHandler
  13. )
  14. ])
  15. }
  16. /** * 资产数据的导入和导入模板下载 */
  17. export default {
  18. /** * 下载导入的模板文件 * @param {string} dataName 多级名称写法: 服务器/国产化服务器 */
  19. downloadTemplate(data, dataName) {
  20. getDef(data, dataName).downloadTemplate(dataName.replace('/', '_') + '-数据导入模板')
  21. },
  22. /** * * @param {string} dataName 多级名称写法: 服务器/国产化服务器 * @param {File} file 选择的文件对象 */
  23. async readData(data, dataName, file, rowHandler) {
  24. return getDef(data, dataName, rowHandler).read(file)
  25. }
  26. }

如何在 Vue 页面中使用

模板内容

  1. <el-button type="primary" size="small" @click="downloadTpl">模板下载</el-button>
  2. <el-button type="success" size="small" @click="importExcel">导入</el-button>
  3. <!-- 导入 -->
  4. <el-dialog
  5. title="导入数据"
  6. @close="closeUploadFile"
  7. width="650px"
  8. top="30vh"
  9. :visible.sync="uploadVisible">
  10. <el-row>
  11. <el-col :span="6">选择要导入的文件</el-col>
  12. <el-col :span="12">
  13. <form ref="importForm">
  14. <input type="file" class="el-input" @change="onFileChange" />
  15. </form>
  16. </el-col>
  17. <el-col :span="6">
  18. <button @click="doImport">开始导入</button>
  19. </el-col>
  20. </el-row>
  21. <div class="tip">请选择 xlsx 格式的资产文件</div>
  22. </el-dialog>

js 部分

  1. import InfoImport from '@/assets/scripts/import'
  2. export default {
  3. name: 'TableList',
  4. components: { DeptForm, DeptDetail},
  5. props: {
  6. term: {
  7. type: Array,
  8. default: () => []
  9. }
  10. },
  11. data() {
  12. return {
  13. uploadVisible: false,
  14. selectedFile: null,
  15. tableData: [ // 这个一般不是前端写死,从后台配置文件返回的接口中获取
  16. { label: '编号', field: 'no', required: true },
  17. { label: '负责人', field: 'name', required: true },
  18. { label: '联系电话', field: 'phone', required: true },
  19. { label: '邮箱', field: 'email', required: true },
  20. { label: '备注', field: 'remark', required: false }
  21. ]
  22. }
  23. },
  24. methods: {
  25. downloadTpl() {
  26. InfoImport.downloadTemplate(this.tableData, '人员信息')
  27. },
  28. onFileChange(e) {
  29. this.selectedFile = e.target.files[0]
  30. },
  31. async doImport() {
  32. if (!this.selectedFile) {
  33. this.$message.warning('请选择要导入的文件')
  34. return
  35. }
  36. let data
  37. try {
  38. data = await InfoImport.readData(this.tableData, '人员信息', this.selectedFile, e => {
  39. // 可以对表中字段做额外处理,如
  40. // if (!nos.hasOwnProperty(row.no)) {
  41. // throw new Error(`第 ${e.index + 1} 行数据 "编号" 值 "${row.no}" 无效`)
  42. // }
  43. // row.no= depts[row.no]
  44. })
  45. } catch (e) {
  46. this.$error(e)
  47. return
  48. }
  49. if (!data || !data.length) {
  50. this.$message.warning('选择的文件中没有数据')
  51. return
  52. }
  53. data = data[0].rows // json 格式
  54. this.$API.post('/api/person', {
  55. data
  56. }).then(() => {
  57. this.closeUploadFile()
  58. this.$message.success('导入成功')
  59. this.getList()
  60. }).catch(e => {
  61. this.$error(e)
  62. })
  63. },
  64. importExcel() {
  65. this.uploadVisible = true
  66. },
  67. closeUploadFile() {
  68. this.uploadVisible = false
  69. this.$refs.importForm.reset()
  70. this.selectedFile = null
  71. }
  72. }

发表评论

表情:
评论列表 (有 0 条评论,290人围观)

还没有评论,来说两句吧...

相关阅读