需求概要
电商项目中需要将自己小店的商品带上自己的小程序码生成海报,保存到本地,然后分享到万能的朋友圈,QQ空间,微博等等来广而告之...
如下图,三种海报格式轮播展示,左滑右滑切换到海报,点击下面保存图片按钮,将当前海报保存到手机相册
思路
- 需要商品信息,用户信息以及小程序码。
- 使用swiper组件展示海报,
- 将海报通过wx.createCanvasContext绘制到画布canvas组件。
- 使用canvasToTempFilePath 将canvas海报保存到本地临时文件路径;
- 使用saveImageToPhotosAlbum将图片保存到本地相册
- 根据swiper组件的current属性判断当前保存的海报
解决方案
按照思路逐步实现:
商品信息,用户信息以及小程序码
1.商品信息通过导航事件传递到海报页,在此我使用的是模拟数据;
2.用户信息通过本地存储wx.setStorageSync 到缓存。
// index.js //事件处理函数 navToShare: function () { // 模拟数据 var data = { thumb_images: [ 'https://cbu01.alicdn.com/img/ibank/2018/544/692/8567296445_882293189.400x400.jpg', 'https://cbu01.alicdn.com/img/ibank/2018/971/643/8581346179_882293189.400x400.jpg', 'https://cbu01.alicdn.com/img/ibank/2018/184/392/8567293481_882293189.400x400.jpg' ], name: '2018夏季新款镂空圆领蝙蝠短袖t恤女装韩版宽松棉小衫上衣批发潮', price: 198, } wx.navigateTo({ url: '../poster/poster?data=' + encodeURIComponent(JSON.stringify(data)) }) }, 3.在海报页面onLoad函数的参数中获取商品信息
4.在海报页面获取本地缓存中的用户信息wx.getStorageSync
5.因为canvas绘制图片不支持跨域图片,所以先使用getImageInfo将网络图片返回图片的本地路径,
// poster.js onLoad: function(options) { var data = JSON.parse(decodeURIComponent(options.data)); var userinfo; // 获取本地存储的用户头像和昵称 userinfo = wx.getStorageSync('userInfo'); console.log('用户信息', userinfo) // 渲染页面 this.setData({ avatar_url: userinfo.avatarUrl, nickname: userinfo.nickName, thumb_images: data.thumb_images, pro_price: data.price, pro_name: data.name, }) // 保存网络图片到本地 用于canvas绘制图片 wx.getImageInfo({ src: userinfo.avatarUrl, success: (res) = { tmpAvatarUrl = res.path; } }); // 保存产品图到本地 用于canvas绘制图片 var thumbs = data.thumb_images; tmpThumbs = []; // 先清空,再添加新的产品图 thumbs.forEach((item, i) = { wx.getImageInfo({ src: item, success: (res) = { tmpThumbs.push(res.path) } }) }); },7.小程序码由后端生成,前端通过POST请求将data传入,返回小程序码url,使用 wx.getImageInfo保存到本地
// 封装后的POST方法 wxRequest.postRequest(url, data).then(res = { if (res.data.error_code == 0) { // 保存小程序码到本地 用于canvas绘制图片 wx.getImageInfo({ src: res.data.qrcode, success: (result) = { this.setData({ poster_qrcode: result.path }) } }); } })使用swiper组件展示海报
在这个项目中我是将页面渲染和canvas绘制分开的,因为小程序单位rpx自动适配各种设备屏幕。而canvas绘制单位是px。我没有做px和rpx之间的计算,保存px单位固定大小的图片也不错。
view class='poster_swiper' swiper bindchange="shareChange" current="{{current}}" circular="{{circular}}" previous-margin="100rpx" next-margin="100rpx" class="swiper_share" swiper-item class="swiper_item1" // 根据设计渲染页面 /swiper-item swiper-item class="swiper_item2" wx:if="{{thumb_images.length1}}" // 根据设计渲染页面 /swiper-item swiper-item class="swiper_item3" wx:if="{{thumb_images.length2}}" // 根据设计渲染页面 /swiper-item /swiper /view这里要用到swiper的几个属性列出来
| current | Number | 0 | 当前所在滑块的 index | |
| circular | Boolean | false | 是否采用衔接滑动 | |
| previous-margin | String | "0px" | 前边距,可用于露出前一项的一小部分,接受 px 和 rpx 值 | 1.9.0 |
| next-margin | String | "0px" | 后边距,可用于露出后一项的一小部分,接受 px 和 rpx 值 | 1.9.0 |
| bindchange | EventHandle | current 改变时会触发 change 事件,event.detail = {current: current, source: source} |
将海报通过wx.createCanvasContext绘制到画布canvas组件。。
1.在wxml中添加canvas组件,设置canvas-id以便于wx.createCanvasContext绘制画布
canvas class='canvas-poster' canvas-id='canvasposter'/canvas定义样式固定定位到可视区以外,不影响可视区展示。
.canvas-poster { position: fixed; width: 280px; height: 450px; top: 100%; left: 100%; overflow: hidden; }三种海报分别绘制,具体看注释
/*一张产品图*/ drawPosterOne: function() { var ctx = wx.createCanvasContext('canvasposter'); // ctx.clearRect(0, 0, 280, 450); /* 绘制背景*/ ctx.rect(0, 0, 280, 450); ctx.setFillStyle('white'); ctx.fillRect(0, 0, 280, 450); /*绘制店名*/ ctx.setFontSize(16); ctx.setFillStyle('#333'); ctx.textAlign = "center"; ctx.fillText(this.data.nickname + '的小店', 140, 70); ctx.restore(); /*绘制产品图*/ ctx.drawImage(tmpThumbs[0], 35, 90, 210, 210); /* 绘制产品名称背景*/ ctx.setFillStyle('#FF8409'); ctx.fillRect(35, 300, 210, 60); /*绘制产品名称*/ ctx.setFontSize(12); ctx.setFillStyle('#ffffff'); ctx.textAlign = "left"; ctx.fillText(this.data.pro_name.substr(0, 18), 45, 322); ctx.restore(); ctx.setFontSize(12); ctx.setFillStyle('#ffffff'); ctx.textAlign = "left"; ctx.fillText(this.data.pro_name.substr(18, 20), 45, 344); ctx.restore(); /* 绘制线框*/ ctx.setLineDash([1, 3], 1); ctx.beginPath(); ctx.moveTo(35, 375); ctx.lineTo(160, 375); ctx.moveTo(35, 435); ctx.lineTo(160, 435); ctx.setStrokeStyle('#979797'); ctx.stroke(); ctx.restore(); /*绘制文字*/ ctx.setFontSize(14); ctx.setFillStyle('#333333'); ctx.textAlign = "left"; ctx.fillText('¥', 35, 400); ctx.setFontSize(18); ctx.fillText(this.data.pro_price, 50, 400); ctx.setFontSize(11); ctx.setFillStyle('#666666'); ctx.fillText(this.data.poster_qrtext, 35, 420); ctx.restore(); /*绘制二维码*/ ctx.drawImage(this.data.poster_qrcode, 185, 370, 60, 60); ctx.restore(); /*圆形头像*/ ctx.save() ctx.beginPath(); ctx.arc(140, 30, 20, 0, 2 * Math.PI) ctx.setFillStyle('#fff') ctx.fill() ctx.clip() ctx.drawImage(tmpAvatarUrl, 120, 10, 40, 40) ctx.restore() ctx.draw(false, this.getTempFilePath); }, /*两张产品图*/ drawPosterTwo: function() { var ctx = wx.createCanvasContext('canvasposter'); /* 绘制背景*/ ctx.rect(0, 0, 280, 450); ctx.setFillStyle('white'); ctx.fillRect(0, 0, 280, 450); /*绘制店名*/ ctx.setFontSize(14); ctx.setFillStyle('#333'); ctx.textAlign = "left"; ctx.fillText(this.data.nickname + '的小店', 65, 36); ctx.restore(); /* 绘制虚线框*/ ctx.setLineDash([4, 1], 1); ctx.beginPath(); ctx.moveTo(25, 60); ctx.lineTo(255, 60); ctx.moveTo(25, 325); ctx.lineTo(255, 325); ctx.setStrokeStyle('#979797'); ctx.stroke(); ctx.restore(); /*绘制产品名称*/ ctx.setFontSize(12); ctx.setFillStyle('#333'); ctx.textAlign = "left"; ctx.fillText(this.data.pro_name.substr(0, 13), 25, 82); ctx.setFontSize(12); ctx.setFillStyle('#333'); ctx.fillText(this.data.pro_name.substr(13, 12) + '...', 25, 100); ctx.restore(); /*绘制文字*/ ctx.setFontSize(14); ctx.setFillStyle('#333333'); ctx.textAlign = "left"; ctx.fillText('¥', 190, 90); ctx.setFontSize(16); ctx.fillText(this.data.pro_price, 205, 90); ctx.restore(); ctx.setFontSize(10); ctx.setFillStyle('#666666'); ctx.textAlign = "center"; ctx.fillText(this.data.poster_qrtext, 140, 420); ctx.restore(); /*绘制产品图*/ ctx.drawImage(tmpThumbs[0], 25, 115, 110, 150); ctx.drawImage(tmpThumbs[1], 145, 115, 110, 150); ctx.restore(); /*绘制文字*/ ctx.setFontSize(12); ctx.setFillStyle('#333333'); ctx.textAlign = "left"; ctx.fillText(this.data.slogan1, 25, 290); ctx.fillText(this.data.slogan2, 25, 308); ctx.restore(); /*绘制二维码*/ ctx.drawImage(this.data.poster_qrcode, 110, 330, 70, 70); ctx.restore(); /*圆形头像*/ ctx.save() ctx.beginPath(); ctx.arc(35, 30, 20, 0, 2 * Math.PI) ctx.setFillStyle('#fff') ctx.fill() ctx.clip() ctx.drawImage(tmpAvatarUrl, 15, 10, 40, 40) ctx.restore() ctx.draw(false, this.getTempFilePath); }, /*三张产品图*/ drawPosterThree: function() { var ctx = wx.createCanvasContext('canvasposter'); /* 绘制背景*/ ctx.rect(0, 0, 280, 450); ctx.setFillStyle('white'); ctx.fillRect(0, 0, 280, 450); /*绘制店名*/ ctx.setFontSize(16); ctx.setFillStyle('#333'); ctx.textAlign = "center"; ctx.fillText(this.data.nickname + '的小店', 140, 70); ctx.restore(); /* 绘制虚线框*/ ctx.beginPath() ctx.setLineDash([4, 1], 1); ctx.beginPath(); ctx.moveTo(20, 230); ctx.lineTo(145, 230); ctx.lineTo(145, 305); ctx.lineTo(40, 305); /*左下角圆角 ctx.arcTo( , 左下角左边坐标,左上角左边坐标,半径)*/ ctx.arcTo(20, 305, 20, 230, 20); ctx.moveTo(20, 230); ctx.lineTo(20, 285); ctx.setStrokeStyle('#333333') ctx.stroke() ctx.setStrokeStyle('#979797'); ctx.stroke(); ctx.restore(); /*绘制产品名称*/ ctx.setFontSize(12); ctx.setFillStyle('#333'); ctx.textAlign = "left"; ctx.fillText(this.data.pro_name.substr(0, 9), 30, 250); ctx.setFontSize(12); ctx.setFillStyle('#333'); ctx.fillText(this.data.pro_name.substr(9, 8) + '...', 30, 268); ctx.restore(); /*绘制文字*/ ctx.setFontSize(14); ctx.setFillStyle('#333333'); ctx.textAlign = "left"; ctx.fillText('¥', 30, 290); ctx.setFontSize(16); ctx.fillText(this.data.pro_price, 45, 290); ctx.restore(); ctx.setFontSize(10); ctx.setFillStyle('#666666'); ctx.textAlign = "center"; ctx.fillText(this.data.poster_qrtext, 140, 420); ctx.restore(); /*绘制产品图*/ ctx.drawImage(tmpThumbs[0], 20, 90, 125, 125); ctx.drawImage(tmpThumbs[1], 160, 90, 100, 100); ctx.drawImage(tmpThumbs[2], 160, 205, 100, 100); ctx.restore(); ctx.restore(); /*绘制二维码*/ ctx.drawImage(this.data.poster_qrcode, 110, 330, 70, 70); ctx.restore(); /*圆形头像*/ ctx.save() ctx.beginPath(); ctx.arc(140, 30, 20, 0, 2 * Math.PI) ctx.setFillStyle('#fff') ctx.fill() ctx.clip() ctx.drawImage(tmpAvatarUrl, 120, 10, 40, 40) ctx.restore() ctx.draw(false, this.getTempFilePath); },绘制中用到的数据如下
var tmpAvatarUrl = ""; /*用于绘制头像*/var tmpThumbs = []; /*用于绘制产品图*/var drawing = false; /*避免多次点击保存按钮*/Page({ /** * 页面的初始数据 */ data: { circular: true, // swiper 是否采用衔接滑动 current: 0, // swiper 当前所在滑块的 index avatar_url: '', // 渲染头像 nickname: '', // 渲染昵称 poster_qrcode: '/images/poster_qrcode.png', // 小程序码 poster_qrtext: '长按识别,即可查看商品', pro_name: '', //产品名 pro_price: '', // 产品价格 slogan1: '我的小店上新了,', // 标语 1 slogan2: '快来一起快来一起看看吧!', // 标语 2 thumb_images: [] // 渲染图片 },使用canvasToTempFilePath 将canvas海报保存到本地临时文件路径;
//获取临时路径 getTempFilePath: function() { wx.canvasToTempFilePath({ canvasId: 'canvasposter', success: (res) = { this.saveImageToPhotosAlbum(res.tempFilePath) } }) },使用saveImageToPhotosAlbum将图片保存到本地相册
//保存至相册 saveImageToPhotosAlbum: function(imgUrl) { if (imgUrl) { wx.saveImageToPhotosAlbum({ filePath: imgUrl, success: (res) = { wx.showToast({ title: '保存成功', icon: 'success', duration: 2000 }) drawing = false }, fail: (err) = { wx.showToast({ title: '保存失败', icon: 'none', duration: 2000 }) drawing = false } }) }else{ wx.showToast({ title: '绘制中……', icon: 'loading', duration: 3000 }) } },注意canvas绘制需要时间,所以设置 drawing 防止绘制被打断
根据swiper组件的current属性判断当前保存的海报
1.首先根据 change 事件设置current
shareChange: function(e) { if (e.detail.source == 'touch') { this.setData({ current: e.detail.current }) } },2.通过点击按钮执行savePoster保存海报到手机相册
view class="common_btn" catchtap="savePoster" text保存图片/text /view判断是否获取相册授权,已获得权限直接绘制,若未获得权限需提示用户前去设置授权
/*保存海报到手机相册*/ savePoster: function(e) { var that = this; var current = this.data.current; //获取相册授权 wx.getSetting({ success(res) { if (!res.authSetting['scope.writePhotosAlbum']) { wx.authorize({ scope: 'scope.writePhotosAlbum', success() { //这里是用户同意授权后的回调 that.drawPoster(current); }, fail() { //这里是用户拒绝授权后的回调 wx.showModal({ title: '提示', content: '若不打开授权,则无法将图片保存在相册中!', showCancel: true, cancelText: '去授权', cancelColor: '#000000', confirmText: '暂不授权', confirmColor: '#3CC51F', success: function(res) { if (res) { wx.openSetting({ //调起客户端小程序设置界面,返回用户设置的操作结果。 }) } else { // console.log('用户点击取消') } } }) } }) } else { //用户已经授权过了 that.drawPoster(current); } } }) },3.根据current判断当前海报绘制对应海报
/* 绘制海报*/ drawPoster: function(current) { if(drawing){ wx.showToast({ title: '绘制中……', icon: 'loading', duration: 3000 }) }else{ drawing = true; // loading // 根据swiper当前所在滑块的 index判断绘制对应海报 switch (current) { case 0: this.drawPosterOne() break; case 1: this.drawPosterTwo() break; case 2: this.drawPosterThree() break; } } },保存到手机相册的海报如下:
备注
图片来源于网络若有侵权请通知我立即删除
以上就是全部内容。不足之处请多指教!
完整案例













