1、应用场景
在实现多张图片上传的需求中,例如相册上传、发帖传照片等,实现照片多选以及链式上传效果。
2、实现思路
例如你有一组接口需要串行执行,首先你可能会想到使用await
const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];
for (const requestItem of requestAry) {
await requestItem();
}
但是如果使用promise的写法,那么你可以使用then函数来串联多个promise,从而实现串行执行。
const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];
const finallyPromise = requestAry.reduce(
(currentPromise, nextRequest) => currentPromise.then(() => nextRequest()),
Promise.resolve() // 创建一个初始promise,用于链接数组内的promise
);
由此操作为基础,实现所需UI效果
3、具体实现代码(vue2)
import tiny from "./tinyImage";
import { mkQiniuPublicUrl } from "@/api/common";
export default {
props: {
fileList: {
type: Array,
default() {
return [];
}
},
limit: Number,
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
images: [],
mime: ["image/*"],
uploadLength: 0,
isUploading: false
};
},
watch: {
fileList: {
handler(){
if (!this.images.length) {
this.images = this.fileList;
}
},
immediate: true
}
},
beforeDestroy() {
// 需要在销毁这个组件之前,释放掉图片占用的内容
this.images.forEach(img => img.src && URL.revokeObjectURL(img.src));
},
methods: {
// 删除图片的操作
actClose(index) {
// 禁用状态不可删除
if (this.disabled) return;
// 上传状态不可删除
if (this.isUploading) {
this.$toast("正在上传,请勿操作");
return;
}
this.images[index].src && URL.revokeObjectURL(this.images[index].src);
this.images.splice(index, 1);
this.triggerChange();
},
// 接口上传的方法
apiUpload(file, local) {
return async () => {
let formData = new FormData();
const compressFile = await tiny.compress(file);
formData.append("img", compressFile);
return new Promise((resolve, reject) => {
mkQiniuPublicUrl(formData, (cur, total) => {
// 七牛云API
local.percent = Math.ceil((cur / total) * 100);
})
.then(({ errorCode, errorMsg, responseData }) => {
if (errorCode === 0) {
const { url, width, height } = responseData;
local.u = url;
local.w = width;
local.h = height;
resolve();
} else {
resolve();
this.$toast(errorMsg);
}
})
.catch(() => {
reject("网络请求失败");
})
.finally(() => {
local.percent = 100
if (0 === --this.uploadLength) {
this.triggerChange();
setTimeout(() => {
this.isUploading = false;
this.target.value = "";
}, 0);
}
});
});
};
},
// 触发变更
triggerChange() {
this.$emit(
"change",
this.images.map(item => ({
u: item.u,
h: item.h,
w: item.w
}))
);
},
// 本地图片change后立马预览
imageViewLocal(file) {
const tmp = {
src: URL.createObjectURL(file),
percent: 0,
u: "",
h: 0,
w: 0
};
this.images.push(tmp);
return tmp;
},
// 修建修改之后
fileChangeHandler(e) {
const target = (this.target = e.target);
if (!target.files || target.files.length == 0) return;
this.uploadLength = target.files.length;
// 上传的文件长度 + 目前已经有的文件长度 总和
if (this.uploadLength + this.images.length > this.limit) {
this.$toast(`不能超过${this.limit}张图片`);
this.uploadLength = 0;
return;
}
const promiseArr = [];
Object.keys(target.files).forEach(k => {
let file = target.files[k];
promiseArr.push(this.apiUpload(file, this.imageViewLocal(file)));
});
// 开始上传
this.isUploading = true;
promiseArr.reduce((acc, cur) => {
return acc.finally(() => cur());
}, Promise.resolve());
}
},
computed: {
enabledUpload() {
return this.images.length < this.limit;
}
}
};
html和css如下图
.upload-wrap {
position: relative;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
overflow: hidden;
.upload-item {
position: relative;
margin-right: 8px;
margin-bottom: 10px;
width: 80px;
height: 80px;
background: #efefef;
border-radius: 8px;
overflow: hidden;
&:nth-child(4n) {
margin-right: 0;
}
.image {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100%;
z-index: 1;
}
.close {
position: absolute;
right: 0;
top: 0;
width: 22px;
height: 22px;
z-index: 3;
}
.loading {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 4;
background: rgba(0, 0, 0, 0.65);
.loading-progress {
width: 40px;
height: 6px;
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
background: #ccc;
border-radius: 3px;
.progress {
width: 0;
height: 100%;
border-radius: 3px;
background: #fff;
}
}
}
}
.pub-upload {
position: relative;
width: 80px;
height: 80px;
margin-bottom: 10px;
overflow: hidden;
border-radius: 8px;
input {
opacity: 0;
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 2;
}
.img {
position: absolute;
height: 100%;
width: 100%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
}
/* prettier-ignore */
@media screen and (min-width: 414px) and (orientation: landscape) {
.upload-wrap {
.upload-item {
margin-right: 8PX;
margin-bottom: 10PX;
width: 80PX;
height: 80PX;
border-radius: 8PX;
&:nth-child(4n) {
margin-right: 8PX;
}
.close {
width: 22PX;
height: 22PX;
cursor: pointer;
transition: all .3s;
&:hover{
transform: scale(1.1,1.1);
}
}
.loading {
.loading-progress {
width: 40PX;
height: 6PX;
border-radius: 3PX;
.progress {
border-radius: 3PX;
}
}
}
}
.pub-upload {
width: 80PX;
height: 80PX;
margin-bottom: 10PX;
border-radius: 8PX;
input{
cursor: pointer;
}
}
}
}