文末代码可以直接复制运行(只需要将中间的二维码图片、底部的微信和相册图片和微信头像配置白名单 改成你项目内的img图片即可成功运行)
一、场景:在微信小程序 个人名片页面 含有微信头像和个人信息二维码(识别可跳转小程序指定页面并携带参数),要求点击 保存到相册 按钮,将此页面上半部分进行截图保存;还有分享功能,和识别二维码一样,文末代码逻辑也有;
个人名片页面:
保存到相册页面:
二、需求分析:
–2.1:二维码:生成动态二维码图片及携带参数跳转指定小程序页面(点此查看),这些功能和二维码图片都是后端实现的。前端只调用接口拿图即可;
–2.2:实现小程序保存图片到系统相册(点此查看),需要有相应权限和图片域名白名单配置;
–2.3:前端如何将对应页面部分保存成图片?使用canvas画布生成图片(最稳定但也是麻烦的方式)!只需要将UI设计稿的内容,按照对应的比例画到画布上,最后使用画布生成图片;
.
–总体思路就是:通过后端接口拿到二维码图片–正常写一个 个人名片页面(同时需要用canvas绘制出一个 个人名片页面,且这个canvas画布绘制的页面图片需要隐藏掉,而不是清除掉)–最后点击 保存到相册 按钮时候调用微信或者uni-app的保存图片方法即可;
.
–注意上述的两个点击查看链接一定要看下,避免很多坑!
三、针对可能遇到的问题和文末代码的部分解释:
–3.1代码内图片替换: 页面最底部 分享到微信~@/static/icon-weixin.png
和保存到相册~@/static/icon-pics.png
两张图片需要替换成你自己的static静态图片;
二维码图片imgUrl: '../static/iconimg/codeimg.png',
也要替换成后端接口给你的真实二维码图片;
–3.2头像替换:uni.getStorageSync('avatarUrl')
是你自己存的微信头像,需要配置download域名白名单,否则报错getImageInfo:fail download image fail. reason: downloadFile:fail createDownloadTask:fail url not in domain list
;导致的下载失败;
–3.3canvas画布,需要存在,但是要隐藏在页面中;画布只能绘制本地图片和临时路径图片,不能绘制网络图片(所以我们需要用uni.getImageInfo()
获取微信头像的网络图片,然后把这个绘制到画布上);
–3.4画布的内容绘制方法,都是有大小和颜色和定位位置的:使用画布绘制同样的UI 页面时候,需要计算比例:计算UI设计稿和你手机的屏幕宽度比例(例如UI设计稿是750宽度 你手机是350宽度 比例就是2;那么你画布画图时候 所有的尺寸大小、宽高、位置、定位左右上下都需要除以 / 比例2;此时假如UI设计稿上的二维码图片宽高是340,图片距离UI设计稿顶部是400,距离最左边是60,那么你的画布上设置都是直接 340/2 , 400/2 , 60/2 )
–另附上画布canvas使用方法
–3.5如果你想直接看到画布绘制图片结果:可以打开代码524行的三行注释 点击一下 保存到相册 按钮就会看到绘制的图片 可能小程序模拟器上有误差 真机基本没误差
–3.6onShareAppMessage是分享功能,配合<button class="share_btn" open-type="share"></button>
实现分享;
四、以下代码可以直接复制使用运行(注意上述的3.1和3.2替换图片以及配置微信头像的白名单)
无论真机还是模拟器都可以正常保存页面图片到相册
–文中的2.1和2.2最好看一下
<template><view class="percard"><view class="top_card_box"><view class="top_info"><img class="t1" :src="myObj.head_image" alt=""><view class="t2"><view class="t3"><uni-icons class="icons_btn" type="arrowright" size="22" color="#ccc" /><view>{{myObj.nickname}}</view><view>{{myObj.personal_signature?myObj.personal_signature:'未设置个性签名'}}</view></view></view></view><view class="bot_info"><img class="erweima_img" :src="imgUrl" alt=""><view class="t5">用微信扫描二维码</view><view class="t6">加入保客多多,加入我的团队</view></view></view><canvas canvas-id="myCanvas" :style="{ width: canvasWidth, height: canvasHeight }" v-if="true"></canvas><view class="bot_card_box"><view class="fl"><img @click="aa" src="~@/static/icon-weixin.png" alt=""><view @click="aa">分享到微信</view><button class="share_btn" open-type="share"></button></view><view class="fl"><img @click="myimg" src="~@/static/icon-pics.png" alt=""><view @click="myimg">保存到相册</view></view></view></view>
</template><script>// var base64src = require('./base64.js')
// 导入外部JS库
export default {data () {return {myObj: {nickname: '喜喜', //微信昵称head_image: uni.getStorageSync('avatarUrl'),// 获取缓存内的微信头像--并且需要在你自己的小程序后台配置download域名白名单--否则会获取失败personal_signature: '个人名片二维码,携带个人的唯一标识参数id;他人识别此二维码,可以跳转至首页,并拿到此id', //个性签名user_code: "rjfhkb", //用户码---自定义二维码传递的动态参数},imgHeadNow: '',//微信头像网络图片下载本地的临时图片--画布只能绘制本地图片不能是网络图片imgUrl: '../static/iconimg/codeimg.png',//二维码图片(在这里我是直接引用了本地二维码图片 正常逻辑是后端返回二维码图片 getCodeImg方法就是)canvasWidth: '',//画布宽度canvasHeight: '',//画布高度ratio: 0,//计算UI设计稿和你手机的屏幕宽度比例(例如UI设计稿是750宽度 你手机是350宽度 比例就是2 那么你画布画图时候 所有的尺寸大小、宽高、位置、定位左右上下都需要除以 / 比例2 )}},// 分享函数onShareAppMessage (res) {if (res.from === 'button') {// 来自页面内分享按钮console.log(res.target)}return {title: '诚邀您使用保客多多,开启客户管理轻松之旅!',path: `pages/tabBar/home/index?user_code=${this.myObj.user_code}`// 分享跳转页面和二维码跳转携带参数页面是一样传参和逻辑(也都会打开跳转页的onLoad函数 接收参数)}},onLoad () {let that = thisuni.getSystemInfo({success: res => {// console.log(res)that.canvasWidth = res.screenWidth + 'px'that.ratio = 750 / res.screenWidththat.canvasHeight = 1000 / that.ratio + 'px'}})this.getCodeImg()},methods: {// 通过后端获取二维码图片getCodeImg () {// let user_code = this.myObj.user_code// let home_url = `/pages/tabBar/home/index?user_code=${user_code}`// let redirect_url = encodeURI(home_url)// getMinQrcode({ redirect_url: redirect_url }).then(res => {// uni.showLoading({// title: '加载中...',// mask: true// })// // 解码后端返回的base64二维码图片// var shareQrImg = `data:image/jpg;base64,` + res.data.base64// base64src(shareQrImg, resCurrent => {// this.imgUrl = resCurrent// uni.hideLoading()// })// })},// 绘制圆角矩形/**** @param {*} x 起始x坐标* @param {*} y 起始y坐标* @param {*} width 矩形宽度* @param {*} height 矩形高度* @param {*} r 矩形圆角* @param {*} bgcolor 矩形填充颜色* @param {*} lineColor 矩形边框颜色*/rectangle (ctx, x, y, width, height, r, bgcolor, lineColor) {ctx.beginPath()ctx.moveTo(x + r, y)ctx.lineTo(x + width - r, y)ctx.arc(x + width - r, y + r, r, Math.PI * 1.5, Math.PI * 2)ctx.lineTo(x + width, y + height - r)ctx.arc(x + width - r, y + height - r, r, 0, Math.PI * 0.5)ctx.lineTo(x + r, y + height)ctx.arc(x + r, y + height - r, r, Math.PI * 0.5, Math.PI)ctx.lineTo(x, y + r)ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5)ctx.fillStyle = bgcolorctx.strokeStyle = lineColorctx.fill()ctx.stroke()ctx.closePath()},// 使用画布绘制页面drawPageImg () {let _this = this// 生成画布const ctx = uni.createCanvasContext('myCanvas')// 获取微信头像的临时地址let headImg = this.imgHeadNow || '../static/kdd.jpg'// 绘制矩形this.rectangle(ctx, (55 / _this.ratio), (50 / _this.ratio), (640 / _this.ratio), (860 / _this.ratio), (8 / _this.ratio), '#fff', "#e4e4e4")// 绘制直线ctx.beginPath() //开始绘制ctx.moveTo((55 / _this.ratio), (264 / _this.ratio)) //起点ctx.lineTo((695 / _this.ratio), (264 / _this.ratio)) //终点ctx.lineWidth = 1 // 设置线的宽度,单位是像素ctx.strokeStyle = '#e4e4e4' //设置线的颜色ctx.stroke() //进行绘制// 绘制头像ctx.save() // 先保存状态 已便于画完圆再用ctx.beginPath() //开始绘制//先画个圆ctx.arc((130 / _this.ratio), (157 / _this.ratio), (45 / _this.ratio), 0, Math.PI * 2, false)ctx.clip()//画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内ctx.drawImage(headImg, (85 / _this.ratio), (112 / _this.ratio), (90 / _this.ratio), (90 / _this.ratio), (85 / _this.ratio), (112 / _this.ratio))//描绘图片 // 第一个参数是图片 第二、三是图片在画布位置 第四、五是将图片绘制成多大宽高(不写四五就是原图宽高)ctx.restore() //恢复之前保存的绘图上下文 恢复之前保存的绘图上下午即状态 可以继续绘制// 绘制微信名称ctx.font = (32 / _this.ratio) + "px"ctx.fillStyle = '#212121'ctx.fillText(_this.myObj.nickname, (205 / _this.ratio), (110 / _this.ratio))//描绘文本// 绘制个性签名以及个性签名自动换行var temp = ""var row = []let gxqm = ''if (this.myObj.personal_signature) {gxqm = this.myObj.personal_signature} else {gxqm = '未设置个性签名'}let gexingqianming = gxqm.split("")let x = 205 / _this.ratiolet y = 110 / _this.ratiolet w = 320 / _this.ratiofor (var a = 0; a < gexingqianming.length; a++) {if (ctx.measureText(temp).width < w) {;} else {row.push(temp)temp = ""}temp += gexingqianming[a]}row.push(temp)ctx.font = (24 / _this.ratio) + "px"ctx.fillStyle = "#9E9E9E"for (var b = 0; b < row.length; b++) {ctx.fillText(row[b], x, y + (b + 1) * 20)}// 把二维码图片绘制到画布中ctx.drawImage(_this.imgUrl, (205 / _this.ratio), (375 / _this.ratio), (340 / _this.ratio), (360 / _this.ratio), (190 / _this.ratio), (375 / _this.ratio))//描绘图片// 绘制文字ctx.font = (26 / _this.ratio) + "px PingFangSC-Light"ctx.fillStyle = "#212121"ctx.textAlign = 'center'ctx.fillText('用微信扫描二维码', (375 / _this.ratio), (750 / _this.ratio))//描绘文本// 绘制文字ctx.font = (28 / _this.ratio) + "px PingFangSC-Regular"ctx.fillStyle = "#212121"ctx.textAlign = 'center'ctx.fillText('加入保客多多,加入我的团队', (375 / _this.ratio), (788 / _this.ratio))//描绘文本// 渲染画布ctx.draw(false, (() => {setTimeout(() => {uni.canvasToTempFilePath({canvasId: 'myCanvas',destWidth: _this.cropW * 2, //展示图片尺寸=画布尺寸1*像素比2destHeight: _this.cropH * 2,quality: 1,fileType: 'jpg',success: (res1) => {uni.hideLoading()console.log('通过画布绘制出的图片--保存的就是这个图', res1.tempFilePath)// 真正的保存图片画布绘制的图片到相册uni.saveImageToPhotosAlbum({filePath: res1.tempFilePath,success: function () {uni.showToast({icon: 'none',position: 'bottom',title: "已保存到系统相册",})},fail: function (error) {uni.showModal({title: '提示',content: '若点击不授权,将无法使用保存图片功能',cancelText: '不授权',cancelColor: '#999',confirmText: '授权',confirmColor: '#f94218',success (res4) {console.log(res4)if (res4.confirm) {// 选择弹框内授权uni.openSetting({success (res4) {console.log(res4.authSetting)}})} else if (res4.cancel) {// 选择弹框内 不授权console.log('用户点击不授权')}}})}})},fail: function (error) {uni.hideLoading()console.log(error)uni.showToast({icon: 'none',position: 'bottom',title: "绘制图片失败", // res.tempFilePath})}}, _this)}, 500)})())},myimg () {// 头像网络图片下载本地的临时图片--画布只能绘制本地图片不能是网络图片uni.getImageInfo({src: this.myObj.head_image,success: (res) => {console.log('微信头像的临时路径', res.path)this.imgHeadNow = res.pathlet that = this// 获取用户是否开启 授权保存图片。uni.getSetting({success (res) {console.log(res)// 如果没有授权if (!res.authSetting['scope.writePhotosAlbum']) {// 则拉起授权窗口uni.authorize({scope: 'scope.writePhotosAlbum',success () {uni.showLoading({title: '加载中...',mask: true})//点击允许后--就一直会进入成功授权的回调 就可以使用获取的方法了that.drawPageImg()},fail (error) {//点击了拒绝授权后--就一直会进入失败回调函数--此时就可以在这里重新拉起授权窗口console.log('点击了拒绝授权', error)uni.showModal({title: '提示',content: '若点击不授权,将无法使用保存图片功能',cancelText: '不授权',cancelColor: '#999',confirmText: '授权',confirmColor: '#f94218',success (res) {console.log(res)if (res.confirm) {// 选择弹框内授权uni.openSetting({success (res) {console.log(res.authSetting)}})} else if (res.cancel) {// 选择弹框内 不授权console.log('用户点击不授权')}}})}})} else {uni.showLoading({title: '加载中...',mask: true})// 有权限则直接获取that.drawPageImg()}},fail: (error) => {console.log('获取用户是否开启保存图片 接口失败', error)uni.hideLoading()uni.showToast({title: error.errMsg,icon: 'none',})}})},fail: (error) => {console.log('临时图片获取失败', error)uni.hideLoading()uni.showToast({title: error.errMsg,icon: 'none',})}})}}
}
</script><style lang="less" scope>
.percard {overflow-x: hidden; //解决因为画布浮动超出导致的滚动条--需要隐藏掉height: calc(100vh - 90rpx);padding: 50rpx 55rpx;background-color: rgba(245, 247, 250, 1);// 加这两行代码是为了固定定位 解决此页面上下可滑动回弹问题position: fixed;width: calc(100vw - 110rpx);.top_card_box {border: 1px solid #e4e4e4;border-radius: 8rpx;background-color: #fff;.top_info {// height: 214rpx;display: flex;padding: 30rpx 72rpx 30rpx 30rpx;box-sizing: border-box;position: relative;.t1 {display: inline-block;width: 90rpx;height: 90rpx;position: absolute;top: 50%;transform: translate(0, -50%);border-radius: 50%;// margin-top: 37rpx;}.t2 {margin-left: 120rpx;flex: 1;// background-color: #1fff;// padding-right: 50rpx;display: inline-block;.t3 {flex: 1;view {font-family: PingFangSC-Medium;font-size: 32rpx;color: #212121;letter-spacing: 0;}view:last-child {margin-top: 20rpx;font-family: PingFangSC-Regular;font-size: 22rpx;color: #9e9e9e;}}.icons_btn {width: 40rpx;position: absolute;top: 50%;right: 30rpx;transform: translate(0, -50%);border-radius: 50%;}}}.bot_info {border-top: 1px solid #e4e4e4;height: 644rpx;// background-color: #1fff;text-align: center;.erweima_img {margin-top: 90rpx;display: block;width: 340rpx;height: 360rpx;// margin-left: 165rpx;margin-left: 150rpx;}.t5 {font-family: PingFangSC-Light;font-size: 26rpx;color: #212121;text-align: center;margin-top: 30rpx;}.t6 {font-family: PingFangSC-Regular;font-size: 28rpx;color: #212121;letter-spacing: 0;text-align: center;margin-top: 16rpx;}}}.bot_card_box {position: fixed;bottom: 31rpx;overflow: hidden;.fl {float: left;text-align: center;width: 335rpx;.share_btn {width: 120rpx;height: 165rpx;position: absolute;top: 0;left: 111rpx;background-color: rgba(255, 255, 255, 0);}button {border: none;}button::after {border: none;}img {margin-left: 111rpx;display: block;width: 112rpx;height: 112rpx;}view {margin-top: 22rpx;font-family: PingFangSC-Regular;font-size: 26rpx;color: #212121;letter-spacing: 0;text-align: center;}}}
}
</style>
<style>
canvas {float: left;margin-left: 1155rpx;margin-top: -911rpx;/* 打开以下注释 点击一下 保存到相册 按钮就会看到绘制的图片 可能小程序模拟器上有误差 真机基本没误差 *//* background-color: rgb(117, 250, 250);margin-left: -55rpx;margin-top: 0rpx; */
}
</style>