最近做了一个小程序项目,这个小程序是搭配APP端使用的,大概需求是这样的,用户用微信的扫一扫或小程序首页的扫一扫都可以实现对APP上的二维码进行扫码识别,识别成功之后跳到页面进行渲染,然后用户可以对其签字和编辑提交等。这个小程序前端方面主要的技术难点有根据后端返回的接口数据生成动态的表单、签字功能、获取用户微信头像昵称并检测是否重复授权、微信APP上的扫一扫扫码跳到小程序对应的结果页面上去。
小程序首页:
这里要注意的是,我们要获取微信用户的头像、昵称、手机号,而头像、昵称作为一步,第一次授权,获取授权号作为第二步,第二次授权,然后我们通过调接口来知道是否已经授过权:
//index.wxml
<!--index.wxml-->
<view class="Box" >
<!-- 扫描logo -->
<image class="logo" src="/images/Scancode.png"/>
<button type="primary" class="scanButton" bindtap="getUserProfile">扫一扫</button>
</view>
<!-- 授权获取手机号弹窗 -->
<view class="mask" wx:if="{{isShowModal}}">
<view class="content">
<text class="title">绑定手机号</text>
<text class="explain">请先绑定手机号再进行此操作</text>
<view class="close" catchtap="close">x</view>
<button type="primary" class="clickButton" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">微信用户一键绑定</button>
</view>
</view>
// index.js
var util = require('../../utils/util.js')
import api from '../../utils/api.js';
const app = getApp()
Page({
/**
* 页面的初始数据
*/
data: {
isbutton: false, //是否授权了获取了头像昵称手机号,只有全部授权了才为true,默认没授权为false
code: '',
isShowModal: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function(options) {
// console.log("onLoad")
let that = this
wx.login({
success: (res) => {
that.data.code = res.code;
},
})
//当签完字后跳回到首页的时候 这个时候会跳到携带的有hasLogin参数为true 此时肯定是已经授权了的 isbutton为true
if (options.hasLogin) {
//已经授权过
that.setData({
isbutton: true
})
}
},
onShow: function() {
let that = this;
wx.login({
success: async(res) => {
//获取openid
await util.getOpenId(res.code)
// console.log("1231",wx.getStorageSync('openid'))
//检测是否授权过
api.getSetting({
"openId": wx.getStorageSync('openid')
}).then(res => {
// console.log("检测是否授权过", res)
if(res.data.code == 0){
if(res.data.result){
//授权过
that.setData({
isbutton: true
})
}else{
//没授权过
that.setData({
isbutton: false
})
}
}
})
},
})
//显示"转发给朋友"
wx.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', ],
success: function(res) {
},
fail: function(res) {
}
})
},
onLaunch: function() {
// console.log("onLaunch")
},
getUserProfile() {
//检测判断是否已经授权了
if (!this.data.isbutton) {
//没授权
wx.getUserProfile({
desc: '用于获取头像昵称',
success: (res) => {
// console.log("获取用户信息", res)
//将用户信息存储到全局变量里
app.globalData.userInfo = res.userInfo
app.globalData.userInfo.avatarUrl = res.userInfo.avatarUrl
this.setData({
isShowModal: true
})
//将用户的头像昵称保存到数据库
// let params = {
// openId:wx.getStorageSync('openid'),
// phone:'',
// nickName:res.userInfo.nickName,
// photo:res.userInfo.avatarUrl,
// sex:res.userInfo.gender
// }
// api.SaveWechatInfo(params).then(res=>{
// // console.log("保存用户头像昵称",res)
// })
}
})
} else {
//已经授权了 直接开始扫一扫
this.scanCode()
}
},
//用户授权后获取其手机号
async getPhoneNumber(e) {
// console.log('e.detail', e.detail)
let that = this
//判断用户是选择拒绝还是允许
if (e.detail.errMsg != 'getPhoneNumber:ok') {
//拒绝
that.setData({
isbutton: false,
})
return false
} else {
//允许
that.setData({
isbutton: true,
})
await util.getPhoneNumberToLogin(e, that.data.isbutton, that.data.code);
//授权绑定成功 自动关闭弹窗
that.setData({
isShowModal: false
})
wx.showToast({
title: '绑定成功',
icon: 'success',
duration: 3000
})
that.setData({
isbutton: true,
})
}
},
//用户手动点击右上角关闭弹窗 相当于没有同意授权获取手机号
close() {
this.setData({
isShowModal: false,
isbutton: false
})
},
//授权了头像昵称手机号后开始扫描
scanCode() {
wx.scanCode({
success(res) {
console.log("扫描", res)
let result = res.result;
wx.navigateTo({
url: '/pages/temporary/temporary?result='+encodeURIComponent(result)
})
}
})
},
})
当用户扫码成功后会跳到一个临时页面temporary.wxml,这个页面作为一个中转页面,主要是为了当用户从微信的扫一扫进来时也是先进这个中转页面再到结果页面,相当于从微信的扫一扫进来时不用到小程序的扫一扫里再授权什么的。这里我在js的onLoad里做了区分, 通过decodeURIComponent( )来获取二维码原始内容,代码如下:
//temporary.wxml
<view class="temporary">
</view>
//temporary.js
// pages/temporary/temporary.js
import api from '../../utils/api.js';
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(query) {
console.log("接收", query)
wx.showLoading({
title: '加载中',
})
let string;
if (query.hasOwnProperty('result')) {
//从小程序内的扫一扫跳转过来的
string = decodeURIComponent(query.result)
console.log("sadas", decodeURIComponent(query.result))
} else if (query.hasOwnProperty('q')) {
//从微信扫一扫跳转过来的
string = decodeURIComponent(query.q) //获取到二维码原始链接内容
console.log("外面微信扫码获得的", string)
// const scancode_time = parseInt(query.scancode_time) // 获取用户扫码时间 UNIX 时间戳
}
// console.log("route", this.getQueryVariable(string, "route"))
// console.log("tkId", this.getQueryVariable(string, "tkId"))
// console.log("batchNo", this.getQueryVariable(string, "batchNo"))
// console.log("randomCode", this.getQueryVariable(string, "randomCode"))
// console.log("formId", this.getQueryVariable(string, "formId"))
try {
var val = wx.getStorageSync('openid')
let route = this.getQueryVariable(string, "route")
let params = {
route: route,
tkId: this.getQueryVariable(string, "tkId"),
batchNo: this.getQueryVariable(string, "batchNo"),
randomCode: this.getQueryVariable(string, "randomCode"),
formId: this.getQueryVariable(string, "formId") ? this.getQueryVariable(string, "formId") :
''
}
if (val) {
// 本地有openid
params.openId = val
let that = this
//检测用户之前是否授权过
api.getSetting({
"openId": val
}).then(res => {
console.log("检测是否授权过", res.data)
wx.hideLoading()
if (res.data.code == 0) {
if (res.data.result) {
//授权过
api.getCodeContent(params).then(res => {
console.log("二维码业务内容成功获取", res)
if (res.data.code == 1) {
//二维码失效
wx.showModal({
content: `${res.data.message}`,
showCancel: false,
confirmText: '我知道了',
success(res) {
if (res.confirm) {
// console.log('用户点击确定')
that.goHome()
} else if (res.cancel) {
// console.log('用户点击取消授权')
}
}
})
} else if (res.data.code == 0) {
if (route == 'activeTask') {
//激活任务
let myParams = {
...params,
}
if (myParams.hasOwnProperty('formId')) {
delete myParams.formId
}
api.saveScanResult(myParams).then(response => {
console.log("激活", response)
if(response.data.code == 0){
wx.showModal({
title: '提示',
content: `${response.data.result}`,
showCancel: false,
confirmText: '我知道了'
}).then(res => {
that.goHome()
}).catch(err => {
})
}else{
wx.showModal({
title: '提示',
content: `${response.data.message}`,
showCancel: false,
confirmText: '我知道了'
}).then(res => {
that.goHome()
}).catch(err => {
})
}
}).catch(error => {
wx.showModal({
title: '服务器内部出错',
content: '请稍后重试',
showCancel: false,
confirmText: '我知道了'
})
})
} else if (route == 'submitForm') {
//表单提交
// formType为1新表单 通用表单
if (res.data.result.formType == 1) {
wx.navigateTo({
url: '/pages/check/check_detail'
})
wx.setStorageSync('FormDetailTitle',
res.data.result.title)
//将场所登记表缓存在本地
wx.setStorageSync('placeFormJson', res.data
.result
.formJson)
wx.setStorageSync('taskItemResultId', JSON
.stringify(res.data.result
.taskItemResultId)
)
//将从业人员列表缓存到本地
wx.setStorageSync('personFormJson', JSON
.stringify(
res.data.result.personFormJson))
} else if (res.data.result.formType == 3) {
//formType为3旧表单 自定义表单
wx.navigateTo({
url: '/pages/check/check_detail_old'
})
wx.setStorageSync('oldFormDetailTitle',
res.data.result.title)
wx.setStorageSync('oldFormJson',
res.data.result.formJson)
wx.setStorageSync('taskItemResultId', JSON
.stringify(res.data.result
.taskItemResultId)
)
wx.setStorageSync('oldResultId',JSON
.stringify(res.data.result
.taskItemResultId))
}
wx.setStorageSync('params', JSON.stringify(params))
wx.setStorageSync('taskItemId', res.data.result
.taskItemId)
//通过isFill判断表单是否填写过 isFill为0是未填写
wx.setStorageSync('isFill', JSON.stringify(res.data
.result.isFill))
}
} else if (res.data.code == 401) {
wx.showModal({
title: '服务器内部出错',
showCancel: false,
content: '请稍后重试',
confirmText: '我知道了'
})
}
}).catch(err => {
console.log("二维码业务内容获取失败", err)
})
} else {
wx.hideLoading()
//没授权过就提醒其授权
that.isAuthorize()
}
}
})
} else {
//本地拿不到openId 提醒需要授权
that.isAuthorize()
}
} catch (e) {
}
},
//提醒用户授权
isAuthorize() {
let that = this;
wx.showModal({
content: '您还没有授权',
showCancel: false,
confirmText: '我知道了',
success(res) {
if (res.confirm) {
// console.log('用户点击确定')
that.goHome()
} else if (res.cancel) {
// console.log('用户点击取消授权')
}
}
})
},
//跳回首页
goHome() {
wx.hideLoading()
wx.showLoading({
title: '正在跳回首页...'
})
//跳回首页
setTimeout(() => {
wx.redirectTo({
url: '/pages/index/index',
success: function() {
},
fail: function() {
},
complete: function() {
wx.hideLoading()
}
})
}, 2000)
},
//获取url参数的方法
getQueryVariable(url, variable)
{
var query = url.substr(url.indexOf('?') + 1);
// console.log("query",query)
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (pair[0] == variable) {
return pair[1];
}
}
return (false);
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
wx.hideLoading()
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})
我们写完这些逻辑还不够,要实现微信APP上的扫一扫扫码跳到小程序上的某个页面,我们还需要在微信公众平台后台进行配置,找到后台的开发管理–开发设置–扫普通链接二维码打开小程序:
我们线上环境是8443端口,然后我们对其配置如下:
然后我们的app.json里pages页面第一个依然是pages/index/index,而我们在提交代码审核的页面修改路径一定要改为pages/temporary/temporary,这样就能保证我们用户如果是正常搜索进小程序来是首页index.wxml,如果是用微信的扫一扫进小程序就是临时页temporary.wxml
小程序签字页面主要用到了canvas,然后页面上有重新签名和完成两个按钮:
//sign.wxml
<canvas canvas-id="firstCanvas" id='firstCanvas' bindtouchstart="bindtouchstart" bindtouchmove="bindtouchmove"></canvas>
<view class="bottomBox">
<view class="clear" bindtap='clear'>重新签名</view>
<view class="save {{sureBtn?'sureBet':''}}" bindtap='export'>完成</view>
</view>
//sign.js
//index.js
//获取应用实例
const app = getApp()
Page({
data: {
context: null,
index: 0,
height: 0,
width: 0,
writeTips: '请清晰书写您的签名',
writeTipsTrue: true,
src: '',
src1: '',
sureBtn: false,
saveContext: null,
pixelRatio:2 //设备像素比默认为2
},
/**记录开始点 */
bindtouchstart: function(e) {
let {
writeTipsTrue
} = this.data
if (writeTipsTrue) {
this.data.context.clearRect(0, 0, this.data.width, this.data.height)
this.setData({
writeTipsTrue: false,
sureBtn: true
})
}
this.data.context.moveTo(e.changedTouches[0].x, e.changedTouches[0].y)
},
/**记录移动点,刷新绘制 */
bindtouchmove: function(e) {
this.data.context.setLineWidth(2) // 设置线条宽度
this.data.context.lineTo(e.changedTouches[0].x, e.changedTouches[0].y)
this.data.context.stroke()
this.data.context.draw(true)
this.data.context.moveTo(e.changedTouches[0].x, e.changedTouches[0].y)
},
/**清空画布 */
clear() {
let context = this.data.context
this.setData({
writeTipsTrue: true,
sureBtn: false
})
context.clearRect(0, 0, this.data.width, this.data.height)
// this.data.saveContext.clearRect(0, 0, this.data.height, this.data.width);
context.save()
// context.setTransform(1, 0, 0, 1, Math.ceil(this.data.width / 2), 155) // 旋转画布 默认文字区域
context.setTransform(1, 0, 0, 1, 50, 50)
let str = this.data.writeTips
context.setFontSize(24)
context.setFillStyle('#ADADB2')
context.fillText(str, 0, 0)
context.restore()
context.draw()
},
/**导出图片 */
export () {
const that = this
if (!that.data.sureBtn) {
wx.showModal({
title: '您没有签名',
content: '请签完名后再提交',
showCancel: false,
confirmText: '我知道了'
})
return false
}
let signImg
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.data.width,
height: that.data.height,
destWidth: that.data.width * that.data.pixelRatio,
destHeight: that.data.height * that.data.pixelRatio,
canvasId: 'firstCanvas',
success(res) {
signImg = res.tempFilePath
that.setData({
src1: signImg
})
//下载图片
wx.getImageInfo({
src: signImg, // 签字画布生成的暂存地址
success(res) {
let rototalImg = res.path
that.setData({
src: rototalImg
})
if (rototalImg) {
// 单独处理图片旋转
that.saveImg(rototalImg)
}
},
fail(err) {
}
})
},
})
},
// drew img
saveImg(signImg) {
// 旋转图
let that = this
let context = wx.createCanvasContext('firstCanvas');
that.setData({
saveContext: context
})
context.setTransform(1, 0, 0, 1, 50, 50)
//绘制图片 生成图片函数写在draw()的回调中,不然会出现还没有画图就生成图片的问题
context.draw(true, function() {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.data.width,
height: that.data.height,
destWidth: that.data.width * that.data.pixelRatio,
destHeight: that.data.height * that.data.pixelRatio,
canvasId: 'firstCanvas',
fileType: 'png',
success: function(res) {
var tempFilePath = res.tempFilePath
//将图片转成base64格式
wx.getFileSystemManager().readFile({
filePath:tempFilePath,
encoding: 'base64',
success: res => {
var userImageBase64 =
'data:image/jpg;base64,' + res.data;
// console.log("签字base64",
// userImageBase64);
// 生成图片 并返回上一页 赋值
var curPages = getCurrentPages()
var currPage = curPages[curPages.length - 1]; //当前页面
var prevPage = curPages[curPages
.length - 2] //上一页面
prevPage.setData({
sign: userImageBase64
})
wx.navigateBack({
delta: 1,
})
}
})
},
fail: function(res) {
// console.log(res)
},
})
})
},
onShow: function() {
// 进入页面渲染canvas
let query = wx.createSelectorQuery()
const that = this
let context
query.select('#firstCanvas').boundingClientRect()
query.exec(function(rect) {
let width = rect[0].width
let height = rect[0].height
that.setData({
width,
height,
})
const context = wx.createCanvasContext('firstCanvas')
that.setData({
context: context,
})
context.save()
// context.translate(Math.ceil(width / 2) - 20,0)
context.setTransform(1, 0, 0, 1, 50, 50)
let str = that.data.writeTips
context.setFontSize(24)
context.setFillStyle('#ADADB2')
context.fillText(str, 0, 0)
context.restore()
context.draw()
})
},
onLoad(){
//获取设备像素比
wx.getSystemInfo({
success: (res) => {
// console.log("获取设备像素比",res)
this.setData({
pixelRatio : res.pixelRatio
})
}
})
},
// 弹窗
onToast() {
app.toast(this, {
type: 'text',
text: '生成成功',
})
},
onShareAppMessage: (res) => {}
})
//sign.wxss
/* pages/sign/sign.wxss */
#firstCanvas{
width:95vw;
height:78vh;
margin:0 auto;
border:1rpx solid #ACACAC;
}
#saveImg{
width:95vw;
border:1px solid blue;
}
.bottomBox{
display: flex;
margin-top:10rpx;
justify-content: center;
}
.clear{
width: 157rpx;
text-align: center;
height: 44rpx;
line-height: 44rpx;
border-radius: 22rpx ;
opacity: 1;
border: 1rpx solid #ACACAC;
font-size: 15rpx;
font-family: PingFang SC-Bold, PingFang SC;
font-weight: bold;
color: #EC3535;
margin-right:20rpx;
}
.save{
width: 157rpx;
height: 44rpx;
line-height: 44rpx;
border-radius: 22rpx ;
text-align: center;
font-size: 15rpx;
font-family: PingFang SC-Bold, PingFang SC;
font-weight: bold;
color: #FFFFFF;
background: linear-gradient(270deg, #3670FF 0%, #65A7FF 100%);
opacity: 1;
}
.sureBet{
}
//sign.json
{
"usingComponents": {},
"navigationBarTitleText":"签名",
"pageOrientation":"landscape",
"disableScroll":true
}