一、场景:在微信小程序 个人名片页面 含有微信头像和个人信息二维码(识别可跳转小程序指定页面并携带参数),要求点击 保存到相册 按钮,将此页面上半部分进行截图保存;还有分享功能,和识别二维码一样,文末代码逻辑也有;
二、需求分析:
–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是分享功能,配合实现分享;
四、以下代码可以直接复制使用运行(注意上述的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 = this
uni.getSystemInfo({
success: res => {
// console.log(res)
that.canvasWidth = res.screenWidth + 'px'
that.ratio = 750 / res.screenWidth
that.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 = bgcolor
ctx.strokeStyle = lineColor
ctx.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.ratio
let y = 110 / _this.ratio
let w = 320 / _this.ratio
for (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*像素比2
destHeight: _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.path
let 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>