在实际开发中,经常会遇到一些文件上传处理,但往往还会伴随一些上传后的异步处理以及异步条件下批量处理。在解决这种矛盾事件时,会有点头痛。
问题描述
本人在开发时有一个需求,实现上传图片,提取图片信息,并自动推送图片信息到数据库,这本来并不复杂,但是加了前提条件,就有些头痛了。现在的情况是,提取图片并推送图片已有现成的接口(不可以更改接口,这是一个公用的接口,不会因为需求随便变动),但是该接口每次只能处理一张图片,且提取完成后接口会自动推送图片信息到数据库,前端只需在上传成功后刷新页面即可。但问题是,本来接口提取的成功率就不是很大,现在需求方又要求必须支持批量导入,其实导入方面也是没什么问题,问题就在每次导入成功或失败后前端如何友好的提示,接口每完成一次前端提示一次,这明显使用起来体验不是很好。
解决方案
现在的实现方式是,做一个列表,每次选完文件后,展示这个列表,并给每个文件一个loading状态,在相应的文件处理完后,更改其状态,在列表中所有的文件都处理完后,通过Promise.all()在进行下一步处理,这样解决了前端频繁弹出提示框的问题。效果图类似如下(示意效果没有做过多的css处理):
实现这种效果很简单,知识点就一点就是 Promise.all()的应用。
实现方式(Vue)
首先通过input标签创建出类型为file的文件上传按钮
<input type="file" multiple accept="image/*, .pdf" @change="getFileList"/>
multiple属性控制是否支持多选,accept控制文件的类型(image/* 所有的图片类型)change事件 其次展示列表 注:如果想使用谷歌的icon需要在最开始的index.html中引入,在main.js中引用也可以,需要export导出
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
想实现loading动画请参考:css实现loading动画 列表:
<div class="listarea"> <!-- 通过for循环循环列表 --> <template v-for="(item, i) in fileList"> <div class="filelist" :key="i"> <div class="file" style="width: 300px"> {{ item.name }} </div> <div class="file">{{ item.type }}</div> <div class="file">{{ item.size }}</div> <!-- 通过文件的状态判断显示,这里loading效果可以用文字表示,我上图的loading效果是通过css动画单独写的一个文件 --> <div class="file"> <div v-if="item.status === 'loading'" class="loading-icon"> <div> <loading-icon class="icon" /> </div> <div style="width: 70px"> <span>正在上传</span> </div> </div> <div v-if="item.status === 'success'" class="loading-icon" style="color: #19be6b" > <div style="display: table; vertical-align: middle"> <i class="material-icons" style="font-size: 24px" >check_circle</i > </div> <div style="width: 70px"> <span>上传成功</span> </div> </div> <div v-if="item.status === 'error'" class="loading-icon" style="color: #ed4014" > <div style="display: table; vertical-align: middle"> <i class="material-icons" style="font-size: 24px" >highlight_off</i > </div> <div style="width: 70px"> <span>上传失败</span> </div> </div> </div> <div class="file" style="width: 300px">{{ item.message }}</div> </div> </template> </div>
在js中主要两个函数一个是模拟后端异步接口处理函数,一个是change事件触发函数 异步接口:
asyncFun(file, callback) { console.log(file); // 假设对file文件处理 const n = Math.floor(Math.random() * 10 + 1); // 生成一个随机数 // 模拟异步 setTimeout(() => { if (n % 2 === 0) { const res = { success: true, message: "上传成功", }; return callback(res); } else { const res = { success: false, message: "上传失败, 错误编码" + n, }; return callback(res); } }, n * 1000); },
change事件触发函数
getFileList(e) { this.completeText = ""; this.fileList = []; const files = e.target.files; // 获取文件列表 if (files.length) { let promiseArr = []; for (let file of files) { const fileObj = { name: file.name, // 文件name type: file.type, // 文件类型 size: parseFloat(file.size / 1024 / 1024).toFixed(2) + "MB", // 文件大小 status: "loading", // 文件状态 人为赋值 message: "", // 报错信息 }; this.fileList.push(fileObj); // 展示列表 // 异步接收后端接口返回 let fileUpload = new Promise((resolve) => { this.asyncFun(file, (res) => { if (res.success) { fileObj.status = "success"; // 更改文件上传后状态 fileObj.message = res.message; } else { fileObj.status = "error"; // 更改文件上传后状态 fileObj.message = res.message; } resolve(true); }); }); promiseArr.push(fileUpload); } Promise.all(promiseArr).then(() => { // 异步函数处理完下一步处理如:刷新列表 this.completeText = "上传全部完成"; }); } },
完整代码:
<template> <div class="main"> <div> <input type="file" multiple accept="image/*, .pdf" @change="getFileList" /> <span style="color: red">{{ completeText }}</span> </div> <div class="listarea"> <!-- <loading-icon class="icon" /> --> <template v-for="(item, i) in fileList"> <div class="filelist" :key="i"> <div class="file" style="width: 300px"> {{ item.name }} </div> <div class="file">{{ item.type }}</div> <div class="file">{{ item.size }}</div> <div class="file"> <div v-if="item.status === 'loading'" class="loading-icon"> <div> <loading-icon class="icon" /> </div> <div style="width: 70px"> <span>正在上传</span> </div> </div> <div v-if="item.status === 'success'" class="loading-icon" style="color: #19be6b" > <div style="display: table; vertical-align: middle"> <i class="material-icons" style="font-size: 24px" >check_circle</i > </div> <div style="width: 70px"> <span>上传成功</span> </div> </div> <div v-if="item.status === 'error'" class="loading-icon" style="color: #ed4014" > <div style="display: table; vertical-align: middle"> <i class="material-icons" style="font-size: 24px" >highlight_off</i > </div> <div style="width: 70px"> <span>上传失败</span> </div> </div> </div> <div class="file" style="width: 300px">{{ item.message }}</div> </div> </template> </div> </div></template><script>import loadingIcon from "./loadingIcon.vue";export default { components: { loadingIcon }, name: "FileUpload", data() { return { fileList: [], completeText: "", }; }, methods: { getFileList(e) { this.completeText = ""; this.fileList = []; const files = e.target.files; if (files.length) { let promiseArr = []; for (let file of files) { const fileObj = { name: file.name, type: file.type, size: parseFloat(file.size / 1024 / 1024).toFixed(2) + "MB", status: "loading", message: "", }; this.fileList.push(fileObj); let fileUpload = new Promise((resolve) => { this.asyncFun(file, (res) => { console.log("res", res); if (res.success) { fileObj.status = "success"; fileObj.message = res.message; } else { fileObj.status = "error"; fileObj.message = res.message; } resolve(true); }); }); promiseArr.push(fileUpload); } Promise.all(promiseArr).then(() => { this.completeText = "上传全部完成"; }); } }, asyncFun(file, callback) { console.log(file); const n = Math.floor(Math.random() * 10 + 1); setTimeout(() => { if (n % 2 === 0) { const res = { success: true, message: "上传成功", }; return callback(res); } else { const res = { success: false, message: "上传失败, 错误编码" + n, }; return callback(res); } }, n * 1000); }, },};</script><style scoped>.main { width: 1000px;}.listarea { margin: 8px; width: 100%; height: 500px; border: 1px solid #ccc; border-radius: 8px; overflow-y: auto;}.filelist { display: flex; justify-content: space-between; text-align: left;}.filelist .file { margin: 8px; padding: 8px;}.loading-icon { display: flex; justify-content: space-between;}.icon { width: 20px; height: 20px; display: inline-block;}</style>原文:https://juejin.cn/post/7098264067046899743