Skip to content

ml-signature手写签名

组件名:ml-signature

mode是布局是必传的;

默认已有保存到相册的功能,想要点击保存后上传图片,需用upload配合onSign事件使用;

竖屏保存图片,图片就是竖的,反之横屏也是横的

调用方法

js
<template>
	<view>
		<ml-signature mode="horizontal"></ml-signature>
	</view>
</template>

<script>
	import mlSignature from "@/components/ml-signature/ml-signature.vue"
	export default {
		data() {
			return {};
		},
		components:{
			mlSignature
		},
	}
</script>
<template>
	<view>
		<ml-signature mode="horizontal"></ml-signature>
	</view>
</template>

<script>
	import mlSignature from "@/components/ml-signature/ml-signature.vue"
	export default {
		data() {
			return {};
		},
		components:{
			mlSignature
		},
	}
</script>

组件参数

参数注释
color画笔(字)的颜色,默认#000
size画笔(字)的粗细,默认4
mode布局,横屏:vertical,竖屏:horizontal,默认:horizontal
upload上传,布尔类型,默认false
text提示语
textColor提示语颜色
clearText清空按钮
clearColor清空按钮颜色,默认#fff
clearBgColor清空按钮背景色,默认#F565C6C
saveText保存按钮
saveColor保存按钮颜色,默认#FFF
saveBgColor保存按钮背景色,默认#409EFF

事件

事件说明
onSign此事件用于上传图片,参数就是图片链接,需要搭配upload参数使用,默认已有保存到本地相册功能

组件代码

vue
<template>
	<view class="box">
		<view :class="[mode==='horizontal'?'vertical':'horizontal']">
			<canvas canvas-id="canvasId" @touchstart="handleTouchStart" @touchmove="handleTouchMove"
				:style="{width: width+'px', height: height+'px'}"></canvas>
			<view class="bottom">
				<view class="bot_text" :style="{color:textColor}">{{text}}</view>
				<view class="btn">
					<button class="clear" :style="{color:clearColor,background:clearBgColor}" @click="clear">{{clearText}}</button>
					<button class="save" :style="{color:saveColor,background:saveBgColor}" @click="save">{{saveText}}</button>
				</view>
			</view>
		</view>
	</view>
	
</template>

<script>
	export default {
		data() {
			return {
				width: '',
				height: '',
				ctx: '',
				moveX: '',
				moveY: '',
			};
		},
		props: {
			color: {
				type: String,
				default: "#000"
			},
			size: {
				type: Number,
				default: 4
			},
			mode:{
				type:String,
				default:"horizontal"
			},
			upload:{
				type:Boolean,
				default:false
			},
			text:{
				type:String,
				default:"请在输入框内签名"
			},
			textColor:{
				type:String,
				default:"#999"
			},
			clearText:{
				type:String,
				default:"清空"
			},
			clearColor:{
				type:String,
				default:"#fff"
			},
			clearBgColor:{
				type:String,
				default:"#F56C6C"
			},
			saveText:{
				type:String,
				default:"保存"
			},
			saveColor:{
				type:String,
				default:"#fff"
			},
			saveBgColor:{
				type:String,
				default:"#409EFF"
			}
		},
		mounted() {
			// 获取屏幕宽高
			if(this.mode=='horizontal'){
				let width = uni.getSystemInfoSync().windowWidth
				let height = uni.getSystemInfoSync().windowHeight-70
				// 计算画板的高宽
				this.width = width - 4
				this.height = height
			}else{
				let width = uni.getSystemInfoSync().windowWidth
				let height = uni.getSystemInfoSync().windowHeight-13
				// 计算画板的高宽
				this.width = width - 4
				this.height = height*0.99
			}
			
			this.ctx = uni.createCanvasContext("canvasId", this)
			this.ctx.setLineWidth(this.$props.size)
			this.ctx.setStrokeStyle(this.$props.color)
			this.ctx.lineCap = 'round'
			this.ctx.lineJoin = 'round'
		},
		methods: {
			handleTouchStart(e) {
				// console.log(e);
				let startX = e.changedTouches[0].x;
				let startY = e.changedTouches[0].y;
				this.ctx.beginPath()
				this.ctx.moveTo(startX, startY)
				this.moveX = ''
				this.moveY = ''
			},
			handleTouchMove(e) {
				// console.log(e);
				let movesX = e.changedTouches[0].x;
				let movesY = e.changedTouches[0].y;
				// 防止线条出现断点
				if (this.moveX && this.moveY) {
					this.ctx.moveTo(this.moveX, this.moveY)
					this.ctx.lineTo(this.moveX, this.moveY)
				}
				this.ctx.lineTo(movesX, movesY)
				this.moveX = movesX
				this.moveY = movesY
				this.ctx.stroke()
				this.ctx.draw(true)

			},

			clear() {
				this.ctx.clearRect(0, 0, this.width, this.height);
				this.ctx.draw();
				uni.showToast({
					icon: "none",
					title: "已清空"
				})
			},
			saveImage(){
				
				const that = this;
				if(that.moveX==0&&that.moveY==0){
					uni.showToast({
						icon:"none",
						title:"请先签名"
					})
					return false;
				}
				uni.canvasToTempFilePath({
					canvasId: 'canvasId',
					destWidth:that.mode=='horizontal'?uni.getSystemInfoSync().windowWidth:uni.getSystemInfoSync().windowHeight,
					destHeight:that.mode=='horizontal'?uni.getSystemInfoSync().windowHeight:uni.getSystemInfoSync().windowWidth,
					success: function(res) {
						let path = res.tempFilePath;
						if(that.upload){
							that.$emit("onSign",path)
						}else{
							uni.showLoading({
								title: '正在保存图片...'
							});
							setTimeout(()=>{
								// #ifdef APP-PLUS||MP-WEIXIN
								uni.saveImageToPhotosAlbum({
									filePath: path,
									success() {
										uni.showToast({
											icon: "none",
											title: "已保存到相册"
										})
									},
									fail() {
										uni.showToast({
											icon: "none",
											title: "已取消"
										})
									}
								})
								// #endif
								
								// #ifdef H5
								const bytes = atob(path.split(',')[1]);
								const ab = new ArrayBuffer(bytes.length);
								const ia = new Uint8Array(ab);
								for (let i = 0; i < bytes.length; i++) {
									ia[i] = bytes.charCodeAt(i);
								}
												
								const blob = new Blob([ab], {
									type: 'image/png'
								});
								const url = URL.createObjectURL(blob);
												
								const link = document.createElement('a');
								link.href = url;
								link.download = 'image.png';
								link.click();
								uni.showToast({
									icon: "none",
									title: "已保存到相册"
								})
								// #endif
								uni.hideLoading()
							},2000)
						}
						
					},
					fail(err) {
						console.log(err);
					}
				},that)
			},
			
			save() {
				
				this.saveImage()
				
			}
		}
	}
</script>

<style lang="less" scoped>
	.box{
		.vertical{
			height: 100%;
			.bottom{
				display: flex;
				justify-content: space-between;
				align-items: center;
				padding: 20rpx;
				.bot_text{
					font-size: 30rpx;
					color: #999;
				}
				.btn {
					display: flex;
					align-items: center;
					
					.save {
						font-size: 28rpx;
						background: #409EFF;
						color: #fff;
						padding: 0 30rpx;
						margin-left: 30rpx;
					}
					
					.clear {
						font-size: 28rpx;
						background: #F56C6C;
						color: #fff;
						padding: 0 30rpx;
						margin: 0;
					}
				}
			}
			
			canvas {
				border: 2px dashed #DCDFE6;
			}
		} 
		.horizontal{
			display: flex;
			flex-direction: row-reverse;
			.bottom{
				display: flex;
				flex-direction: column;
				justify-content: space-between;
				align-items: center;
				padding: 140rpx 20rpx;
				box-sizing: border-box;
				width: 15%;
				.bot_text{
					font-size: 30rpx;
					color: #999;
					width: max-content;
					transform: rotate(90deg);
				}
				.btn {
					display: flex;
					margin-top: 15px;
					transform: rotate(90deg);
					.save {
						font-size: 28rpx;
						background: #409EFF;
						color: #fff;
						width: 120rpx;
						margin-left: 30rpx;
					}
					
					.clear {
						font-size: 28rpx;
						background: #F56C6C;
						color: #fff;
						width: 120rpx;
						margin: 0;
					}
				}
			}
			canvas {
				border: 2px dashed #DCDFE6;
			}
		} 
	}
	
	
</style>
<template>
	<view class="box">
		<view :class="[mode==='horizontal'?'vertical':'horizontal']">
			<canvas canvas-id="canvasId" @touchstart="handleTouchStart" @touchmove="handleTouchMove"
				:style="{width: width+'px', height: height+'px'}"></canvas>
			<view class="bottom">
				<view class="bot_text" :style="{color:textColor}">{{text}}</view>
				<view class="btn">
					<button class="clear" :style="{color:clearColor,background:clearBgColor}" @click="clear">{{clearText}}</button>
					<button class="save" :style="{color:saveColor,background:saveBgColor}" @click="save">{{saveText}}</button>
				</view>
			</view>
		</view>
	</view>
	
</template>

<script>
	export default {
		data() {
			return {
				width: '',
				height: '',
				ctx: '',
				moveX: '',
				moveY: '',
			};
		},
		props: {
			color: {
				type: String,
				default: "#000"
			},
			size: {
				type: Number,
				default: 4
			},
			mode:{
				type:String,
				default:"horizontal"
			},
			upload:{
				type:Boolean,
				default:false
			},
			text:{
				type:String,
				default:"请在输入框内签名"
			},
			textColor:{
				type:String,
				default:"#999"
			},
			clearText:{
				type:String,
				default:"清空"
			},
			clearColor:{
				type:String,
				default:"#fff"
			},
			clearBgColor:{
				type:String,
				default:"#F56C6C"
			},
			saveText:{
				type:String,
				default:"保存"
			},
			saveColor:{
				type:String,
				default:"#fff"
			},
			saveBgColor:{
				type:String,
				default:"#409EFF"
			}
		},
		mounted() {
			// 获取屏幕宽高
			if(this.mode=='horizontal'){
				let width = uni.getSystemInfoSync().windowWidth
				let height = uni.getSystemInfoSync().windowHeight-70
				// 计算画板的高宽
				this.width = width - 4
				this.height = height
			}else{
				let width = uni.getSystemInfoSync().windowWidth
				let height = uni.getSystemInfoSync().windowHeight-13
				// 计算画板的高宽
				this.width = width - 4
				this.height = height*0.99
			}
			
			this.ctx = uni.createCanvasContext("canvasId", this)
			this.ctx.setLineWidth(this.$props.size)
			this.ctx.setStrokeStyle(this.$props.color)
			this.ctx.lineCap = 'round'
			this.ctx.lineJoin = 'round'
		},
		methods: {
			handleTouchStart(e) {
				// console.log(e);
				let startX = e.changedTouches[0].x;
				let startY = e.changedTouches[0].y;
				this.ctx.beginPath()
				this.ctx.moveTo(startX, startY)
				this.moveX = ''
				this.moveY = ''
			},
			handleTouchMove(e) {
				// console.log(e);
				let movesX = e.changedTouches[0].x;
				let movesY = e.changedTouches[0].y;
				// 防止线条出现断点
				if (this.moveX && this.moveY) {
					this.ctx.moveTo(this.moveX, this.moveY)
					this.ctx.lineTo(this.moveX, this.moveY)
				}
				this.ctx.lineTo(movesX, movesY)
				this.moveX = movesX
				this.moveY = movesY
				this.ctx.stroke()
				this.ctx.draw(true)

			},

			clear() {
				this.ctx.clearRect(0, 0, this.width, this.height);
				this.ctx.draw();
				uni.showToast({
					icon: "none",
					title: "已清空"
				})
			},
			saveImage(){
				
				const that = this;
				if(that.moveX==0&&that.moveY==0){
					uni.showToast({
						icon:"none",
						title:"请先签名"
					})
					return false;
				}
				uni.canvasToTempFilePath({
					canvasId: 'canvasId',
					destWidth:that.mode=='horizontal'?uni.getSystemInfoSync().windowWidth:uni.getSystemInfoSync().windowHeight,
					destHeight:that.mode=='horizontal'?uni.getSystemInfoSync().windowHeight:uni.getSystemInfoSync().windowWidth,
					success: function(res) {
						let path = res.tempFilePath;
						if(that.upload){
							that.$emit("onSign",path)
						}else{
							uni.showLoading({
								title: '正在保存图片...'
							});
							setTimeout(()=>{
								// #ifdef APP-PLUS||MP-WEIXIN
								uni.saveImageToPhotosAlbum({
									filePath: path,
									success() {
										uni.showToast({
											icon: "none",
											title: "已保存到相册"
										})
									},
									fail() {
										uni.showToast({
											icon: "none",
											title: "已取消"
										})
									}
								})
								// #endif
								
								// #ifdef H5
								const bytes = atob(path.split(',')[1]);
								const ab = new ArrayBuffer(bytes.length);
								const ia = new Uint8Array(ab);
								for (let i = 0; i < bytes.length; i++) {
									ia[i] = bytes.charCodeAt(i);
								}
												
								const blob = new Blob([ab], {
									type: 'image/png'
								});
								const url = URL.createObjectURL(blob);
												
								const link = document.createElement('a');
								link.href = url;
								link.download = 'image.png';
								link.click();
								uni.showToast({
									icon: "none",
									title: "已保存到相册"
								})
								// #endif
								uni.hideLoading()
							},2000)
						}
						
					},
					fail(err) {
						console.log(err);
					}
				},that)
			},
			
			save() {
				
				this.saveImage()
				
			}
		}
	}
</script>

<style lang="less" scoped>
	.box{
		.vertical{
			height: 100%;
			.bottom{
				display: flex;
				justify-content: space-between;
				align-items: center;
				padding: 20rpx;
				.bot_text{
					font-size: 30rpx;
					color: #999;
				}
				.btn {
					display: flex;
					align-items: center;
					
					.save {
						font-size: 28rpx;
						background: #409EFF;
						color: #fff;
						padding: 0 30rpx;
						margin-left: 30rpx;
					}
					
					.clear {
						font-size: 28rpx;
						background: #F56C6C;
						color: #fff;
						padding: 0 30rpx;
						margin: 0;
					}
				}
			}
			
			canvas {
				border: 2px dashed #DCDFE6;
			}
		} 
		.horizontal{
			display: flex;
			flex-direction: row-reverse;
			.bottom{
				display: flex;
				flex-direction: column;
				justify-content: space-between;
				align-items: center;
				padding: 140rpx 20rpx;
				box-sizing: border-box;
				width: 15%;
				.bot_text{
					font-size: 30rpx;
					color: #999;
					width: max-content;
					transform: rotate(90deg);
				}
				.btn {
					display: flex;
					margin-top: 15px;
					transform: rotate(90deg);
					.save {
						font-size: 28rpx;
						background: #409EFF;
						color: #fff;
						width: 120rpx;
						margin-left: 30rpx;
					}
					
					.clear {
						font-size: 28rpx;
						background: #F56C6C;
						color: #fff;
						width: 120rpx;
						margin: 0;
					}
				}
			}
			canvas {
				border: 2px dashed #DCDFE6;
			}
		} 
	}
	
	
</style>

程序员小洛文档