场景:微信小程序用户,点击小程序里商品的分享按钮时,想要不同的商品展示不用的分享内容,比如分享图片上展示商品的图片、价格等信息。分享的UI图如下:
实现方法:
1. 分享按钮:<button open-type="share">和onShareAppMessage(OBJECT)
2. 自定义内容:因为onShareAppMessage的imageUrl参数的支持的是本地文件路径、代码包文件路径或者网络图片路径 ,所以这里实现自定义的分享的方法是结合canvas画布uni.createCanvasContext(canvasId, this),将画好的内容利用uni.canvasToTempFilePath(object, component)导出生成指定大小的图片,再将返回的文件路径赋值给imageUrl,即可实现。
具体可看官网:分享 | uni-app官网 、uni.createCanvasContext(canvasId, this) | uni-app官网、uni-app官网、uni.canvasToTempFilePath(object, component) | uni-app官网
代码:先贴上画布的代码,这里画了三种自定义分享的内容,样式在代码下方。
1. 新建一个组件文件:ShareCanvas.vue
<template> <view class="ShareCanvas"> <view class="canvas"> <canvas canvas-id="shareCanvas" /> </view> </view> </template> <script> export default { name: 'ShareCanvas', methods: { // 订单分享 setOrderCanvas(info) { return new Promise(async (resolve, reject) => { console.log('订单分享-info', info); try { const ctx = uni.createCanvasContext('shareCanvas', this) // 绘制背景图 ctx.setFillStyle('#19C161') ctx.fillRect(0, 0, 211, 170) // 保证宽高比是 5:4 // 绘制文本信息 ctx.setFontSize(21); ctx.setTextAlign('left') ctx.setFillStyle('#FFFFFF') ctx.fillText('我买好啦!', 9, 32) // 浅绿色背景 this.setRadius(ctx, 10, 106, 11, 97, 27) // 加圆角 ctx.setFillStyle('#EFF9F1') ctx.fill() // ctx.fillRect(106, 11, 97, 27) // x, y, width, height ctx.setFontSize(14); ctx.setTextAlign('center') ctx.setFillStyle('#19C161') ctx.fillText('跟团号:' + info.followNum, 155, 30) this.setRadius(ctx, 3, 9, 49, 194, 106) ctx.setFillStyle('#FFFFFF') ctx.fill() // ctx.fillRect(9, 49, 194, 106) // 不设置圆角的时候这么画有背景色的矩形 // 画商品图 ctx.save(); this.setRadius(ctx, 5, 17, 59, 85, 85) ctx.clip();//画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.orderCartInfos[0].productImg) ctx.drawImage(path, 17, 59, 85, 85) } catch (error) { console.error(error); } ctx.restore(); if (info.teamLeaderUser && info.teamLeaderUser.avatar) { // 团长头像 ctx.save(); this.setRadius(ctx, 5, 110, 60, 25, 25) ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.teamLeaderUser.avatar) ctx.drawImage(path, 110, 60, 25, 25) } catch (error) { console.error(error); } ctx.restore(); } if (info.teamLeaderUser && info.teamLeaderUser.nickname) { // 团长昵称 ctx.setFontSize(12); ctx.setTextAlign('left') ctx.setFillStyle('#96999B') ctx.fillText(info.teamLeaderUser.nickname.length > 4 ? info.teamLeaderUser.nickname.slice(0, 4) + '...' : info.teamLeaderUser.nickname, 140, 76) } ctx.setFontSize(14); ctx.setTextAlign('center') ctx.setFillStyle('#FB7415') ctx.fillText(`¥${info.orderCartInfos[0].unitPrice}`, 152, 105) this.setRadius(ctx, 10, 115, 118, 75, 26) const grd = ctx.createLinearGradient(115, 118, 115, 144) grd.addColorStop(0, '#FDAC2F') grd.addColorStop(0.5, '#FDA72C') grd.addColorStop(1, '#FB5615') // 橙色按钮背景 ctx.setFillStyle(grd) // ctx.fillRect(230/2, 218/2, 149/2, 53/2) // x, y, width, height ctx.fill() ctx.setFontSize(12); ctx.setTextAlign('center') ctx.setFillStyle('#FFFFFF') ctx.fillText('去看看 >', 152, 135) ctx.draw(false, (() => { setTimeout(() => { uni.canvasToTempFilePath({ canvasId: 'shareCanvas', success: (res) => { return resolve(res.tempFilePath) }, fail: function (error) { console.log('fail----fail', error); //TODO return reject(error) } }, this) }, 500); })) } catch (error) { console.log('画图失败error', error); return reject(error) } }) }, // 商品分享 setGoodsShareCanvas(info) { console.log('商品分享--info', info); return new Promise(async (resolve, reject) => { try { const ctx = uni.createCanvasContext('shareCanvas', this) // 绘制背景图 ctx.setFillStyle('#FFFFFF') ctx.fillRect(0, 0, 211, 170) // 团长头像 ctx.save(); this.setRadius(ctx, 5, 0, 0, 30, 30) ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.avatar) ctx.drawImage(path, 0, 0, 30, 30) } catch (error) { console.error(error); } ctx.restore(); // 团长昵称 ctx.setFontSize(12); ctx.setTextAlign('left') ctx.setFillStyle('#96999B') ctx.fillText(info.nickname.length > 11 ? info.nickname.slice(0, 11) + `${info.pinkId ? '...的团' : '...'}` : info.nickname + '的团', 35, 18) // 商品1图 ctx.save(); this.setRadius(ctx, 3, 0, 35, 211, 211) ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.image) ctx.drawImage(path, 0, 35, 211, 211) } catch (error) { console.error(error); } ctx.restore(); // 绿色背景 ctx.setFillStyle('#19C161') ctx.fillRect(0, 130, 131, 40) // x, y, width, height ctx.setFontSize(16); ctx.setTextAlign('center') ctx.setFillStyle('#FFFFFF') ctx.fillText(`¥${info.pinkId ? info.pinkPrice : info.price}起`, 65, 130 + 18) ctx.setFontSize(12); ctx.setTextAlign('center') ctx.setFillStyle('#FFFFFF') ctx.fillText(`¥${info.otPrice}`, 65, 130 + 34) // 划线 ctx.beginPath() ctx.setLineWidth(1); ctx.setStrokeStyle('#FFFFFF') ctx.moveTo(40, 130 + 30) ctx.lineTo(90, 130 + 30) ctx.stroke() // 深绿色背景 ctx.setFillStyle('#19AF5C') ctx.fillRect(131, 130, 211 - 131, 40) if (info.pinkId) { // 立即跟团按钮 ctx.setFontSize(16); ctx.setTextAlign('center') ctx.setFillStyle('#FFFFFF') ctx.fillText(`立即跟团`, 131 + (211 - 131) / 2, 130 + 26) } else { // 已团数量 ctx.setFontSize(12); ctx.setTextAlign('center') ctx.setFillStyle('#FFFFFF') ctx.fillText(`已团 ${this.getSales(info.sales)} 件`, 131 + (211 - 131) / 2, 130 + 25) } ctx.draw(false, (() => { uni.canvasToTempFilePath({ canvasId: 'shareCanvas', success: (res) => { return resolve(res.tempFilePath) }, fail: function (error) { console.log('fail----fail', error); //TODO return reject(error) } }, this) })) } catch (error) { uni.hideLoading() console.log('画图失败error', error); return reject(error) } }) }, // 团分享 setGroupShareCanvas(info) { console.log('团分享-info', info); return new Promise(async (resolve, reject) => { try { const ctx = uni.createCanvasContext('shareCanvas', this) // 绘制背景图 ctx.setFillStyle('#FFFFFF') ctx.fillRect(0, 0, 211, 170) if (info.productImageList.length == 1) { // 团长头像 ctx.save(); this.setRadius(ctx, 5, 0, 0, 30, 30) ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.avatar) ctx.drawImage(path, 0, 0, 30, 30) } catch (error) { console.error(error); } ctx.restore(); // 团长昵称 ctx.setFontSize(12); ctx.setTextAlign('left') ctx.setFillStyle('#96999B') ctx.fillText((info.nickname.length > 11 ? info.nickname.slice(0, 11) + '...' : info.nickname) + '的团', 35, 18) // 商品1图 ctx.save(); this.setRadius(ctx, 3, 0, 35, 211, 211) ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.productImageList[0]) ctx.drawImage(path, 0, 35, 211, 211) } catch (error) { console.error(error); } ctx.restore(); } if (info.productImageList.length >= 2) { // 团长头像 ctx.save(); this.setRadius(ctx, 5, 0, 0, 42, 42) ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.avatar) ctx.drawImage(path, 0, 0, 42, 42) } catch (error) { console.error(error); } ctx.restore(); // 团长昵称 ctx.setFontSize(12); ctx.setTextAlign('left') ctx.setFillStyle('#96999B') ctx.fillText((info.nickname.length > 10 ? info.nickname.slice(0, 10) + '...' : info.nickname) + '的团', 47, 25) // 商品1图 ctx.save(); this.setRadius(ctx, 3, 0, 51, 69, 69) ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.productImageList[0]) ctx.drawImage(path, 0, 51, 69, 69) } catch (error) { console.error(error); } ctx.restore(); // 商品2图 ctx.save(); this.setRadius(ctx, 3, 69 + 2, 51, 69, 69) ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.productImageList[1]) ctx.drawImage(path, 69 + 2, 51, 69, 69) } catch (error) { console.error(error); } ctx.restore(); if (info.productImageList.length >= 3) { // 商品3图 ctx.save(); this.setRadius(ctx, 3, 69 * 2 + 4, 51, 69, 69) ctx.clip(); // 画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内 try { const { path } = await this.getImge(info.productImageList[2]) ctx.drawImage(path, 69 * 2 + 4, 51, 69, 69) } catch (error) { console.error(error); } ctx.restore(); } } // 绿色背景 ctx.setFillStyle('#19C161') ctx.fillRect(0, 128, 211, 42) // x, y, width, height ctx.setFontSize(16); ctx.setTextAlign('center') ctx.setFillStyle('#FFFFFF') ctx.fillText(`立即跟团`, 211 / 2, 128 + 26) ctx.draw(false, (() => { uni.canvasToTempFilePath({ canvasId: 'shareCanvas', success: (res) => { return resolve(res.tempFilePath) }, fail: function (error) { console.log('fail----fail', error); //TODO return reject(error) } }, this) })) } catch (error) { console.log('画图失败error', error); return reject(error) } }) }, /** * 设置圆角矩形 * * @param ctx 绘图上下文 * @param cornerRadius 圆角半径 * @param width 矩形宽度 * @param height 矩形高度 * @param x 矩形左上角的 x 坐标 * @param y 矩形左上角的 y 坐标 * @returns 无返回值 */ setRadius(ctx, cornerRadius, x, y, width, height) { // 开始绘制路径 ctx.beginPath(); // 绘制最左侧的圆角 ctx.arc(x + cornerRadius, y + cornerRadius, cornerRadius, Math.PI, Math.PI * 1.5); // 绘制顶部边缘 ctx.moveTo(x + cornerRadius, y); ctx.lineTo(x + width - cornerRadius, y); ctx.lineTo(x + width, y + cornerRadius); // 绘制最右侧的圆角 ctx.arc(x + width - cornerRadius, y + cornerRadius, cornerRadius, Math.PI * 1.5, Math.PI * 2); // 绘制右侧边缘 ctx.lineTo(x + width, y + height - cornerRadius); ctx.lineTo(x + width - cornerRadius, y + height); // 绘制最下侧的圆角 ctx.arc(x + width - cornerRadius, y + height - cornerRadius, cornerRadius, 0, Math.PI * 0.5); // 绘制底部边缘 ctx.lineTo(x + cornerRadius, y + height); ctx.lineTo(x, y + height - cornerRadius); // 绘制最左侧的圆角 ctx.arc(x + cornerRadius, y + height - cornerRadius, cornerRadius, Math.PI * 0.5, Math.PI); // 绘制左侧边缘 ctx.lineTo(x, y + cornerRadius); ctx.lineTo(x + cornerRadius, y); // 闭合路径 ctx.closePath(); }, // 获取图片地址 getImge(path) { // 利用promise异步转同步,否则可能显示不了~ return new Promise((resolve, reject) => { uni.getImageInfo({ src: path, success: function (res) { if (res && res.path) { resolve(res) } else { reject(false) } }, fail: function (res) { reject(res) } }) }) }, getSales(sales) { return sales >= 10000 ? sales / 10000 + 'w+' : sales }, } } </script> <style lang="scss" scoped> // 隐藏画布 .ShareCanvas { position: absolute; top: -200px; z-index: -1; opacity: 0; .canvas canvas { width: 211px; height: 170px; // +16 } } </style>
2. 在分享按钮的页面使用这个画布组件。
onShareAppMessage 方法的内容:
注意:
- onShareAppMessage 方法要和 onLoad 等生命周期函数同级
- 因为里面画布生成图片是异步的,我在上面用Promise处理了,这里需要async await接收~
async onShareAppMessage(res) { const { id, title, avatar, nickname, productDetailList } = this.detailInfo if (res.target && res.target.id) { // 分享商品 console.log('分享商品'); const item = productDetailList.find(p => p.id == res.target.id) || {} try { uni.showLoading({ title: '分享信息生成中', mask: true }) const imageUrl = await this.$refs.ShareCanvas.setGoodsShareCanvas({ ...item, avatar, nickname, pinkId: id }) // 用不同的画布画样式,就调对应的方法名,注意里面需要的参数要传对 uni.hideLoading() return { title: item.storeName || '好物多多,快来选购啦~', path: '/pages/home/index', // 这里是你的分享里面的跳转地址 imageUrl: imageUrl || '' } } catch (error) { uni.hideLoading() } } else { // 分享团 console.log('分享团', productDetailList); try { uni.showLoading({ title: '分享信息生成中', mask: true }) const productImageList = productDetailList.map(item => item.image) const imageUrl = await this.$refs.ShareCanvas.setGroupShareCanvas({ avatar, nickname, productImageList }) // 用不同的画布画样式,就调对应的方法名,注意里面需要的参数要传对 uni.hideLoading() return { title: title || '好物多多,快来选购啦~', path: '/pages/home/index', // 这里是你的分享里面的跳转地址 imageUrl: imageUrl || '' } } catch (error) { uni.hideLoading() } } },
setOrderCanvas()方法的样式
setGoodsShareCanvas()方法的样式
setGroupShareCanvas()方法的样式
画画的时候,要是找不准xy的位置,可以从这三种样式里选一个样式接近的再慢慢修改~