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为blob
const 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) return
pictures.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) return
pictures.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 三种类型的相互转换 最详细