开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情
前言
通常项目中可以通过导入excel文件对数据进行增加,本文记录该功能的实现
实现
基本样式
需要实现两种导入形式,基本样式如下
点击上传使用el-button和input即可
新建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>
逻辑处理
导入方式分为两种,点击上传和拖拽上传
点击上传
点击上传按钮后,触发input的change事件
使用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方法中需要处理以下逻辑
- 获取解析到的数据
- 利用 XLSX 对数据进行解析
- 获取第一张表格名称
- 读取第一张表格的数据
- 解析数据表头
- 解析数据体
- 传入解析之后的数据
- loading 处理
- 异步完成
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'
}
当元素或选中的文本在可释放目标上被释放时,需要处理以下逻辑
- 是否上传中
- 是否成功选中文件
- 文件格式是否正确
如果以上判断完成符合要求,直接调用上传事件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>