Uniapp简易使用canvas绘制分享海报

使用UniApp Canvas实现分享海报

一、分享海报

现在使用 Uniapp 中的 canvas 简单实现下商品的分享海报,附上二维码(这个可以附上各种信息例如分享绑定下单等关系),开箱即用。

  • 动态生成包含商品信息、用户二维码的分佣海报
  • 一键保存到手机相册
  • 支持App原生分享和小程序分享
  • 打通社交裂变传播路径

注:这里的分享功能用了微信的 showShareImageMenu,会调起朋友分享、朋友圈分享、收藏、保存图片等,会跟页面功能重复,并且使用这个接口记得绑定项目的 appid,否则会报错。


二、技术支持(使用 Uniapp canvas,直接复制进行更改就行)

<template> 	<view class="container"> 		<!-- 商品展示区域 --> 		<view class="product-canvas"> 			<canvas canvas-id="productCanvas" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }" 				id="productCanvas" class="canvas" /> 			<loading v-if="loading"></loading> 		</view>  		<!-- 四个功能按钮 --> 		<view class="functions"> 			<view class="function-item" @tap="shareToFriend"> 				<view class="icon-circle icon-share"></view> 				<text class="function-text">发送给朋友</text> 			</view> 			<view class="function-item" @tap="shareToMoments"> 				<view class="icon-circle icon-moments"></view> 				<text class="function-text">分享到朋友圈</text> 			</view> 			<view class="function-item" @tap="collectProduct"> 				<view class="icon-circle icon-collect"></view> 				<text class="function-text">收藏</text> 			</view> 			<view class="function-item" @tap="savePoster"> 				<view class="icon-circle icon-save"></view> 				<text class="function-text">保存图片</text> 			</view> 		</view>  <!-- 		<view class="" @click="close" style="         position: absolute;         left: 50%;         bottom: 50rpx;         transform: translateX(-50%);       "> 			<image src="作为组件底部叉叉" mode="widthFix" style="width: 50rpx; height: auto"></image> 		</view> --> 	</view> </template>  <script> 	export default { 		data() { 			return { 				canvasWidth: 355, // px 				canvasHeight: 425, 				loading: false, 				canvasPath: "", 				product: { 					name: "海天调味品十件套", 					desc_text: "精选优质原料,家庭烹饪必备套装,含酱油、蚝油、陈醋、料酒等多种调味品", 					market_price: 39.9, 					pic: "https://dummyimage.com/180x230/f5f5f5/999", 				}, 				qrcode: "https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=https://shop.example.com", 			}; 		}, 		onLoad() { 			this.open() 		}, 		methods: { 			open() { 				this.drawCanvas(); 			}, 			close() { 				this.$emit("update:show", !this.show); 			}, 			shareToFriend() { 				const that = this; 				// #ifdef APP 				uni.share({ 					provider: "weixin", 					scene: "WXSceneSession", 					type: 2, 					imageUrl: that.canvasPath, 					success(res) { 						console.log("分享给朋友成功", res); 						uni.showToast({ 							title: "已分享给朋友", 							icon: "success", 						}); 					}, 					fail(err) { 						console.log("分享给朋友失败", err); 						uni.showToast({ 							title: "分享失败,请重试", 							icon: "none", 						}); 					}, 				}); 				// #endif 				// #ifdef MP-WEIXIN 				uni.showShareImageMenu({ 					path: that.canvasPath, 					success() {}, 					fail(err) { 						console.log(err) 					} 				}); 				// #endif 			}, 			shareToMoments() { 				const that = this; 				// #ifdef APP 				uni.share({ 					provider: "weixin", 					scene: "WXSceneTimeline", 					type: 2, 					imageUrl: that.canvasPath, 					success(res) { 						console.log("分享到朋友圈成功", res); 						uni.showToast({ 							title: "已分享到朋友圈", 							icon: "success", 						}); 					}, 					fail(err) { 						console.log("分享到朋友圈失败", err); 						uni.showToast({ 							title: "分享失败,请重试", 							icon: "none", 						}); 					}, 				}); 				// #endif 				// #ifdef MP-WEIXIN 				wx.showShareImageMenu({ 					path: that.canvasPath, 					success() {}, 				}); 				// #endif 			}, 			collectProduct() { 				// #ifdef MP-WEIXIN 				const that = this; 				wx.addFileToFavorites({ 					filePath: that.canvasPath, 					success: function() { 						console.log("收藏成功"); 						uni.showToast({ 							title: "收藏成功", 							icon: "success", 						}); 					}, 					fail: function(err) { 						console.error("收藏失败:", err); 						uni.showToast({ 							title: "收藏失败", 							icon: "error", 						}); 					}, 				}); 				// #endif 			}, 			savePoster() { 				const that = this; 				uni.saveImageToPhotosAlbum({ 					filePath: that.canvasPath, 					success(res) { 						uni.showToast({ 							title: "保存成功", 							icon: "success", 						}); 					}, 				}); 			}, 			async drawCanvas() { 				this.loading = true; 				const that = this; 				const dpr = uni.getSystemInfoSync().pixelRatio; 				const width = this.canvasWidth; 				const height = this.canvasHeight; 				const ctx = uni.createCanvasContext("productCanvas", this); 				// ctx.canvas.width = width * dpr; 				// ctx.canvas.height = height * dpr; 				// ctx.scale(dpr, dpr); 				const { 					pic: image, 					name: title, 					desc_text: desc, 					market_price: price, 				} = this.product; 				const qrcode = this.qrcode; 				// 背景白色 + 红色边框 				const borderMargin = 20; 				const borderWidth = 3; 				ctx.setFillStyle("#fff"); 				ctx.fillRect(0, 0, width, height); 				ctx.setStrokeStyle("#e60012"); 				ctx.setLineWidth(borderWidth); 				// 边框内缩绘制 				ctx.strokeRect( 					borderMargin + borderWidth / 2, 					borderMargin + borderWidth / 2, 					this.canvasWidth - 2 * (borderMargin + borderWidth / 2), 					this.canvasHeight - 2 * (borderMargin + borderWidth / 2) 				);  				// 徽章 				const badgeX = 190; 				const badgeY = 5; 				const badgeW = 125; 				const badgeH = 30; 				const badgeRadius = 15;  				// 阴影模拟(底层填充深色模糊) 				ctx.setFillStyle("rgba(230, 0, 18, 0.1)"); 				ctx.beginPath(); 				ctx.moveTo(badgeX + badgeRadius, badgeY + 4); 				ctx.arcTo( 					badgeX + badgeW, 					badgeY + 4, 					badgeX + badgeW, 					badgeY + badgeH + 4, 					badgeRadius 				); 				ctx.arcTo( 					badgeX + badgeW, 					badgeY + badgeH + 4, 					badgeX, 					badgeY + badgeH + 4, 					badgeRadius 				); 				ctx.arcTo(badgeX, badgeY + badgeH + 4, badgeX, badgeY + 4, badgeRadius); 				ctx.arcTo(badgeX, badgeY + 4, badgeX + badgeW, badgeY + 4, badgeRadius); 				ctx.closePath(); 				ctx.fill();  				// 绘制渐变圆角背景 				const gradient = ctx.createLinearGradient(badgeX, 0, badgeX + badgeW, 0); 				gradient.addColorStop(0, "#ff4d6d"); 				gradient.addColorStop(1, "#e60012");  				ctx.setFillStyle(gradient); 				ctx.beginPath(); 				ctx.moveTo(badgeX + badgeRadius, badgeY); 				ctx.arcTo( 					badgeX + badgeW, 					badgeY, 					badgeX + badgeW, 					badgeY + badgeH, 					badgeRadius 				); 				ctx.arcTo( 					badgeX + badgeW, 					badgeY + badgeH, 					badgeX, 					badgeY + badgeH, 					badgeRadius 				); 				ctx.arcTo(badgeX, badgeY + badgeH, badgeX, badgeY, badgeRadius); 				ctx.arcTo(badgeX, badgeY, badgeX + badgeW, badgeY, badgeRadius); 				ctx.closePath(); 				ctx.fill();  				// 白色文字 				ctx.setFontSize(14); 				ctx.setFillStyle("#fff"); 				ctx.setTextAlign("center"); 				ctx.setTextBaseline("middle"); 				ctx.fillText("分享海报", badgeX + badgeW / 2, badgeY + badgeH / 2);  				// 商品图 				await this.drawImage(ctx, image, 40, 50, 120, 150);  				// 标题 				ctx.setFontSize(18); 				ctx.setFillStyle("#333"); 				ctx.setTextAlign("left"); 				ctx.font = "bold 18px sans-serif"; 				const titleLines = this.splitText(title, 160, ctx); 				titleLines.forEach((line, index) => { 					ctx.fillText(line, 170, 60 + index * 20); 				});  				// 描述(多行) 				ctx.setFontSize(14); 				ctx.setFillStyle("#666"); 				const lines = this.splitText(desc, 160, ctx); 				lines.forEach((line, index) => { 					ctx.fillText(line, 170, 85 + titleLines.length * 20 + index * 18); 				});  				// 价格 				ctx.setFontSize(20); 				ctx.setFillStyle("#e60012"); 				ctx.fillText( 					"¥" + price.toFixed(2), 					170, 					110 + titleLines.length * 20 + lines.length * 18 				);  				// 小店名 				ctx.setFontSize(16); 				ctx.setFillStyle("#07c160"); 				ctx.fillText( 					"微信小店", 					50, 					200 + titleLines.length * 20 + lines.length * 18 				);  				// 提示 				ctx.setFontSize(12); 				ctx.setFillStyle("#999"); 				ctx.fillText( 					"微信扫一扫购买", 					45, 					230 + titleLines.length * 20 + lines.length * 18 				);  				// 二维码 				await this.drawImage( 					ctx, 					qrcode, 					200, 					160 + titleLines.length * 20 + lines.length * 18, 					90, 					90 				);  				ctx.draw(true, () => { 					setTimeout(() => { 						uni.canvasToTempFilePath({ 								destWidth: that.canvasWidth, 								destHeight: that.canvasHeight, 								canvasId: "productCanvas", 								success: (res) => { 									console.log("临时图片路径:", res.tempFilePath); 									that.canvasPath = res.tempFilePath; 								}, 							}, 							that 						); 					}, 100); 				}); 			},  			// 远程图片绘制 			drawImage(ctx, src, x, y, w, h) { 				return new Promise((resolve) => { 					uni.getImageInfo({ 						src, 						success: (res) => { 							ctx.drawImage(res.path, x, y, w, h); 							this.loading = false; 							resolve(); 						}, 						fail: () => { 							console.warn("图片加载失败", src); 							this.loading = false; 							resolve(); 						}, 					}); 				}); 			},  			// 文本换行 			splitText(text, maxWidth, ctx) { 				const result = []; 				let temp = ""; 				for (let char of text) { 					const testLine = temp + char; 					const { 						width 					} = ctx.measureText(testLine); 					if (width > maxWidth) { 						result.push(temp); 						temp = char; 					} else { 						temp = testLine; 					} 				} 				if (temp) result.push(temp); 				return result; 			}, 		}, 	}; </script> <style scoped> 	.container { 		height: 100vh; 		width: 100%; 		display: flex; 		flex-direction: column; 		justify-content: space-between; 	}  	/* 商品展示区域 - Canvas */ 	.product-canvas { 		width: 100%; 		background: linear-gradient(135deg, #fff8f8, #fff); 		position: relative; 		display: flex; 		flex-direction: column; 		justify-content: center; 		align-items: center; 		padding: 50rpx 0; 	}  	.canvas { 		width: 315px; 		height: 425px; 	}  	.canvas-content { 		width: 90%; 		height: 85%; 		background: #fff; 		border: 3px solid #e60012; 		border-radius: 12px; 		box-shadow: 0 8px 20px rgba(230, 0, 18, 0.1); 		padding: 20rpx; 		position: relative; 	}  	.badge { 		position: absolute; 		top: -16px; 		right: 20px; 		background: linear-gradient(to right, #ff4d6d, #e60012); 		color: white; 		padding: 8px 16px; 		border-radius: 20px; 		font-weight: bold; 		font-size: 14px; 		box-shadow: 0 4px 10px rgba(230, 0, 18, 0.3); 	}  	.product-info { 		display: flex; 		height: 100%; 	}  	.product-image { 		flex: 1; 		background: #f9f9f9; 		border-radius: 8px; 		display: flex; 		justify-content: center; 		align-items: center; 		overflow: hidden; 	}  	.product-image img { 		width: 100%; 		height: 100%; 		object-fit: contain; 	}  	.product-details { 		flex: 1; 		padding: 20rpx; 		display: flex; 		flex-direction: column; 		justify-content: space-between; 	}  	.product-title { 		font-size: 32rpx; 		font-weight: bold; 		color: #333; 		margin-bottom: 10px; 	}  	.product-desc { 		font-size: 14px; 		color: #666; 		line-height: 1.5; 	}  	.price { 		margin: 15px 0; 		color: #e60012; 		font-weight: bold; 		font-size: 38rpx; 	}  	.qrcode-section { 		display: flex; 		align-items: center; 		justify-content: space-between; 		margin-top: 20px; 		padding-top: 15px; 		border-top: 1px dashed #eee; 	}  	.qrcode { 		width: 100px; 		height: 100px; 		background: #f5f5f5; 		display: flex; 		justify-content: center; 		align-items: center; 		margin-bottom: 8px; 	}  	.qrcode-hint { 		color: #999; 		font-size: 12px; 	}  	.wx-store { 		color: #07c160; 		font-weight: bold; 		font-size: 15px; 		margin-top: 5px; 	}  	/* 功能区样式 */ 	.functions { 		display: flex; 		flex-direction: row; 		background-color: #fff; 		padding: 25px 10px; 		border-top: 1px solid #f0f0f0; 	}  	.function-item { 		flex: 1; 		display: flex; 		flex-direction: column; 		align-items: center; 		padding: 10px 0; 		transition: all 0.3s; 	}  	.function-item:active { 		background-color: #f9f9f9; 		transform: translateY(2px); 	}  	.icon-circle { 		width: 70rpx; 		height: 70rpx; 		border-radius: 50%; 		display: flex; 		justify-content: center; 		align-items: center; 		margin-bottom: 12rpx; 	}  	.icon-share { 		background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); 	}  	.icon-moments { 		background: linear-gradient(135deg, #3ae7b1 0%, #00d2a9 100%); 	}  	.icon-collect { 		background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%); 	}  	.icon-save { 		background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%); 	}  	.function-text { 		font-size: 26rpx; 		color: #555; 	} </style> 

三、性能优化与注意事项

1. 使用问题

  • Canvas 问题:这里的 canvas 宽高使用固定的px格式,这里没做过多的适配,需要各位自己进行适配,并且绘制的时候 canvas 的背景设置的是白色,因为要作为图片进行保存,如果对其分装为组件的时候要注意层级关系并且白色背景跟 mask 背景和组件背景要做好适配兼容。还有 canvas 顶部的徽章在真机可能没有那么好看,自己再进行优化吧。
  • 模糊问题:使用pixelRatio适配高分屏,上面没做,注释了
  • 文字溢出:这里的文字有做分割,如果过长可能还需进行优化

2. 性能优化建议

  1. 预加载网络图片
  2. 对绘制操作进行节流控制
  3. 使用离屏Canvas处理复杂图形

四、效果展示

Uniapp简易使用canvas绘制分享海报
Uniapp简易使用canvas绘制分享海报


五、扩展思路

  1. 动态模板:配置不同风格的海报模板
  2. 海报审核:对接内容安全API
  3. 数据分析:跟踪海报分享转化率
  4. 裂变激励:如有是有自己的一些模式的话,可以分享后给予佣金奖励
发表评论

评论已关闭。

相关文章