uniapp实现canvas保存海报功能

153 阅读1分钟

Snipaste_2022-11-21_15-11-35.png 父组件引入邀请海报组件:

<QrcodePoster ref="poster" :title="title" :subTitle="subTitle" :headerImg="newIamge" :price="pinkT.totalPrice" :productPrice="productPrice" :quota="quota" :abImg="pinkT.avatar" :leaderNickname="newHeaderName" :qrcode="posterUrl" ></QrcodePoster>

父组件打开海报方法:

openPoster() {
      this.$nextTick(() => {
        this.$refs.poster.showCanvas(
          "https:xxxx.png"
        );
      });
    },

title: "", // 海报标题

subTitle: "", // 海报副标题

headerImg "", // 商品图片

price "", // 商品现价

productPrice "", // 商品原价

quota: "", // 海报仅限x个名额

abImg: "", // 团长头像

leaderNickname: "", // 团长名字

qrcode: "", // 后端返回的二维码图片

海报组件代码

<template>
	<view class="content" v-if="isShow" @click.stop="isShow=false">
		<canvas @click.stop="" :style="{ width: canvasW + 'px', height: canvasH + 'px' }"
			canvas-id="my-canvas"></canvas>
		<view class="save-btn" @click.stop="saveImage"></view>
	</view>
</template>

<script>
	export default {
		props: {
			headerImg: {
				type: String,
				default: ''
			},
			title: {
				type: String,
				default: '商城用户'
			},
			subTitle: {
				type: String,
				default: ''
			},
			price: {
				type: String,
				default: ''
			},
			abImg: {
				type: String,
				default: ''
			},
			productPrice: {
				type: String,
				default: ''
			},
			quota: {
				type: String,
				default: ''
			},
			leaderNickname: {
				type: String,
				default: ''
			},
			qrcode: {
				type: String,
				default: ''
			}
		},
		data() {
			return {
				canvasW: 0,
				canvasH: 0,
				ctx: null,
				isShow: false,
				priceTxt: '元/人',
				posterBgc: 'xxxxx.png', // 设置背景图片
				leaderFlag: 'xxxx.png', // 团长标识logo
			}
		},
		methods: {
			//显示
			showCanvas() {
				this.isShow = true
				this.__init()
			},

			//初始化画布
			async __init() {

				uni.showLoading({
					title: '加载中...',
					mask: true
				})

				// 创建画布
				this.ctx = uni.createCanvasContext('my-canvas', this)
				
				this.canvasW = uni.upx2px(571);
				this.canvasH = uni.upx2px(1111);
				
				//设置画布背景颜色透明
				this.ctx.setFillStyle('#fff')

				//设置画布大小
				this.ctx.fillRect(0, 0, this.canvasW, this.canvasH)

				// 设置背景图片
				let posterImg = await this.getImageInfo(this.posterBgc)
				this.ctx.drawImage(posterImg.path, 0, 0, this.canvasW, this.canvasH)


				// 设置商品图片
				let headerImg = await this.getImageInfo(this.headerImg)
				let hW = uni.upx2px(473);
				let hH = uni.upx2px(473);
				// this.drawRoundImg(ctx, img, x, y, width, height, radius)
				this.drawRoundImg(this.ctx, headerImg.path, ((this.canvasW - hW) / 2), ((this
					.canvasW - hW) / 2 + (uni.upx2px(60))), hW, hH, (uni.upx2px(16)))

				//绘制商品标题
				this.ctx.beginPath()
				this.ctx.setFontSize(12);
				this.ctx.setFillStyle('#000');
				this.ctx.font = 'normal bold 12px sans-serif';
				// toFormateStr(ctx, str, draw_width, lineNum, startX, startY, steps)
				this.toFormateStr(this.ctx, this.title, 170, 2, ((this.canvasW - hW) / 2), ((this.canvasW - hW) / 2 + (
					uni.upx2px(564))), 18);
				this.ctx.fill()
				this.ctx.closePath()

				//绘制副标题
				this.ctx.beginPath()
				this.ctx.setFontSize(12);
				this.ctx.setFillStyle('#333');
				this.ctx.fillText(this.subTitle, ((this.canvasW - hW) / 2), ((this.canvasW - hW) / 2 + (
					uni.upx2px(650))))
				this.ctx.fill()
				this.ctx.closePath()


				//绘制价格
				this.ctx.beginPath()
				this.ctx.setFontSize(29);
				this.ctx.setFillStyle('#E01E1E');
				this.ctx.fillText(this.price, ((this.canvasW - hW) / 2), ((this.canvasW - hW) / 2 + (
					uni.upx2px(715))))
				this.ctx.closePath()


				// 获取价格长度 
				let pLength = this.computedWidth(this.price)


				// 元/人
				this.ctx.beginPath()
				this.ctx.setFontSize(12);
				this.ctx.setFillStyle('#333');
				// this.ctx.fillText(this.priceTxt, (((this.canvasW - hW) / 2) + pLength), (
				// 	((this.canvasW - hW) / 2) + hH + 124))
				this.ctx.fillText(this.priceTxt, (((this.canvasW - hW) / 2) + pLength), ((this.canvasW - hW) / 2 + (
					uni.upx2px(715))))
				this.ctx.closePath()

				// 原价
				this.ctx.beginPath()
				let textWidth = this.ctx.measureText(this.productPrice).width - 9
				this.ctx.setFontSize(10);
				this.ctx.setFillStyle('#999');
				this.ctx.fillText(this.productPrice, (((this.canvasW - hW) / 2) + pLength + (
					uni.upx2px(80))), ((this.canvasW - hW) / 2 + (
					uni.upx2px(715))))
				this.ctx.rect((((this.canvasW - hW) / 2) + pLength + (
					uni.upx2px(82))), ((this.canvasW - hW) / 2 + (
					uni.upx2px(707))), textWidth, 1)
				this.ctx.fill()
				this.ctx.closePath()

				// 仅限x个名额
				this.ctx.beginPath()
				this.ctx.setFontSize(12);
				this.ctx.setFillStyle('#fff');
				// this.ctx.fillText(this.quota, (((this.canvasW - hW) / 2) + 170), (
				// 	((this.canvasW - hW) / 2) + hH + 88))
				this.ctx.fillText(this.quota, ((this.canvasW - hW) / 2 + (
					uni.upx2px(330))), ((this.canvasW - hW) / 2 + (
					uni.upx2px(648))))
				this.ctx.closePath()

				//团长图片
				let avaImg = await this.getImageInfo(this.abImg)
				// avaRound(ctx, img, x, y, radius) 
				this.avaRound(this.ctx, avaImg.path, (((this.canvasW - hW) / 2 + (
					uni.upx2px(38)))), ((this.canvasW - hW) / 2 + (
					uni.upx2px(820))), (
					uni.upx2px(56)));

				// 团长标识
				this.ctx.beginPath()
				let leaderFlagImg = await this.getImageInfo(this.leaderFlag)
				this.ctx.drawImage(leaderFlagImg.path, ((this.canvasW - hW) / 2), ((this.canvasW - hW) / 2 + (
					uni.upx2px(860))), (
					uni.upx2px(76)), (
					uni.upx2px(32)))
				this.ctx.closePath()

				// 团长名字
				this.ctx.beginPath()
				// this.ctx.font = 'bold 12px PingFangSC'
				this.ctx.setFontSize(12); //设置标题字体大小
				this.ctx.setFillStyle('#fff'); //设置标题文本颜色
				this.ctx.fillText(this.leaderNickname, (((this.canvasW - hW) / 2 + (
					uni.upx2px(222)))), ((this.canvasW - hW) / 2 + (
					uni.upx2px(808))))
				this.ctx.fill()
				this.ctx.closePath()

				//小程序码
				this.ctx.beginPath()
				let qrcodeImg = await this.getImageInfo(this.qrcode)
				this.ctx.drawImage(qrcodeImg.path, (((this.canvasW - hW) / 2 + (
					uni.upx2px(8)))), ((this.canvasW - hW) / 2 + (
					uni.upx2px(910))), 50, 50)
				this.ctx.closePath()

				//延迟渲染
				setTimeout(() => {
					this.ctx.draw(true, () => {
						uni.hideLoading()
					})
				}, 1000)

			},
			toFormateStr(ctx, str, draw_width, lineNum, startX, startY, steps) {

				var strWidth = ctx.measureText(str).width; // 测量文本源尺寸信息(宽度)
				var startpoint = startY,
					keyStr = '',
					sreLN = strWidth / draw_width;
				var liner = Math.ceil(sreLN); // 计算文本源一共能生成多少行
				let strlen = parseInt(str.length / sreLN); // 等比缩放测量一行文本显示多少个字符

				// 若文本不足一行,则直接绘制,反之大于传入的最多行数(lineNum)以省略号(...)代替
				if (strWidth < draw_width) {
					ctx.fillText(str, startX, startpoint);
				} else {
					for (var i = 1; i < liner + 1; i++) {
						let startPoint = strlen * (i - 1);
						if (i < lineNum || lineNum == -1) {
							keyStr = str.substr(startPoint, strlen);
							ctx.fillText(keyStr, startX, startpoint);
						} else {
							keyStr = str.substr(startPoint, strlen - 5) + '...';
							ctx.fillText(keyStr, startX, startpoint);
							break;
						}
						startpoint = startpoint + steps;
					}

				}
			},

			// 获取价格长度 更好控制priceTxt的位置 
			computedWidth(val) {
				let count = 0
				let arr = String(val).split('')
				arr.map(item => {
					// 如果价格为数字类型
					if (Number(item) || item == '0') {
						count += 19
					} else {
						// 如果价格有小数点
						count += 5
					}
				})
				return count
			},



			//带圆角图片
			drawRoundImg(ctx, img, x, y, width, height, radius) {
				ctx.beginPath()
				ctx.save()
				// 左上角
				ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5)
				// 右上角
				ctx.arc(x + width - radius, y + radius, radius, Math.PI * 1.5, Math.PI * 2)
				// 右下角
				ctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI * 0.5)
				// 左下角
				ctx.arc(x + radius, y + height - radius, radius, Math.PI * 0.5, Math.PI)
				// 回到原点的直线
				ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI)
				ctx.stroke()
				ctx.clip()
				ctx.drawImage(img, x, y, width, height);
				ctx.restore()
				ctx.closePath()

			},

			// 团长头像
			avaRound(ctx, img, x, y, radius) {
				ctx.save();
				let size = 2 * radius;
				ctx.arc(x, y, radius, 0, 2 * Math.PI);
				ctx.clip();
				ctx.drawImage(img, x - radius, y - radius, size, size);
				ctx.restore()
			},

			//圆角矩形
			drawRoundRect(ctx, x, y, width, height, radius, color) {
				ctx.save();
				ctx.beginPath();
				ctx.setFillStyle(color);
				ctx.setStrokeStyle(color)
				ctx.setLineJoin('round'); //交点设置成圆角
				ctx.setLineWidth(radius);
				ctx.strokeRect(x + radius / 2, y + radius / 2, width - radius, height - radius);
				ctx.fillRect(x + radius, y + radius, width - radius * 2, height - radius * 2);
				ctx.stroke();
				ctx.closePath();
			},

			//获取图片
			getImageInfo(imgSrc) {
				return new Promise((resolve, reject) => {
					uni.getImageInfo({
						src: imgSrc,
						success: (image) => {
							resolve(image);
							console.log('获取图片成功', image)
						},
						fail: (err) => {
							reject(err);
							console.log('获取图片失败', err)
						}
					});
				});
			},

			//保存图片到相册
			saveImage() {
				//判断用户授权
				// uni.getSetting({
				// 	success(res) {
				// 		// console.log('获取用户权限', res.authSetting)
				// 		if (Object.keys(res.authSetting).length > 0) {
				// 			//判断是否有相册权限
				// 			if (res.authSetting['scope.writePhotosAlbum'] == undefined) {
				// 				//打开设置权限
				// 				uni.openSetting({
				// 					success(res) {
				// 						// console.log('设置权限', res.authSetting)
				// 					}
				// 				})
				// 			} else {
				// 				if (!res.authSetting['scope.writePhotosAlbum']) {
				// 					//打开设置权限
				// 					uni.openSetting({
				// 						success(res) {
				// 							// console.log('设置权限', res.authSetting)
				// 						}
				// 					})
				// 				}
				// 			}
				// 		} else {
				// 			return
				// 		}
				// 	}
				// })
				var that = this
				uni.canvasToTempFilePath({
					canvasId: 'my-canvas',
					quality: 1,
					complete: (res) => {
						// console.log('保存到相册', res);
						uni.saveImageToPhotosAlbum({
							filePath: res.tempFilePath,
							success(res) {
								uni.showToast({
									title: '已保存到相册',
									icon: 'success',
									duration: 2000
								})
								setTimeout(() => {
									that.isShow = false
								}, 2000)
							}
						})
					}
				}, that);
			}
		}
	}
</script>

<style scoped lang="scss">
	.content {
		position: fixed;
		width: 750rpx;
		height: 100%;
		top: 0;
		left: 0;
		right: 0;
		bottom: 0;
		background: rgba(0, 0, 0, .4);
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: center;
		
		// 保存按钮
		.save-btn {
			margin-top: 35rpx;
			width: 501rpx;
			height: 83rpx;
			background: url(https://xxxx.png) no-repeat;
			background-size: 100%;
		}
	}
</style>
注意

子组件的获取图片方法getImageInfo(),会验证图片获取成功否. 所以在开发过程中如果海报未能展示,请查看该方法,相关图片是否获取成功