1. 实现
背景:在表单中使用element-plus实现多张图片上传(限制最多10张),因为还要与其他参数一起上传,所以使用formData格式。
编辑表单回显时得到的是图片路径数组,上传的格式是File,所以要进行一次转换。
<template><el-dialog v-model="visible" :title="`${props.type === 'add' ? '新增' : '编辑'}`" direction="rtl" @close="handleDialogClose":close-on-click-modal="false" class="auto-dialog" :center="true" destroy-on-close><el-form ref="ruleFormRef" :model="ruleForm" label-position="right" label-width="auto"><!-- 省略表单项... --><!-- 上传多张图片 --><el-upload v-model:file-list="pictureList" accept=".png,.jpg,.jpeg" :auto-upload="false"list-type="picture-card" :class="{ 'upload-hide': pictureList?.length === 10 }" :on-change="handleChanges" :on-preview="handlePictureCardPreview"><el-icon><Plus /></el-icon></el-upload><el-dialog v-model="previewVisible"><img w-full :src="dialogImageUrl" alt="Preview Image" /></el-dialog><el-button type="primary" @click="handleSubmit">提交</el-button></el-form></el-dialog>
</template>
<script setup lang="ts">
import type { UploadProps, UploadFile, UploadFiles } from 'element-plus';
import _ from '@lodash';const visible = defineModel<boolean>({ default: false })
const props = defineProps<{type: 'add' | 'mod',id?: string
}>()
// 图片列表
const pictureList = ref<any[]>([])
// 图片预览显示
const previewVisible = ref(false)
// 图片预览url
const dialogImageUrl = ref('')
// 除图片外上传的其他参数
const ruleForm = reactive<Record<string, string>>({code: '',// 省略..
})// 编辑时数据回显
watch(() => visible.value, async (val) => {if (val && props.type === 'mod' && props.id) {await getEditData(props.id)}
}, {deep: true
})
// 上传图片
const handleChanges: UploadProps['onChange'] = (file: UploadFile, fileList: UploadFiles) => {// 文件格式const isPngOrJpg = ['image/png', 'image/jpeg'].includes(file.raw.type)if (!isPngOrJpg) {ElMessage.warning('上传文件格式错误!');return false;}// 文件名重复const isDuplicate = pictureList.value?.some(item => item.name === file.name);if (isDuplicate) {ElMessage.warning('该文件已存在,请重新选择!');// 移除新添加的重复文件fileList.pop();pictureList.value = fileList;} else {pictureList.value = fileList;}
};// 点击图片预览
const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile: UploadFile) => {dialogImageUrl.value = uploadFile.url!previewVisible.value = true
}
// 编辑时数据回显
async function getEditData(id?: number) {try {if (!id) return;await nextTick()const res = await getEditData({ id });if (res.code || _.isEmpty(res?.data)) throw new Error(res?.message);ruleForm.value = _.cloneDeep(res?.data);//表单项回显// 图片列表数据格式要以{url: '', name: ''}格式,才能正确回显pictureList.value = ruleForm.value.pictures?.map((item: any) => {return {url: item,name: item?.url?.split('/').pop()}})} catch (error) {if (error?.code === RESPONSE_CODE.CANCEL) return;ElMessage.error(error?.message);console.log(`[log] - getEditData - error:`, error);}
};
// 路径url转成file文件格式
async function convertUrlToFile(imageUrl: string, fileName: string) {try {// 发起GET请求获取资源,设置responseType为blobconst response = await fetch(imageUrl, { method: 'GET', mode: 'cors' });// 检查请求是否成功if (!response.ok) {throw new Error('图片加载失败!');}// 获取Blob数据const blob = await response.blob();// 创建File对象const file = new File([blob], fileName, { type: blob.type });return file;} catch (error) {console.error('图片url转换Blob失败!', error);return null;}
}
// 提交
async function handleSubmit() {try {// 表单校验省略...const fd = new FormData();// 除图片外的其他参数 (只上传图片,这步跳过)Object.keys(ruleForm).forEach(key => {fd.append(key, ruleForm[key]);});if (!_.isEmpty(pictureList.value)) {return ElMessage.warning('请先选择图片!');} else {const pictures = [] as File[]// 图片列表处理:for (let item of pictureList.value) {// 1. 图片url,需要先将url转换为文件格式,再上传if (!item?.raw) {const fileName = item?.url?.split('/').pop()const res = await convertUrlToFile(item.url, fileName)if (!res) returnpictures.push(res)} else {// 2. 图片文件,直接上传pictures.push(item?.raw)}}pictures.forEach((item) => {fd.append('pictures', item);});}const res = await updateData(fd);if (res?.code) throw new Error(res?.message);ElMessage.success(res?.message );visible.value = false;} catch (error) {console.log(`[log] - handleSubmit - error:`, error);ElMessage.error(error?.message );}
}
</script>
<style scoped>
:deep(.el-upload-list--picture-card) {--el-upload-list-picture-card-size: 94px;width: 100%;max-height: 210px;overflow: auto;
}:deep(.el-upload--picture-card) {--el-upload-picture-card-size: 94px
}.upload-hide {:deep(.el-upload--picture-card) {display: none;}
}
</style>
2. 踩坑记录
问题:在对图片列表遍历后处理时,一开始在forEach
中进行文件格式转换操作,数据项无法插入formData
中,但控制台打印有值。
原错误写法:
if (!_.isEmpty(pictureList.value)) {const pictures = [] as File[]pictureList.value.forEach(async(item) => {if (!item?.raw) {const fileName = item?.url?.split('/').pop()const res = await convertUrlToFile(item.url, fileName)if(!res) returnpictures.push(res)} else {pictures.push(item?.raw)}})console.log(pictures,'pictures');// 这里能打印pictures.forEach((item) => {fd.append('pictures', item);});}
原因:
forEach
是并发执行,在每次迭代时会立即执行指定的回调函数,并且不会等待上一次迭代的结果,所以并不能保证每次convertUrlToFile操作都已完成。
解决方法: 使用promise.all()
确保遍历执行的所有操作都完成后,再执行append
操作。
另外,也可以使用for...of
循环,因为它是用迭代器实现的,每次迭代都会等待 next()
返回,所以可以保证执行的顺序。
if (!_.isEmpty(pictureList.value)) {const promises = pictureList.value.map(async (item) => {if (!item?.raw) {const fileName = item?.url?.split('/').pop();const res = await convertUrlToFile(item.url, fileName);if (!res) return;return res;} else {return item?.raw;}});Promise.all(promises).then((filledPictures) => {const pictures = filledPictures.filter(Boolean) as File[];pictures.forEach((item) => {fd.append('pictures', item);});}).catch((error) => {console.error('Error:', error);});
}
JavaScript 中的 BLOB 数据结构的使用介绍
谈谈JS二进制:File、Blob、FileReader、ArrayBuffer、Base64
Base64、Blob、File 三种类型的相互转换 最详细