业务场景
批量上传文件后端不进行处理,要前端解析文件并用表格展示给用户看,进行格式校验,然后再调用批量上传接口(也就是新增接口将数据以数组的形式传入)
这里最初对文件有限制是xlsx文件,网上有很多xlsx插件可以解析文件,但是其中不可控因素比较多,也比较麻烦,前端还需要做数据校验。所以为了方便,改成上传csv文件供前端进行解析。当然,这样处理也就需要固定模版,可以看我前面写的文章js实现下载本地文件_Humanideal的博客-优快云博客,将csv文件模版放在本地供大家下载之后,根据模版上传文件。
这里是结合ant-desgin-vue的Upload组件写的:
这里是我自己封装的批量上传组件,但是由于牵扯到很多业务场景,直接复制使用是不可取的,可以借鉴一下思路。
代码实现
<script lang="ts" setup>
import type { UploadProps } from 'ant-design-vue'
import { Button, Upload, message } from 'ant-design-vue'
import TableList from '../components/TableList.vue'
import { inject } from 'vue'
interface Props {
isUpload: boolean //这里是用来判断是否上传了文件,没上传之前展示的是按钮和提示文字,上传之后展示的是表格
csvList: any
}
const props = defineProps<Props>()
const emit = defineEmits(['isUploadEvent', 'csvListEvent'])
const beforeUpload: UploadProps['beforeUpload'] = () => {
return false
}
const parseCSV = (csvString: string) => {
// 使用正则表达式匹配 \r\n 或 \n
const rows = csvString.split(/\r?\n/)
// 将第一行解析为标题
const headers = rows[0].split(',')
const set1 = new Set(headers)
const set2 = new Set(columnsHeaders)//这里是用来判断文件表头和表格表头数据是否一样
if (
!(set1.size === set2.size && [...set1].every((value) => set2.has(value)))
) {
message.warning('文件内容不对')
return
}
// 用于存储解析后的对象数组
const objects = []
// 遍历行数组(从第二行开始)
for (let i = 1; i < rows.length; i++) {
// 跳过空行
if (rows[i].trim() === '') {
continue
}
// 将当前行拆分为值数组
const values = rows[i].split(',')
// 创建一个空对象
const obj: any = {}
// 遍历标题数组,并将每个标题与当前行的相应值关联
for (let j = 0; j < headers.length; j++) {
obj[headers[j]] = values[j].trim()
}
// 将新对象添加到对象数组中
objects.push(obj)
}
return objects
}
const changeFile = ({ file, fileList }: any) => {
//校验格式
if (fileList.length <= 0) {
return false
} else if (!/\.(csv)$/.test(fileList[0].name.toLowerCase())) {
message.error('上传格式不正确,请上传csv格式!')
message.config({
duration: 2,
maxCount: 1,
})
return false
}
// 读取表格数据
const fileReader = new FileReader()
fileReader.readAsText(file)
fileReader.onload = (evt) => {
if (fn && fn(parseCSV(`${evt.target?.result}`)) != '') {
emit('csvListEvent', parseCSV(`${evt.target?.result}`))
} else {
emit('csvListEvent', [])
}
}
emit('isUploadEvent', false)
}
let columns: any = inject('columns') || []
columns = columns.value?.filter((item: any) => item.key !== 'action')
let columnsHeaders = columns?.map((item: any) => item.key)
let fn: Function | undefined = inject('testFormat')
const downloadTemplate=inject('downloadTemplate') as (event: MouseEvent) => void
</script>
<template>
<div
class="box flex grid-justify-center flex-items-center flex-col"
v-if="props.isUpload"
>
<Button type="primary" @click="downloadTemplate">{{ $t('下载模版') }}</Button>
<span class="m-[10px] color-[#838383] text-center w-[50%]">{{
$t(
'请先下载批量添加模版,根据指定格式添加确保各个字段类型正确无误,否则将无法添加'
)
}}</span>
<Upload
@change="changeFile"
list-type="picture"
:max-count="1"
:before-upload="beforeUpload"
class="mt-[50px]"
>
<Button type="primary">{{ $t('批量添加') }}</Button>
</Upload>
<span class="mt-[10px] color-[#838383]">{{ $t('仅支持CSV格式') }}</span>
</div>
<div
class="min-h-[200px] min-w-[400px] flex grid-justify-center flex-items-center flex-col"
v-else
>
<TableList
:list="csvList"
:columns="columns"
v-if="csvList != ''"
></TableList>
</div>
</template>
解析csv代码
下面对以上代码进行一下强调,最重要的是解析csv的代码:
const parseCSV = (csvString: string) => {
// 使用正则表达式匹配 \r\n 或 \n
const rows = csvString.split(/\r?\n/)
// 将第一行解析为标题
const headers = rows[0].split(',')
const set1 = new Set(headers)
const set2 = new Set(columnsHeaders)
if (
!(set1.size === set2.size && [...set1].every((value) => set2.has(value)))
) {
message.warning('文件内容不对')
return
}
// 用于存储解析后的对象数组
const objects = []
// 遍历行数组(从第二行开始)
for (let i = 1; i < rows.length; i++) {
// 跳过空行
if (rows[i].trim() === '') {
continue
}
// 将当前行拆分为值数组
const values = rows[i].split(',')
// 创建一个空对象
const obj: any = {}
// 遍历标题数组,并将每个标题与当前行的相应值关联
for (let j = 0; j < headers.length; j++) {
obj[headers[j]] = values[j].trim()
}
// 将新对象添加到对象数组中
objects.push(obj)
}
return objects
}
csv文件都是以,分割开的,每一行直接会有\r或\n进行换行。根据这一特性我们可以将每一行线分割出来。然后将第一行解析为标题,当然,这里也可以对数据进行trim()。如果自己在处理csv文件模版的时候,严格控制前后没有空格也可以不写。
针对于set1和set2这几行代码,是因为需要对csv的表头和table的表头进行对比校验。也就是说如果csv文件如果缺失一些字段或者与表头字段不一致就阻断其向后端发送请求。因为是后台管理系统,所以table表格一般是用来展示数据,然后对这些列表数据进行添加上传,才有了这一步处理。这里的headers和columnsHeaders都是['xxx','xxxx']格式。
需要注意的是obj[headers[j]] = values[j].trim(),这里用户输入的时候容易输入空格,导致字段可能和添加时的一些校验有误,所以加上trim()比较好。
TableList这个是我自己封装的一个组件,实际上就是用table组件进行的二次封装。也就是展示一下表格数据,为了更加清晰的看到上传的数据。
FileReader.readAsText()开始读取指定的Blob中的内容。一旦完成,result 属性中将包含一个字符串以表示所读取的文件内容。FileReader.onload处理 load 事件。该事件在读取操作完成时触发。这样就可以得到读取到的csv文件内容,然后进行解析。