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. 裂变激励:如有是有自己的一些模式的话,可以分享后给予佣金奖励
相关推荐
小小小小宇1 小时前
虚拟列表兼容老DOM操作
前端
悦悦子a啊1 小时前
Python之--基本知识
开发语言·前端·python
安全系统学习2 小时前
系统安全之大模型案例分析
前端·安全·web安全·网络安全·xss
涛哥码咖2 小时前
chrome安装AXURE插件后无效
前端·chrome·axure
OEC小胖胖3 小时前
告别 undefined is not a function:TypeScript 前端开发优势与实践指南
前端·javascript·typescript·web
行云&流水3 小时前
Vue3 Lifecycle Hooks
前端·javascript·vue.js
Sally璐璐3 小时前
零基础学HTML和CSS:网页设计入门
前端·css
老虎06273 小时前
JavaWeb(苍穹外卖)--学习笔记04(前端:HTML,CSS,JavaScript)
前端·javascript·css·笔记·学习·html
灿灿121383 小时前
CSS 文字浮雕效果:巧用 text-shadow 实现 3D 立体文字
前端·css
烛阴4 小时前
Babel 完全上手指南:从零开始解锁现代 JavaScript 开发的超能力!
前端·javascript