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

news/2024/9/23 9:34:07/

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">javascript">
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中,但控制台打印有值。
原错误写法:

javascript">        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() 返回,所以可以保证执行的顺序。

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


http://www.ppmy.cn/news/1463935.html

相关文章

LeetCode 279 —— 完全平方数

阅读目录 1. 题目2. 解题思路3. 代码实现 1. 题目 2. 解题思路 此图利用动态规划进行求解&#xff0c;首先&#xff0c;我们求出小于 n n n 的所有完全平方数&#xff0c;存放在数组 squareNums 中。 定义 dp[n] 为和为 n n n 的完全平方数的最小数量&#xff0c;那么有状态…

[Unity报错] The type or namespace name ‘Newtonsoft‘ could not be found

简介 Unity在跑别人的代码时&#xff0c;控制台报了以下错误 The type or namespace name Newtonsoft could not be found 鉴于这块资料较少&#xff0c;写一下教程帮助后来者。 报错的原因主要是因为缺少Newtonsoft.json这个包&#xff0c;导致Unity在using该库时出现错误。…

Spring MVC+mybatis 项目入门:旅游网(二) dispatcher与controller与Spring MVC

个人博客&#xff1a;Spring MVCmybatis 项目入门:旅游网&#xff08;二&#xff09;dispatcher与controller与Spring MVC | iwtss blog 先看这个&#xff01; 这是18年的文章&#xff0c;回收站里恢复的&#xff0c;现阶段看基本是没有参考意义的&#xff0c;技术老旧脱离时代…

鸿蒙OS开发:【一次开发,多端部署】(音乐专辑主页)

一多音乐专辑主页 介绍 本示例展示了音乐专辑主页。 头部返回栏: 因元素单一、位置固定在顶部&#xff0c;因此适合采用自适应拉伸&#xff0c;充分利用顶部区域。专辑封面: 使用栅格组件控制占比&#xff0c;在小尺寸屏幕下封面图与歌单描述在同一行。歌曲列表: 使用栅格组…

Java GC问题排查的一些个人总结和问题复盘

个人博客 Java GC问题排查的一些个人总结和问题复盘 | iwts’s blog 是否存在GC问题判断指标 有的比较明显&#xff0c;比如发布上线后内存直接就起飞了&#xff0c;这种也是比较好排查的&#xff0c;也是最多的。如果单纯从优化角度&#xff0c;看当前应用是否需要优化&…

代码随想录算法训练营第二十一天| 530. 二叉搜索树的最小绝对差、501. 二叉搜索树中的众数、236. 二叉树的最近公共祖先

[LeetCode] 530. 二叉搜索树的最小绝对差 [LeetCode] 530. 二叉搜索树的最小绝对差 文章解释 [LeetCode] 530. 二叉搜索树的最小绝对差 视频解释 题目: 给你一个二叉搜索树的根节点 root &#xff0c;返回 树中任意两不同节点值之间的最小差值 。 差值是一个正数&#xff0c;其…

Unity3D MMORPG 主城角色动画控制与消息触发详解

Unity3D是一款强大的游戏开发引擎&#xff0c;它提供了丰富的功能和工具&#xff0c;使开发者能够轻松创建出高质量的游戏。其中&#xff0c;角色动画控制和消息触发是游戏开发中非常重要的一部分&#xff0c;它们可以让游戏角色表现出更加生动和多样的动作&#xff0c;同时也能…

C++算术运算和自增自减运算

一 引言 表示运算的符号称为运算符。 算术运算&#xff1b; 比较运算&#xff1b; 逻辑运算&#xff1b; 位运算&#xff1b; 1 算术运算 算术运算包括加、减、乘、除、乘方、指数、对数、三角函数、求余函数&#xff0c;这些都是算术运算。 C中用、-、*、/、%分别表示加、减…