uniapp (微信小程序)电子签名——横屏,签字图片可正向展示,并传给后端

      最近因为项目需求研究了一阵子微信小程序手写签名,刚开始是从dcloud插件市场下载了一个插件来直接用(图一)。这个插件其实已经具备了很多功能,可以根据需要设置横屏和竖屏,但是有一个很大的问题是横屏签好字之后,生成的图片是竖屏的,这一点很不好,前端可以在展示的时候用 transform: rotate(-90deg);把图片逆时针旋转到正面显示,但是传给后端的格式就不对了,所以,再此基础之上,我做了一些小小的改动,让这个插件更完美。

图一:

步骤1:使用插件,签字图片显示

<template>
	<view class="">
		<view class="preview" v-if="imgUrl">
			<image :src="imgUrl" mode=""></image>
		</view>
		<view class="sign">
			<!--abeam-横屏,portrait ==> 纵向 默认纵向  -->
		    <csr-sign @finish="signatureChange" orientation="abeam"></csr-sign>
		</view>
	</view>
</template>

<script>
	export default {
		data(){
			return{
				imgUrl:'',
			}
		},
		methods:{
			signatureChange(e) {
				this.imgUrl = e
				//根据后端需要自己做处理即可
				//由于我的后端需要把'data:image/png;base64,'去掉,在这里处理一下,
				let str,imgURL
				str = this.imgUrl.indexOf('/',this.imgUrl.indexOf('/')+1)
			    imgURL = this.imgUrl.substring(str+1)
			},
		}
	}
</script>

<style>
	.preview>image{
		/*如果不在插件里面做旋转处理就直接用transform旋转图片就行了 */
		/* transform: rotate(-90deg);
		width:100rpx;
		height: 300rpx; */
		/* 在插件里面做了旋转处理就直接设置图片显示宽高就行 */
		width:500rpx;
		height:200rpx;
	}
</style>

2、 插件内部调整

 (1)创建 Canvas 组件:在 .wxml 文件中添加一个 Canvas 组件。

<view class="">
			<canvas id="myCanvas" canvas-id="myCanvas" @touchmove="move" @touchstart="start" @error="error"
				@touchend="touchend" :style="{width:canvasWidth + 'px',height:canvasHeight + 'px'}">
			</canvas>
		</view>

 注意:新版canvas需要把canvas-id="myCanvas"换成type="2d"

(2)在onReady中获取canvas上下文:  let ctx = uni.createCanvasContext('myCanvas', this);

onReady() {
	this.createCanvas();
},
methods:{
      createCanvas() {
				const pr = this.pr; // 像素比
				ctx = uni.createCanvasContext('myCanvas', this);
				ctx.lineGap = 'round';
				ctx.lineJoin = 'round';
				ctx.lineWidth = this.lineWidth; // 字体粗细
				ctx.strokeStyle = this.strokeStyle; // 字体粗细
				this.ctx = ctx;
			},
}

 注意:在微信小程序中,获取 Canvas 上下文时需要确保 DOM 已经完全加载完毕。在 onLoadonShow 生命周期钩子中直接获取 Canvas 上下文可能会因为 DOM 还未加载完成而导致获取不到正确的节点。

(3) 触摸开始 :

  • 当用户开始触摸屏幕时,记录触摸点的位置,并开始一条新的路径。
  • 使用 beginPath 方法开始一条新的路径,并将起点设置为当前触摸点的位置。
  • 将当前触摸点的位置保存到this.points 中。
start(e) {
				this.points.push({
					X: e.touches[0].x,
					Y: e.touches[0].y
				});
				//每次触摸开始,开启新的路径
				this.ctx.beginPath();
			},

(4) 触摸移动 :

  • 当用户移动手指时,从上次触摸点的位置绘制到当前触摸点的位置。
  • 使用 lineTo 方法将路径延伸到当前触摸点的位置,并使用 stroke 方法绘制路径。
  • 更新 this.points 为当前触摸点的位置。
// 开始移动
	move(e) {
		this.points.push({
		  X: e.touches[0].x,
		  Y: e.touches[0].y
	    }); //存点
	if (this.points.length >= 2) {
		this.draw(); //绘制路径
	}
},
/* ***********************************************
 绘制笔迹
 1.为保证笔迹实时显示,必须在移动的同时绘制笔迹
 2.为保证笔迹连续,每次从路径集合中区两个点作为起点(moveTo)和终点(lineTo)
 3.将上一次的终点作为下一次绘制的起点(即清除第一个点)
************************************************ */
draw() {
	let point1 = this.points[0];
	let point2 = this.points[1];
	this.points.shift();
	this.ctx.moveTo(point1.X, point1.Y);
	this.ctx.lineTo(point2.X, point2.Y);
	this.ctx.stroke();
	this.ctx.draw(true);
	this.tempPoint.push(this.points)
},

(5)触摸结束: 

  • 当用户抬起手指时,停止绘制。
// 触摸结束,将未绘制的点清空防止对后续路径产生干扰
			touchend() {
				this.points = [];
			},

(6)完成签名

       这里用tempPoint之前存的触摸点的坐标来判断是否有签名,没有会提示

// 点击完成签名
			finish() {
				let that = this
				if (that.tempPoint.length == 0) {
					uni.showToast({
						title: '您还未签名,请先签名',
						icon: 'none',
						duration: 2000
					});
					return
				}
				uni.canvasToTempFilePath({
					canvasId: 'myCanvas',
					success: function(res) {
						if (res.tempFilePath.startsWith('data:image/png;base64')) {
							that.emit(res.tempFilePath)
						} else {
							that.translateImg(res.tempFilePath)
						}
					},
					fail(e) {
						console.log(JSON.stringify(e))
					}

				}, this)//要用this指向,要不然canvasToTempFilePath会报undefined
			},
//回调父组件finish方法
emit(tempFilePath) {
	this.$emit("finish", tempFilePath);
},

(7)最后一步,旋转画布 

a、获取图片信息wx.getImageInfo,拿到刚刚生成的签名图片信息,宽高和本地路径

wx.getImageInfo({
					src: tempFilePath,
					success: (res) => {
						const path = res.path
						const height = res.width; //宽305
						const width = res.height; //高603
})

b、重新获取一个新的上下文,把之前的笔记清除掉

// 2. 创建画布上下文
 const contx = uni.createCanvasContext('myCanvas', _this);
//清除掉原本的笔迹
contx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 将旋转中心设置为canvas的中心    
contx.translate(150, 260);
// 逆时针旋转90度
contx.rotate(-Math.PI / 2);
//3、将图片绘制到canvas上,注意:这里的坐标是旋转后的坐标,需要调整
contx.drawImage(path, -300, -150, 500, 200);

 c、将临时文件路径转换为Base64

pathToBase64(path) {
				return new Promise(function(resolve, reject) {
					// app
					if (typeof plus === 'object') {
						plus.io.resolveLocalFileSystemURL(path, function(entry) {
							entry.file(function(file) {
								var fileReader = new plus.io.FileReader()
								fileReader.onload = function(evt) {
									resolve(evt.target.result)
								}
								fileReader.onerror = function(error) {
									reject(error)
								}
								fileReader.readAsDataURL(file)
							}, function(error) {
								reject(error)
							})
						}, function(error) {
							reject(error)
						})

						return
					}
					// 微信小程序
					if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
						wx.getFileSystemManager().readFile({
							filePath: path,
							encoding: 'base64',
							success: function(res) {
								resolve('data:image/png;base64,' + res.data)
							},
							fail: function(error) {
								reject(error)
							}
						})
						return
					}
					reject(new Error('not support'))
				})
			}

3、完整代码

<template>
	<view class="csr-sign">
		<view v-if="orientation == 'abeam'">
			<view class="sighButtons">
				<view class="sighButtons_button"
					style="border: solid 1rpx #999; color: #999;background-color: #fff;"
					@click.tap="back">
					<view class="sighButtons_button_xiao">返回</view>
				</view>
				<view class="sighButtons_button" style="background: #999;" @click.tap="clearClick">
					<view class="sighButtons_button_xiao">重签</view>
				</view>
				<view class="sighButtons_button" style="background-color: #0a8afd;" @click.tap="finish">
					<view class="sighButtons_button_xiao">完成签名</view>
				</view>
			</view>
		</view>
		<view class="">
			<canvas id="myCanvas" canvas-id="myCanvas" @touchmove="move" @touchstart="start" @error="error"
				@touchend="touchend" :style="{width:canvasWidth + 'px',height:canvasHeight + 'px'}">
			</canvas>
		</view>

		<view v-if="orientation == 'portrait'">
			<view class="sighButton ">
				<view class="sighButton_button" style="border: solid 1rpx #999; color: #999;background-color: #fff;"
					@click.tap="back">
					<view class="sighButton_button_xiao">返回</view>
				</view>
				<view class="sighButton_button" style="background: #999;" @click.tap="clearClick">
					<view class="sighButton_button_xiao">重签</view>
				</view>
				<view class="sighButton_button" style="background-color: #0a8afd;" @click.tap="finish">
					<view class="sighButton_button_xiao">完成签名</view>
				</view>
			</view>
		</view>
	</view>
</template>

<script>
	let ctx
	export default {
		name: "csr-sign",
		props: {
			orientation: {
				type: String,
				default: 'portrait'
			},
			width: {
				type: Number,
				default: 0
			},
			height: {
				type: Number,
				default: 0
			},
			lineWidth: {
				type: Number,
				default: 3
			},
			strokeStyle: {
				type: String,
				default: "black"
			}
		},
		data() {
			return {
				canvas: '',
				ctx: '',
				pr: 0,
				canvasWidth: '',
				canvasHeight: '',
				points: [],
				tempPoint: []
			};
		},
		onReady() {
			this.getSystemInfo();
			this.createCanvas();
		},
		methods: {
			createCanvas() {
				const pr = this.pr; // 像素比
				ctx = uni.createCanvasContext('myCanvas', this);
				ctx.lineGap = 'round';
				ctx.lineJoin = 'round';
				ctx.lineWidth = this.lineWidth; // 字体粗细
				ctx.strokeStyle = this.strokeStyle; // 字体粗细
				this.ctx = ctx;
			},
			// 获取系统信息
			getSystemInfo() {
				uni.getSystemInfo({
					success: (res) => {
						this.pr = res.pixelRatio;
						if (this.orientation == 'portrait') {
							if (this.width > res.windowWidth || this.width == 0) {
								this.canvasWidth = res.windowWidth;
							} else {
								this.canvasWidth = this.width;
							}
							if (this.height > res.windowHeight - 70 || this.height == 0) {
								this.canvasHeight = res.windowHeight - 70;
							} else {
								this.canvasHeight = this.height;
							}
						} else if (this.orientation == 'abeam') {
							if (this.width > res.windowWidth - 70 || this.width == 0) {
								this.canvasWidth = res.windowWidth - 70;
							} else {
								this.canvasWidth = this.width;
							}
							if (this.height > res.windowHeight || this.height == 0) {
								this.canvasHeight = res.windowHeight;
							} else {
								this.canvasHeight = this.height;
							}
						}
					}
				})
			},
			// 触摸开始
			start(e) {
				this.points.push({
					X: e.touches[0].x,
					Y: e.touches[0].y
				});
				//每次触摸开始,开启新的路径
				this.ctx.beginPath();
			},
			// 开始移动
			move(e) {
				this.points.push({
					X: e.touches[0].x,
					Y: e.touches[0].y
				}); //存点
				if (this.points.length >= 2) {
					this.draw(); //绘制路径
				}
			},
			// 触摸结束,将未绘制的点清空防止对后续路径产生干扰
			touchend() {
				this.points = [];
			},
			/* ***********************************************
			   绘制笔迹
			   1.为保证笔迹实时显示,必须在移动的同时绘制笔迹
			   2.为保证笔迹连续,每次从路径集合中区两个点作为起点(moveTo)和终点(lineTo)
			   3.将上一次的终点作为下一次绘制的起点(即清除第一个点)
			************************************************ */
			draw() {
				let point1 = this.points[0];
				let point2 = this.points[1];
				this.points.shift();
				this.ctx.moveTo(point1.X, point1.Y);
				this.ctx.lineTo(point2.X, point2.Y);
				this.ctx.stroke();
				this.ctx.draw(true);
				this.tempPoint.push(this.points)
			},
			error(e) {
				console.log("画布触摸错误" + e);
			},
			// 点击完成签名
			finish() {
				let that = this
				if (that.tempPoint.length == 0) {
					uni.showToast({
						title: '您还未签名,请先签名',
						icon: 'none',
						duration: 2000
					});
					return
				}
				uni.canvasToTempFilePath({
					canvasId: 'myCanvas',
					success: function(res) {
						if (res.tempFilePath.startsWith('data:image/png;base64')) {
							that.emit(res.tempFilePath)
						} else {
							that.translateImg(res.tempFilePath)
						}
					},
					fail(e) {
						console.log(JSON.stringify(e))
					}

				}, this)//要用this指向,要不然canvasToTempFilePath会报undefined
			},
			emit(tempFilePath) {
				this.$emit("finish", tempFilePath);
			},
			//重签
			clearClick() {
				//清除画布
				this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
				this.ctx.draw(true);
				this.tempPoint = []
			},
			// 返回事件
			back() {
				uni.navigateBack();
			},

			//因为设置了横屏手写签名,输出的图片需要逆时针旋转90度才能正确显示,传给后端的图片也是正确的
			// 1. 将Base64转换为临时文件路径
			translateImg(tempFilePath) {
				let _this = this
				// 1、获取图片信息
				wx.getImageInfo({
					src: tempFilePath,
					success: (res) => {
						const path = res.path
						const height = res.width; //宽305
						const width = res.height; //高603

						// 2. 创建画布上下文
						const contx = uni.createCanvasContext('myCanvas', _this);
						//清除掉原本的笔迹
						contx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
						// 将旋转中心设置为canvas的中心    
						contx.translate(150, 260);
						// 逆时针旋转90度
						contx.rotate(-Math.PI / 2);
						//3、将图片绘制到canvas上,注意:这里的坐标是旋转后的坐标,需要调整
						contx.drawImage(path, -300, -150, 500, 200);
						contx.draw(true, () => { // 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中
							uni.canvasToTempFilePath({
								canvasId: 'myCanvas',
								success: function(res) {
									//4. 将临时文件路径转换为Base64
									_this.pathToBase64(res.tempFilePath).then(e => {
										_this.emit(e)
									}).catch(e => {
										console.log(JSON.stringify(e))
									})
								},
								fail(e) {
									console.log(JSON.stringify(e))
								}
							}, _this)
						}, _this) // 在自定义组件下,当前组件实例的this,以操作组件内 canvas 组件
					},
					fail: err => {
						console.error('Failed to get image info:', err);
					}
				});
			},
			pathToBase64(path) {
				return new Promise(function(resolve, reject) {
					// app
					if (typeof plus === 'object') {
						plus.io.resolveLocalFileSystemURL(path, function(entry) {
							entry.file(function(file) {
								var fileReader = new plus.io.FileReader()
								fileReader.onload = function(evt) {
									resolve(evt.target.result)
								}
								fileReader.onerror = function(error) {
									reject(error)
								}
								fileReader.readAsDataURL(file)
							}, function(error) {
								reject(error)
							})
						}, function(error) {
							reject(error)
						})

						return
					}
					// 微信小程序
					if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
						wx.getFileSystemManager().readFile({
							filePath: path,
							encoding: 'base64',
							success: function(res) {
								resolve('data:image/png;base64,' + res.data)
							},
							fail: function(error) {
								reject(error)
							}
						})
						return
					}
					reject(new Error('not support'))
				})
			}
		}
	}
</script>

<style lang="scss">
	page {
		width: 100%;
		height: 100%;
		background-color: #e9f2f1;
	}

	canvas {
		background-color: white;
	}

	.csr-sign {
		width: 100%;
		height: 100%;
		display: flex;
		flex-flow: row;
		align-items: flex-end;
	}

	.sighButton {
		width: 690rpx;
		margin: 0rpx auto;
		height: 60px;
		padding-top: 10px;
		display: flex;
		justify-content: space-between;

		&_button {
			width: 30%;
			height: 100rpx;
			border-radius: 10rpx;
			display: flex;
			align-items: center;
			justify-content: center;
			font-size: 32rpx;
			color: #fff;
		}

	}

	.sighButtons {
		width: 200rpx;
		// margin: 0rpx auto;
		height: calc(100vh);
		display: flex;
		flex-wrap: wrap;
		justify-content: center;
		align-items: center;
		// margin-left: 80rpx;

		.sighButtons_button {
			width: 200rpx;
			height: 55px;
			border-radius: 10rpx;
			display: flex;
			align-items: center;
			justify-content: center;
			font-size: 32rpx;
			color: #fff;
			transform: rotate(90deg);
		}

	}
</style>

 4、效果图

5、我的代码里面用的都是旧版的canvas,新版的canvas api需要参考文档,内容有不足之处,请多多指正。

画布 / wx.createCanvasContext (qq.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值