背景
上传作为一种常见的前端交互场景,在小程序端也不例外。这里总结了一些原生微信小程序中使用vanweap组件进行上传业务开发的场景。
技术栈:原生微信小程序 + ant/weapp(1.10.23)
Uploader组件介绍
用于将本地的图片或文件上传至服务器,并在上传过程中展示预览图和上传进度。目前 Uploader 组件不包含将文件上传至服务器的接口逻辑(该步骤需要自行实现)。
引入
{
"component": true,
"usingComponents": {
"van-cell-group": "@vant/weapp/cell-group/index",
"van-uploader": "@vant/weapp/uploader/index",
"van-loading": "@vant/weapp/loading/index"
}
}
自定义一个组件
尽管vant的样式已经很好看了,但是还是不能满足UI的需求。假设UI的需求如下
我们可以看到,上传前的样式还好处理,但是上传中和上传后,就不太符合要求了。在这里使用自定义上传标志位枚区分上传状态。而且通过max-count属性可以限制上传文件的数量,上传数量达到限制后,会自动隐藏上传区域,所以不需要关注全部上传完成之后如何处理上传组件。
isUploadSucc:{
success: 1,
uploading: 0
}
部分关键代码如下:
wxml
<!-- 上传组 -->
<van-cell-group custom-class="upload-cell-group no-margin-left">
<view wx:for="{{ fileList }}" wx:key="keyId">
<!-- 上传成功 -->
<view wx:if="{{item.isUploadSucc === 1}}" class="upload-img-wrapper {{item.isDelted ? 'del__out' : ''}}">
<image src="{{ item.ossUrl }}" mode="aspectFit" class="pic_choose" mode="aspectFit" data-index="{{ index }}" bind:tap="previewImage" />
<view class="del-btn-wrapper ">
<!-- 删除按钮 -->
<image src="/asserts/images/del-btn.png" class="del-btn {{item.isDelted ? 'del__show' : ''}}" data-index="{{ index }}" catchtap="delImgBefore" />
</view>
</view>
<!-- 上传中 -->
<view wx:elif="{{item.isUploadSucc === 0}}" class="upload-img-wrapper spinner-warpper">
<van-loading type="spinner" size="20" vertical >加载中</van-loading>
</view>
</view>
<van-uploader
max-count="5"
multiple
accept="image"
file-list="{{ fileList }}"
bind:after-read="afterRead"
preview-image="{{false}}"
custom-style="margin-left: 20rpx;"
deletable="{{ false }}"
use-before-read
bind:before-read="beforeRead"
>
<view class="pic_choose">
<image src="/asserts/images/pic_choose.png" ></image>
<view class="upload-placeholder">上传图片</view>
</view>
<text slot="error">加载失败</text>
</van-uploader>
</van-cell-group>
js
注意:这里有一个坑,wx会把gif格式的文件也当成图片,如果业务不允许上传动图的话,需要使用
beforeRead钩子函数进行二次过滤。
// components/imgUploadGroup/imgUploadGroup.js
import { apiPost } from "../../apis/address";
Component({
options: {
styleIsolation: "shared",
},
properties: {},
data: {
fileList: [],
},
behaviors: ["wx://component-export"],
export() {
return { fileList: this.data.fileList };
},
methods: {
// 前置钩子:校验上传文件类型 => 限制为图片
beforeRead(event) {
const { file, callback } = event.detail;
const regex = /\.gif$/i;
for (let i = 0; i < file.length; i++) {
if (regex.test(file[i].url)) {
callback(false);
wx.showToast({
title: "不支持上传动图类型的图片,请重新上传",
icon: "none",
duration: 2000,
});
return ;
}
}
callback(true);
console.log(file, callback);
},
// 上传钩子
async afterRead(event) {
const { file } = event.detail; // 是个数组
// 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
// loading
const _fileList = [...this.data.fileList];
file.forEach((item) => {
item.keyId = Math.random() * 10;
item.isDelted = false;
item.isUploadSucc = 0; // 开始上传
_fileList.push(item);
});
this.setData({ fileList: _fileList }); // 视图更新为loading状态
// 上传
const files = file.map((item) => item.tempFilePath);
const upLoadURLs = await this.multiUploadHandler(files);
// 上传成功后,再次更新视图
let uploadFileIndex = 0;
_fileList.forEach((item, index) => {
if (!item.ossUrl) {
item.ossUrl = upLoadURLs[uploadFileIndex].value; // type: uploadType, value: imgUrl // 使用上传后的阿里云oss地址
item.isUploadSucc = 1; // 更新标志位
uploadFileIndex += 1;
}
});
this.setData({ fileList: [..._fileList] });
},
uploadSingleFile(fileTmpUrl) {
return new Promise((resolve, reject) => {
wx.uploadFile({
url: `${apiPost}/api-faq/auth/file/upload`,
filePath: fileTmpUrl,
name: "file",
success(res) {
const data = JSON.parse(res.data);
if (data.code === 0) {
resolve(data.data);
return;
}
reject(new Error(data.msg));
},
fail(err) {
reject(err);
},
});
});
},
// 多文件上传,使用promise数组处理,这里不必等待所有成功,如果业务需要,可以在fileList中添加status为failed标识上传失败状态。
multiUploadHandler(fileList) {
return Promise.allSettled(
fileList.map((fileUrl) => this.uploadSingleFile(fileUrl))
);
},
// 删除图片
delImgHandle(e) {
const { index } = e.currentTarget.dataset;
const _fileList = [...this.data.fileList];
_fileList[index].isDelted = !_fileList[index].isDelted;
this.setData({ fileList: _fileList });
// 等待动画
setTimeout(() => {
const _fileList = [...this.data.fileList];
_fileList.splice(index, 1);
this.setData({ fileList: _fileList });
}, 900);
},
// 删除前确认
delImgBefore(e) {
wx.showModal({
title: "删除提示",
content: "请确认是否删除该图片",
success: (res) => {
this.delImgHandle(e);
},
});
},
// 预览上传的图片
previewImage(e) {
const { index } = e.currentTarget.dataset;
wx.previewImage({
current: this.data.fileList[index].url, // 当前显示图片的http链接
urls: this.data.fileList.map((item) => item.url), // 需要预览的图片http链接列表
});
},
},
});
less
这里只记录删除动画
// 删除按钮
.del-btn-wrapper {
width: 45rpx;
height: 45rpx;
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
image {
.img-fill()
}
}
// 删除按钮的动效
.del-btn.del__show {
animation: del__show__rotate 0.7s forwards ease-in-out;
}
@keyframes del__show__rotate {
0% {
transform-origin: 50% 50%;
transform: rotate(0deg);
}
100% {
transform-origin: 50% 50%;
transform: rotate(180deg);
}
}
// 上传的预览图隐藏
.del__out {
transition: opacity 0.8s ease-in-out;
opacity: 0;
}
操作展示
其他思考
微信小程序中,对于图片的操作,除了上传,还有就是在接入im功能的时候需要发送图片。微信自带的sdk很强大,可以很方便的对图片进行操作。
以下是在im功能(即时通讯)场景下对于图片的操作
sendImageMessage(sourceType) {
const maxSize = 20480000
wx.chooseMedia({
mediaType: ['image'],
sourceType: album,
count: 1,
success: (res) => {
console.log(res)
const size = res.tempFiles[0].size
if (size > maxSize) {
wx.showToast({
title: '大于20M图片不支持发送',
icon: 'none',
})
return
}
// 消息实例
const message = wx.$TUIKit.createImageMessage({
to: this.getToAccount(),
conversationType: this.data.conversation.type,
payload: {
file: res,
},
onProgress: (percent) => {
message.percent = percent
},
})
this.$sendTIMMessage(message)
},
fail: (err) => {
console.error(err)
},
})
},
$sendTIMMessage(message) {
// im 发送图片类型消息
wx.$TUIKit
.sendMessage(message, {
offlinePushInfo: {
disablePush: true,
},
})
.then((res) => {
// do something
})
.catch((error) => {
// do something
})
},
最后
最近在搞im,第一次做这种类型的业务,做的是甲方(pc)乙方(小程序)和一个机器人做智能客服的群聊形式,之后有时间记录一下其中的坑。