Excel导入功能实现

146 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情

前言

通常项目中可以通过导入excel文件对数据进行增加,本文记录该功能的实现

实现

基本样式

需要实现两种导入形式,基本样式如下

1.png

点击上传使用el-buttoninput即可

新建components/UploadExcel,为按钮和输入框绑定事件

<template>
  <div class="upload-excel">
    <div class="btn-upload">
      <el-button :loading="loading" type="primary" @click="handleUpload">
        {{ $t('msg.uploadExcel.upload') }}
      </el-button>
    </div>

    <input
      ref="excelUploadInput"
      class="excel-upload-input"
      type="file"
      accept=".xlsx, .xls"
      @change="handleChange"
    />
    <div
      class="drop"
    >
      <i class="el-icon-upload" />
      <span>{{ $t('msg.uploadExcel.drop') }}</span>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.upload-excel {
  display: flex;
  justify-content: center;
  margin-top: 100px;
  .excel-upload-input {
    display: none;
    z-index: -9999;
  }
  .btn-upload,
  .drop {
    border: 1px dashed #bbb;
    width: 350px;
    height: 160px;
    text-align: center;
    line-height: 160px;
  }
  .drop {
    line-height: 60px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    color: #bbb;
    i {
      font-size: 60px;
      display: block;
    }
  }
}
</style>

逻辑处理

导入方式分为两种,点击上传拖拽上传

点击上传

点击上传按钮后,触发inputchange事件

使用ref拿到input对象触发点击事件

const excelUploadInput = ref(null)


const handleUpload = () => {
  excelUploadInput.value.click()
}

触发input change事件后,判断是否选中文件,如果未选中直接退出方法,选中则执行上传方法

const handleChange = (e) => {
  const files = e.target.files
  const rawFile = files[0]
  if (!rawFile) {
    return
  }
  upload(rawFile)
}

在上传方法upload中,将input.value赋为空

父组件传给上传Excel组件一个上传之前的回调,一个上传成功的回调,如果没有指定上传前回调 直接解析文件,指定则返回true才解析文件

const props = defineProps({
  // 上传之前的回调
  beforeUpload: Function,

  // 上传成功的回调
  onSuccess: Function
})

// 上传事件
const upload = (rawFile) => {
  // 将input的值赋为空
  excelUploadInput.value.value = null

  // 没有指定上传前回调 直接解析文件
  if (!props.beforeUpload) {
    readerData(rawFile)
    return
  }
  // 如果指定上传前的回调 返回true才会解析文件
  const before = props.beforeUpload()
  if (before) {
    readerData(rawFile)
  }
}

解析数据需要用到FileReader

// 异步解析数据
const readerData = (rawFile) => {
  loading.value = true
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    // 该事件在读取操作完成时触发  e:ProgressEvent事件对象
    reader.onload = (e) => {}
    // 开始读取指定文件
    reader.readAsArrayBuffer(rawFile)
  })
}

需要注意reader.onload方法必须写在读取文件方法readAsArrayBuffer之前,在reader.onload方法中需要处理以下逻辑

  1. 获取解析到的数据
  2. 利用 XLSX 对数据进行解析
  3. 获取第一张表格名称
  4. 读取第一张表格的数据
  5. 解析数据表头
  6. 解析数据体
  7. 传入解析之后的数据
  8. loading 处理
  9. 异步完成
reader.onload = (e) => {
      // 1. 获取解析到的数据
      const data = e.target.result
      // 2. 利用 XLSX 对数据进行解析
      const workbook = XLSX.read(data, { type: 'array' })
      // 3. 获取第一张表格名称
      const firstSheetName = workbook.SheetNames[0]
      // 4. 只读取 Sheet1(第一张表格)的数据
      const worksheet = workbook.Sheets[firstSheetName]
      // 5. 解析数据表头
      const header = getHeaderRow(worksheet)
      // 6. 解析数据体
      const results = XLSX.utils.sheet_to_json(worksheet)
      // 7. 传入解析之后的数据
      generateData({ header, results })
      // 8. loading 处理
      loading.value = false
      // 9. 异步完成
      resolve()
    }

解析表头数据getHeaderRow是一个固定方法

新建utils.js

import XLSX from 'xlsx'

// 获取表头通用方式
 
export const getHeaderRow = sheet => {
  const headers = []
  const range = XLSX.utils.decode_range(sheet['!ref'])
  let C
  const R = range.s.r
  for (C = range.s.c; C <= range.e.c; ++C) {
    const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
    let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
    if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
    headers.push(hdr)
  }
  return headers
}

generateData触发上传成功的回调

const generateData = excelData => {
  props.onSuccess && props.onSuccess(excelData)
}

即可在父组件中操作数据

在父组件中

<template>
  <upload-excel :onSuccess="onSuccess"></upload-excel>
</template>

<script setup>
import UploadExcel from '@/components/UploadExcel'

// 数据解析成功之后的回调
const onSuccess = excelData => {
  console.log(excelData)
  // 对数据进行处理
}
</script>

拖拽上传

完成此功能需要了解HTML拖拽API

drop:当元素或选中的文本在可释放目标上被释放时触发

dragover:当元素或选中的文本被拖到一个可释放目标上时触发

dragenter:当拖拽元素或选中的文本到一个可释放目标时触发

在模板中 为div绑定拖拽事件

···
<div
  class="drop"
  @drop.stop.prevent="handleDrop"
  @dragover.stop.prevent="handleDragover"
  @dragenter.stop.prevent="handleDragover"
>
  <i class="el-icon-upload"></i>
  <span>{{ $t('msg.uploadExcel.drop') }}</span>
</div>
···

首先需要了解 DataTransfer.dropEffect

当元素或选中的文本被拖到一个可释放目标上 或者 当拖拽元素或选中的文本到一个可释放目标时

const handleDragover = (e) => {
  // 在新位置生成源项的副本
  e.dataTransfer.dropEffect = 'copy'
}

当元素或选中的文本在可释放目标上被释放时,需要处理以下逻辑

  1. 是否上传中
  2. 是否成功选中文件
  3. 文件格式是否正确

如果以上判断完成符合要求,直接调用上传事件upload即可

const handleDrop = (e) => {
  // 上传中
  if (loading.value) {
    return
  }
  // e.dataTransfer.files 包含数据传输中可用的所有本地文件的列表。如果拖动操作不涉及拖动文件,则此属性为空列表。
  const files = e.dataTransfer.files
  if (files.length !== 1) {
    ElMessage.error('必须选择一个文件')
    return
  }
  const rawFile = files[0]
  // 判断文件格式
  if (!isExcel(rawFile)) {
    ElMessage.error('文件格式错误')
    return
  }
  // 上传事件
  upload(rawFile)
}

完整代码

<template>
  <div class="upload-excel">
    <div class="btn-upload">
      <el-button :loading="loading" type="primary" @click="handleUpload">{{
        $t('msg.uploadExcel.upload')
      }}</el-button>
    </div>
    <input
      type="file"
      ref="excelUploadInput"
      class="excel-upload-input"
      accept=".xlsx, .xls"
      @change="handleChange"
    />
    <!-- https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API -->
    <div
      class="drop"
      @drop.stop.prevent="handleDrop"
      @dragover.stop.prevent="handleDragover"
      @dragenter.stop.prevent="handleDragover"
    >
      <i class="el-icon-upload"></i>
      <span>{{ $t('msg.uploadExcel.drop') }}</span>
    </div>
  </div>
</template>

<script setup>
import XLSX from 'xlsx'
import { getHeaderRow, isExcel } from './utils'
import { ref, defineProps } from 'vue'
import { ElMessage } from 'element-plus'

const props = defineProps({
  // 上传之前的回调
  beforeUpload: Function,

  // 上传成功的回调
  onSuccess: Function
})
console.log(props, XLSX)

const loading = ref(false)

const excelUploadInput = ref(null)

const handleUpload = () => {
  excelUploadInput.value.click()
}

const handleChange = (e) => {
  console.log(e)
  const files = e.target.files
  const rawFile = files[0]
  if (!rawFile) {
    return
  }
  console.log(rawFile)
  upload(rawFile)
}
// 上传事件
const upload = (rawFile) => {
  // 将input的值赋为空
  excelUploadInput.value.value = null

  // 没有指定回调 直接解析文件
  if (!props.beforeUpload) {
    readerData(rawFile)
    return
  }
  // 如果指定上传前的回调 返回true才会解析文件
  const before = props.beforeUpload()
  if (before) {
    readerData(rawFile)
  }
}
// 异步解析数据
const readerData = (rawFile) => {
  loading.value = true
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    // 该事件在读取操作完成时触发  e:ProgressEvent事件对象
    reader.onload = (e) => {
      // 1. 获取解析到的数据
      const data = e.target.result
      // 2. 利用 XLSX 对数据进行解析
      const workbook = XLSX.read(data, { type: 'array' })
      // 3. 获取第一张表格名称
      const firstSheetName = workbook.SheetNames[0]
      // 4. 只读取 Sheet1(第一张表格)的数据
      const worksheet = workbook.Sheets[firstSheetName]
      // 5. 解析数据表头
      const header = getHeaderRow(worksheet)
      // 6. 解析数据体
      const results = XLSX.utils.sheet_to_json(worksheet)
      // 7. 传入解析之后的数据
      generateData({ header, results })
      // 8. loading 处理
      loading.value = false
      // 9. 异步完成
      resolve()
    }
    // 开始读取指定文件
    reader.readAsArrayBuffer(rawFile)
  })
}
const generateData = (excelData) => {
  props.onSuccess && props.onSuccess(excelData)
}

// 拖拽上传 当元素或选中的文本在可释放目标上被释放时触发
const handleDrop = (e) => {
  // 上传中
  if (loading.value) {
    return
  }
  // 包含数据传输中可用的所有本地文件的列表。如果拖动操作不涉及拖动文件,则此属性为空列表。
  const files = e.dataTransfer.files
  if (files.length !== 1) {
    ElMessage.error('必须选择一个文件')
    return
  }
  const rawFile = files[0]
  // 判断文件格式
  if (!isExcel(rawFile)) {
    ElMessage.error('文件格式错误')
    return
  }
  upload(rawFile)
}
// 当元素或选中的文本被拖到一个可释放目标上时触发 && 当拖拽元素或选中的文本到一个可释放目标时触发
const handleDragover = (e) => {
  e.dataTransfer.dropEffect = 'copy'
}
</script>

<style lang="scss" scoped>
.upload-excel {
  display: flex;
  justify-content: center;
  margin-top: 100px;
  .excel-upload-input {
    display: none;
    z-index: -9999;
  }
  .btn-upload,
  .drop {
    border: 1px dashed #bbb;
    width: 350px;
    height: 160px;
    text-align: center;
    line-height: 160px;
  }
  .drop {
    line-height: 60px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    color: #bbb;
    i {
      font-size: 60px;
      display: block;
    }
  }
}
</style>