多张图片上传、图片回显、url路径转成File文件

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 三种类型的相互转换 最详细

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值