在实际开发中,经常会遇到一些文件上传处理,但往往还会伴随一些上传后的异步处理以及异步条件下批量处理。在解决这种矛盾事件时,会有点头痛。
问题描述
本人在开发时有一个需求,实现上传图片,提取图片信息,并自动推送图片信息到数据库,这本来并不复杂,但是加了前提条件,就有些头痛了。现在的情况是,提取图片并推送图片已有现成的接口(不可以更改接口,这是一个公用的接口,不会因为需求随便变动),但是该接口每次只能处理一张图片,且提取完成后接口会自动推送图片信息到数据库,前端只需在上传成功后刷新页面即可。但问题是,本来接口提取的成功率就不是很大,现在需求方又要求必须支持批量导入,其实导入方面也是没什么问题,问题就在每次导入成功或失败后前端如何友好的提示,接口每完成一次前端提示一次,这明显使用起来体验不是很好。
解决方案
现在的实现方式是,做一个列表,每次选完文件后,展示这个列表,并给每个文件一个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>