所谓:一图胜千言。这话说明了图片描述事物的能力是非常强大的(怪不得我们可以用表情包聊一整天),尤其现在的手机拍照功能那么方便,用户对使用拍照和相册的需求日益上升。因此,在我们的移动应用中,可能经常会碰到这样的功能需求,需要为用户提供在相册中选择照片或者拍照片并上传的功能。
例如下图所示的应用界面,这是一个比较典型的创建帖子或问答等内容的表单,用户可以填写标题和正文,并从自己的手机相册中选择3张图片(或直接通过摄像头拍摄),且当点击缩略图时,可以全屏预览查看这些图片:
像这样一个带图片上传和预览功能的表单,在移动app中是比较常见的。那么在微信小程序中该如何来实现呢?且看我们一步步来构建这样的功能。
标题和正文输入框
对于这个表单,我们首先来创建上部的2个输入区域:标题和正文输入区。我们使用了一个单行输入框组件<input>
来接收标题的输入,而使用一个多行输入组件<textarea>
来接收正文的输入,并且为它们分别设置了maxlength
属性来作最大输入字符数的限制。然后,为了更加直观,我们还为这2个输入区域分别放置了一个展示当前已输入字符数统计状态的标签。
界面的WXML代码大致如下所示:
<view class="question-input-area">
<!-- 问题标题区域 -->
<view class="question-title-wrap">
<!-- 标题输入框 -->
<input class="question-title" placeholder="请输入标题" maxlength="40" placeholder-style="color:#b3b3b3;font-size:18px;" bindinput="handleTitleInput"></input>
<!-- 标题输入字数统计 -->
<view class="title-input-counter">{{titleCount}}/40</view>
</view>
<!-- 问题正文区域 -->
<view class="weui-cells weui-cells_after-title">
<view class="weui-cell">
<view class="weui-cell__bd">
<!-- 多行输入框 -->
<textarea class="weui-textarea" placeholder="请输入问题的正文内容。" maxlength="500" placeholder-style="color:#b3b3b3;font-size:14px;" style="height: 12rem" bindinput="handleContentInput" />
<!-- 正文输入字数统计 -->
<view class="weui-textarea-counter">{{contentCount}}/500</view>
</view>
</view>
</view>
</view>
而与之对应的Page代码如下:
import { $init, $digest } from '../../utils/common.util'
Page({
data: {
titleCount: 0, //标题字数
contentCount: 0, //正文字数
title: '', //标题内容
content: '' //正文内容
},
onLoad(options) {
$init(this)
},
handleTitleInput(e) {
const value = e.detail.value
this.data.title = value
this.data.titleCount = value.length //计算已输入的标题字数
$digest(this)
},
handleContentInput(e) {
const value = e.detail.value
this.data.content = value
this.data.contentCount = value.length //计算已输入的正文字数
$digest(this)
}
}
【注意】有人可能会对这里的一些代码觉得奇怪,这段JavaScript代码中出现的$init和$digest是什么?其实它是一个通过对象深层比较,将Page的data对象中的数据进行批量、按需更新到视图层WXML中的一个功能。对初学者来说,你暂且可以认为是在每个调用$digest(this)的地方调用了一次this.setData()的操作吧,方便理解。
通过上面的两段代码,我们就已经把表单的输入框部分创建出来了。下面,我们要进入本文的关键功能部分。
选择和预览图片、以及上传图片
微信小程序提供的众多API中,wx.chooseImage
函数就是用来访问手机相册或摄像头的。调用该函数后,界面下方会呼出一个菜单,可以分别选择进入相册挑选已有照片或是打开摄像头进行拍照:
二话不说继续上代码!我们往WXML里新添一个按钮,点击该按钮就会触发wx.chooseImage
的调用:
<button
type="default" size="mini" bindtap="chooseImage"
wx:if="{{images.length < 3}}"
>添加图片</button>
import { $init, $digest } from '../../utils/common.util'
Page({
data: {
images: []
},
onLoad(options) {
$init(this)
},
chooseImage(e) {
wx.chooseImage({
sizeType: ['original', 'compressed'], //可选择原图或压缩后的图片
sourceType: ['album', 'camera'], //可选择性开放访问相册、相机
success: res => {
const images = this.data.images.concat(res.tempFilePaths)
// 限制最多只能留下3张照片
this.data.images = images.length <= 3 ? images : images.slice(0, 3)
$digest(this)
}
})
}
}
通过以上代码,我们就可以开始把玩起手机相册和摄像头了。但是目前选择了照片或拍了照之后,在表单界面上并不能看到。下面我们就要继续做选择图片后的展示工作。
我们通过wx:for
语法,将我们之前存在images
数组中的照片展示到界面上来:
<view class="question-images">
<block wx:for="{{images}}" wx:key="*this">
<view class="q-image-wrap">
<!-- 图片缩略图 -->
<image class="q-image" src="{{item}}" mode="aspectFill" data-idx="{{index}}" bindtap="handleImagePreview"></image>
<!-- 移除图片的按钮 -->
<view class="q-image-remover" data-idx="{{index}}" bindtap="removeImage">删除</view>
</view>
</block>
</view>
我们在每个缩略图元素上绑定了一个点击事件,当点击缩略图的时候,会调用微信小程序提供的预览图片的方法wx.previewImage
进行全屏预览,用户可以左右滑动查看选中图片列表中的大图。另外,在每个缩略图的下方,还有一个删除按钮,用于移除所选的图片,方便重新选图。下面是对应的JS代码:
import { $init, $digest } from '../../utils/common.util'
Page({
data: {
images: []
},
onLoad(options) {
$init(this)
},
removeImage(e) {
const idx = e.target.dataset.idx
this.data.images.splice(idx, 1)
$digest(this)
},
handleImagePreview(e) {
const idx = e.target.dataset.idx
const images = this.data.images
wx.previewImage({
current: images[idx], //当前预览的图片
urls: images, //所有要预览的图片
})
}
}
终于,只剩下最后一件事,就是提交表单数据及上传图片到后端,将的这些数据组成一个完整的问题,保存进数据库。
对于我们的WXML,还缺最后这个提交按钮呢!立马补上吧:
<!-- 提交表单按钮 -->
<button class="weui-btn" type="primary" bindtap="submitForm">提交</button>
然后就是这Page中的集大成者(大杂烩吧,哈哈)submitForm
函数:
import { $init, $digest } from '../../utils/common.util'
Page({
data: {
images: []
},
onLoad(options) {
$init(this)
},
submitForm(e) {
const title = this.data.title
const content = this.data.content
if (title && content) {
const arr = []
//将选择的图片组成一个Promise数组,准备进行并行上传
for (let path of this.data.images) {
arr.push(wxUploadFile({
url: config.urls.question + '/image/upload',
filePath: path,
name: 'qimg',
}))
}
wx.showLoading({
title: '正在创建...',
mask: true
})
// 开始并行上传图片
Promise.all(arr).then(res => {
// 上传成功,获取这些图片在服务器上的地址,组成一个数组
return res.map(item => JSON.parse(item.data).url)
}).catch(err => {
console.log(">>>> upload images error:", err)
}).then(urls => {
// 调用保存问题的后端接口
return createQuestion({
title: title,
content: content,
images: urls
})
}).then(res => {
// 保存问题成功,返回上一页(通常是一个问题列表页)
const pages = getCurrentPages();
const currPage = pages[pages.length - 1];
const prevPage = pages[pages.length - 2];
// 将新创建的问题,添加到前一页(问题列表页)第一行
prevPage.data.questions.unshift(res)
$digest(prevPage)
wx.navigateBack()
}).catch(err => {
console.log(">>>> create question error:", err)
}).then(() => {
wx.hideLoading()
})
}
}
}
这个提交保存函数的主要流程是:
- 将图片分别通过文件上传API
wx.uploadFile
进行上传,并返回上传后的图片地址备用; - 接着将标题、正文、以及刚才的图片地址一并通过调用后端创建问题的API,保存到数据库中。
- 保存完毕,返回问题列表页
在我的这个实现代码中,是将上传文件和创建问题分别通过2个后端API来进行的,其实wx.uploadFile
除了上传文件,同时也可以携带其他表单数据,这样一来,就可以用单一API来实现。具体选择哪一种方式,主要看你们实际的后端API的设计了。
最后,附上比较完整的源代码供大家参考吧。