文末代码可以直接复制运行(只需要将中间的二维码图片、底部的微信和相册图片和微信头像配置白名单 改成你项目内的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 ) –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>
export default {
data () {
return {
myObj: {
nickname: '喜喜',
head_image: uni.getStorageSync('avatarUrl'),
personal_signature: '个人名片二维码,携带个人的唯一标识参数id;他人识别此二维码,可以跳转至首页,并拿到此id',
user_code: "rjfhkb",
},
imgHeadNow: '',
imgUrl: '../static/iconimg/codeimg.png',
canvasWidth: '',
canvasHeight: '',
ratio: 0,
}
},
onShareAppMessage (res) {
if (res.from === 'button') {
console.log(res.target)
}
return {
title: '诚邀您使用保客多多,开启客户管理轻松之旅!',
path: `pages/tabBar/home/index?user_code=${this.myObj.user_code}`
}
},
onLoad () {
let that = this
uni.getSystemInfo({
success: res => {
that.canvasWidth = res.screenWidth + 'px'
that.ratio = 750 / res.screenWidth
that.canvasHeight = 1000 / that.ratio + 'px'
}
})
this.getCodeImg()
},
methods: {
getCodeImg () {
},
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,
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: "绘制图片失败",
})
}
}, _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 {
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%;
}
.t2 {
margin-left: 120rpx;
flex: 1;
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;
text-align: center;
.erweima_img {
margin-top: 90rpx;
display: block;
width: 340rpx;
height: 360rpx;
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;
}
</style>
|