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

使用UniApp Canvas实现分享海报

一、分享海报

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

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

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


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

vue 复制代码
<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处理复杂图形

四、效果展示



五、扩展思路

  1. 动态模板:配置不同风格的海报模板
  2. 海报审核:对接内容安全API
  3. 数据分析:跟踪海报分享转化率
  4. 裂变激励:如有是有自己的一些模式的话,可以分享后给予佣金奖励
相关推荐
永日456701 小时前
学习日记-HTML-day51-9.9
前端·学习·html
狗头大军之江苏分军2 小时前
iPhone 17 vs iPhone 17 Pro:到底差在哪?买前别被忽悠了
前端
小林coding2 小时前
再也不怕面试了!程序员 AI 面试练习神器终于上线了
前端·后端·面试
文心快码BaiduComate2 小时前
WAVE SUMMIT深度学习开发者大会2025举行 文心大模型X1.1发布
前端·后端·程序员
babytiger2 小时前
python 通过selenium调用chrome浏览器
前端·chrome
passer9812 小时前
基于webpack的场景解决
前端·webpack
奶昔不会射手2 小时前
css3之grid布局
前端·css·css3
举个栗子dhy2 小时前
解决在父元素上同时使用 onMouseEnter和 onMouseLeave时导致下拉菜单无法正常展开或者提前收起问题
前端·javascript·react.js
Coding_Doggy2 小时前
苍穹外卖前端Day1 | vue基础、Axios、路由vue-router、状态管理vuex、TypeScript
前端