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 三種類型的相互轉換 最詳細