uniapp实现微信小程序端动态生成海报
泛域名ssl证书 239元1年送1个月、单域名39元1年,Sectigo(原Comodo证书)全球可信证书,强大的兼容性,高度安全性,如有问题7天内可退、可开发票
加微信VX 18718058521 备注SSL证书
【腾讯云】2核2G4M云服务器新老同享99元/年,续费同价
背景:
基于uniapp实现微信小程序中商品详情的海报生成与保存
效果图:
思路:
首先把海报上需要的内容准备好,比如用户头像,商品信息,二维码等。需要注意的是,因为二维码是动态生成的,所以需要后端传给我们,前端需要把路径和参数传给后端,后端请求微信服务接口,通过配置对应的参数就可以生成一个二维码啦,再将二维码发送给前端。
图片不能是网络图片,我们需要的是本地图片,如非本地图片,那我们需要对图片进行处理。uniapp可以通过uni.getImageInfo获取图片本地路径(uni.chooseImage(OBJECT) | uni-app官网 (dcloud.net.cn))
如果通过临时路径来保存图片,canvas一倍会比较模糊,我们可以加到2或3倍,太大安卓机容易出问题,我们可以采用设备的像素作为倍率,uni.getSystemInfo获取设备信息
创建canvas绘制上下文,在绘制canvas的时候,把两个图片都切割成圆形的图片,然后中间就出现了一条线,在之后对原价进行划线删除状态的时候设置划线的颜色,会影响圆形的边线颜色。然后才知道,canvas绘图是在JS中完成,只能通过JS代码来控制,JS提供了beginPath()和closePath()两个函数来控制canvas中模块的开始和结束,这样就可以避免属性覆盖。
补充:和后端交互生成二维码,需要传参数,要注意页面路径不能再根目录前不增加/,携带的参数最大为32个可见字符,且字符有要求限制,如果不符合规范就会生成二维码失败
在页面的onLoad生命周期参数options判断options.scene是否存在,存在就是扫描二维码进入,需要对二维码返回的参数进行处理,先根据官方文档来进行解码,decodeURIComponent会将%3D解码成=,我传参的时候是通过&符号将参数之间隔开的,所以我这里使用split('&'),将解码后的字符串通过&符号分割成数组,循环数组得到键名和键值,代码如下(扫普通链接二维码打开小程序 | 微信开放文档 (qq.com))
生成、获取二维码代码
onLoad: function(options) { if (options.scene) { //二维码进入 const scene1 = decodeURIComponent(options.scene); let a = scene1.toString().split('&'); let obj = {} for (let i = 0; i < a.length; i++) { // 获取=前 let index = a[i].indexOf("=") let name = a[i].substring(0, index);//键名 // 获取=后 let index1 = a[i].indexOf("=") let value = a[i].substring(index1 + 1, a[i].length); obj[name] = value //键值 } this.goodsId = obj.id //获取商品id this.getType = obj.type; //获取进入页面的方式 } else{//非二维码进入 } } //生成二维码向后端传递的参数 let data = { page: 'page_my/pages/chengbei-goods-details/index',//路径 scene: `id=${that.goodsId}&code=${that.userCode}&type=${that.getType}`, //参数 }
HTML代码如下
<!-- 海报 --> <canvas :style="{height: pupopHeight + 'px',width: pupopWidth + 'px'}" canvas-id="myCanvas"></canvas> <uni-popup ref="popup" type="center"> <view class="popup-wrap" :style="'width:' + width + 'px'"> <!-- <view class="popup-head"> 生成海报 <view @click="close_popup()" class="close_icon"></view> </view> --> <image :src="posterImg" mode="widthFix" style="width: 100%;"></image> <view class="popup-footer"> <view class="buttons margin_l30 marTop20" @click="saveToLocal()">保存到相册</view> <view class="buttonNo margin_l30 marTop20" @click="closeToLocal()">取消</view> <!-- <view class="tips">保存图片到相册,你就可以分享啦!</view> --> </view> </view> </uni-popup> <!-- 海报end -->
JS代码如下
data(){ return{ // 海报 pupopWidth: 590, pupopHeight: 900, pixelRatio: 3, //屏幕像数密度 inviteQR: '', //动态二维码 posterImg: '', //最后生成的海报 width: 100, //当前手机宽度 // 海报end } }, onLoad(){ let that = this uni.getSystemInfo({ success: function(res) { that.width = res.windowWidth * 0.8 //获取海报弹出层的宽度 that.pixelRatio = res.pixelRatio //获取像素比 } }) this.pupopWidth = this.pupopWidth * this.pixelRatio this.pupopHeight = this.pupopHeight * this.pixelRatio console.log("屏幕像素密度", this.pixelRatio, this.pupopWidth, this.pupopHeight) }, methods:{ // 生成海报 shareing() { this.isHideSharePopor() uni.showLoading({ title: "海报生成中...", mask: true }) //#ifdef MP-WEIXIN //这里参数是前端和后端商议好,生成二维码需要前端传那些对应的值,这里我传了当前页面的路径和邀请码及当前页面的参数 this.inviteQR = this.ewmImgUrl //这里是获取后端传来的二维码,先给写死了 this.createPoster(); //#endif }, //生成海报--微信端 createPoster() { let _this = this _this.headImg = uni.getStorageSync('avatarUrl') //去本地缓存获取头像 uni.getImageInfo({ src: _this.headImg, success(image) { const canvasId = "myCanvas" let ctx = uni.createCanvasContext(canvasId, _this) // 自定义组件中 一定要传this // 填充背景 ctx.setFillStyle('#FAFAFA') ctx.fillRect(0, 0, _this.pupopWidth, _this.pupopHeight); ctx.save() // 头像和二维码大小都需要在规定大小的基础上放大像素比的比例后面都会*this.pixelRatio let headerW = 102 * _this.pixelRatio let headerX = 40 * _this.pixelRatio let headerY = 40 * _this.pixelRatio // 控制头像为圆形 ctx.beginPath() ctx.setStrokeStyle('rgba(0,0,0,.2)') //设置线条颜色,如果不设置默认是黑色,头像四周会出现黑边框 ctx.arc(headerX + headerW / 2, headerY + headerW / 2, headerW / 2, 0, 2 * Math.PI) ctx.stroke() //画出当前路径的边框 ctx.clip()//画完之后执行clip()方法,否则不会出现圆形效果 ctx.drawImage(image.path, headerX, headerY, headerW, headerW)// 将头像画到画布上 ctx.restore() ctx.strokeStyle = '#EEEEEE'; ctx.save() ctx.closePath() //绘制小程序名字 const uniqueCode = "木之本樱"; let invateCode = `${uniqueCode}` let invateCodeX = headerX + headerW + 14 * _this.pixelRatio let invateCodeY = headerY + (40 * _this.pixelRatio) ctx.setFontSize(26 * _this.pixelRatio); ctx.setFillStyle('#333333'); ctx.fillText(invateCode, invateCodeX, invateCodeY); ctx.stroke(); //绘制广告语 let invateCode1 = "广告语" let invateCodeX1 = headerX + headerW + 14 * _this.pixelRatio let invateCodeY1 = headerY + (84 * _this.pixelRatio) ctx.setFontSize(26 * _this.pixelRatio); ctx.setFillStyle('#333333'); ctx.fillText(invateCode1, invateCodeX1, invateCodeY1); ctx.stroke(); //生成banner图 uni.getImageInfo({ src: _this.goodsThumbnailUrl, //这里的banner是展示的商品图 success(image) { let bannerW = 510 * _this.pixelRatio let bannerH = 510 * _this.pixelRatio let bannerX = 40 * _this.pixelRatio let bannerY = 40 * _this.pixelRatio + headerW + 24 * _this.pixelRatio // 控制商品图片为圆形 ctx.beginPath() ctx.setStrokeStyle('rgba(0,0,0,.2)') //设置线条颜色,如果不设置默认是黑色,头像四周会出现黑边框 ctx.arc(bannerX + bannerW / 2, bannerY + bannerW / 2, bannerW / 2, 0, 2 * Math.PI) // ctx.strokeStyle = 'red'; //设置线的颜色状态 ctx.stroke() //画完之后执行clip()方法,否则不会出现圆形效果 ctx.clip() // 将商品主图画到画布上 ctx.drawImage(image.path, bannerX, bannerY, bannerW, bannerH) ctx.restore() ctx.save() ctx.closePath() //现价 let bannerTextX = 40 * _this.pixelRatio let bannerTextY = bannerY + bannerH + 20 * _this.pixelRatio + 50 * _this.pixelRatio //这里的y轴起始值是顶上的距离还要特意加上文字的行高 let chr = `¥${_this.oldPrice}`; //这个方法是将一个字符串分割成字符串数组 ctx.setFontSize(50 * _this.pixelRatio); ctx.setFillStyle('#EA5506'); ctx.fillText(chr, bannerTextX, bannerTextY); ctx.stroke(); // 测试当前现价的宽度 let metrics = ctx.measureText(`¥${_this.oldPrice}`) // 划线价 let bannerTextX1 = 40 * _this.pixelRatio + metrics.width + 20 * _this .pixelRatio let bannerTextY1 = bannerY + bannerH + 20 * _this.pixelRatio + 50 * _this .pixelRatio //这里的y轴起始值是顶上的距离还要特意加上文字的行高 let chr1 = `¥${_this.newPrice}`; //这个方法是将一个字符串分割成字符串数组 ctx.setFontSize(26 * _this.pixelRatio); ctx.setFillStyle('#999999'); ctx.fillText(chr1, bannerTextX1, bannerTextY1); ctx.stroke(); // 测试原价的宽度 let metrics1 = ctx.measureText(`¥${_this.newPrice}`) //画字体删除线 ctx.beginPath() ctx.moveTo(bannerTextX1, bannerTextY1 - 10 * _this.pixelRatio); //移动到指定位置 X Y //设置起点状态 ctx.lineTo(bannerTextX1 + metrics1.width, bannerTextY1 - 10 * _this .pixelRatio); //设置末端状态 ctx.lineWidth = 1 * _this.pixelRatio; //设置线宽状态 ctx.strokeStyle = '#999999'; //设置线的颜色状态 // ctx.setStrokeStyle('#999999') ctx.stroke(); ctx.closePath() //二维码 uni.getImageInfo({ src: _this.inviteQR, success(res) { // 画当前页面的二维码 _this.pupopWidth const img_w = 160 * _this.pixelRatio const img_x = _this.pupopWidth - 40 * _this.pixelRatio - img_w const img_y = bannerY + bannerH + 20 * _this.pixelRatio ctx.drawImage(res.path, img_x, img_y, img_w, img_w) // 商品名字 goodsName //这里会处理多行显示文字,超出显示省略号的效果 let bannerTextX = 40 * _this.pixelRatio let bannerTextY = bannerY + bannerH + 20 * _this.pixelRatio + 150 * _this.pixelRatio //这里的y轴起始值是顶上的距离还要特意加上文字的行高 let chr = _this.goodsName.split(""); //这个方法是将一个字符串分割成字符串数组 let temp = ""; let row = []; ctx.setFontSize(30 * _this.pixelRatio) ctx.setFillStyle("#333333") for (var a = 0; a < chr.length; a++) { if (ctx.measureText(temp).width < 300 * _this.pixelRatio) { temp += chr[a]; } else { a--; //这里添加了a-- 是为了防止字符丢失,效果图中有对比 row.push(temp); temp = ""; } } row.push(temp); if (row.length > 2) { let rowCut = row.slice(0, 1); let rowPart = rowCut[0]; let test = ""; let empty = []; for (var a = 0; a < rowPart.length; a++) { if (ctx.measureText(test).width < 300 * _this.pixelRatio) { test += rowPart[a]; } else { break; } } empty.push(test); var group = empty[0] + "..." //这里只显示1行,超出的用...表示 rowCut.splice(0, 1, group); row = rowCut; } for (var b = 0; b < row.length; b++) { ctx.fillText(row[b], bannerTextX, bannerTextY + b *50 * _this.pixelRatio, 510 * _this.pixelRatio); } ctx.draw(false, () => { uni.canvasToTempFilePath({ width: _this.pupopWidth, height: _this.pupopHeight, destWidth: _this.pupopWidth, destHeight: _this.pupopHeight, canvasId: canvasId, fileType: 'png', quality: 1, success: function(res) { _this.posterImg = res .tempFilePath; //最终将canvas转换为图片 _this.$refs.popup.open(); uni.hideLoading() }, fail(error) { console.log('4', error) // appEv.arrTips("生成海报失败,请稍后重试!") setTimeout(() => { uni.hideLoading() }, 2000) } }, _this) }) }, fail(error) { console.log('获取二维码失败', error) // appEv.arrTips("生成海报失败,获取二维码失败") setTimeout(() => { uni.hideLoading() }, 2000) } }) }, fail(error) { console.log('生成商品图失败', error) // appEv.arrTips("生成海报失败,获取商品图失败") setTimeout(() => { uni.hideLoading() }, 2000) } }); }, fail(error) { console.log('生成头像失败', error) // appEv.arrTips("生成海报失败,获取头像失败") setTimeout(() => { uni.hideLoading() }, 2000) } }) }, // 取消保存 closeToLocal() { this.$refs.popup.close() }, //将图片保存到本地相册 saveToLocal() { //#ifdef MP-WEIXIN uni.saveImageToPhotosAlbum({ filePath: this.posterImg, success: () => { console.log('保存到相册成功') // appEv.arrTips("保存到相册成功") this.$refs.popup.close() }, fail: (err) => { console.log("保存到相册失败", err) } }); //#endif }, },