uni-app canvas 签名

调用方法

javascript 复制代码
import Signature from "@/components/signature.vue"
const base64Img = ref()
//监听getSignImg
uni.$on('getSignImg', ({ base64, path }) => {
		base64Img.value = base64
	    //console.log('签名base64, path ====>', base64, path) //拿到的图片数据
	    // 之后取消监听,防止重复监听
	    uni.$off('getSignImg')
	})
javascript 复制代码
<Signature :showMark="false"  @cancle="cancle"></Signature>

signature.vue

javascript 复制代码
<template>
	<view class="sign-page" v-cloak>
		<view class="dis-flex justify-end">
			<uv-icon name="close" color="#333" size="48rpx" @click="cancle"></uv-icon>
		</view>
		<view class="sign-body">
			<canvas id="signCanvas" canvas-id="signCanvas" class="sign-canvas" disable-scroll
				@touchstart.stop="signCanvasStart" @touchmove.stop="signCanvasMove"
				@touchend.stop="signCanvasEnd"></canvas>
			<!-- #ifndef APP -->
			<!--用于临时储存横屏图片的canvas容器,H5和小程序需要-->
			<canvas v-if="horizontal" id="hsignCanvas" canvas-id="hsignCanvas"
				style="position: absolute; top: -1000px; z-index: -1"
				:style="{ width: canvasHeight + 'px', height: canvasWidth + 'px' }"></canvas>
			<!-- #endif -->
		</view>
		<view class="sign-footer" :class="[horizontal ? 'horizontal-btns' : 'vertical-btns']">
			<uv-button
				customStyle="margin-top: 20rpx;width:300rpx;height:100rpx;border-radius:20rpx;border:1px solid #3894FF"
				@click="reset">
				<uv-icon name="shuaxin" color="#3894FF" size="48rpx" custom-prefix="custom-icon"></uv-icon>
				<text class="txt">重新签字</text>
			</uv-button>

			<uv-button type="primary" text="确定提交" customTextStyle="font-size:36rpx"
				customStyle="margin-top: 20rpx;width:300rpx;height:100rpx;border-radius:20rpx"
				@click="confirm"></uv-button>
		</view>
	</view>
</template>

<script>
	import {
		pathToBase64,
		base64ToPath
	} from '@/utils/signature.js'
	export default {
		name: 'sign',
		props: {
			// 背景水印图,优先级大于 bgColor
			bgImg: {
				type: String,
				default: ''
			},
			// 背景纯色底色,为空则透明
			bgColor: {
				type: String,
				default: ''
			},
			// 是否显示水印
			showMark: {
				type: Boolean,
				default: true
			},
			// 水印内容,可多行
			markText: {
				type: Array,
				default: () => {
					return [] // ['水印1', '水印2']
				}
			},
			// 水印样式
			markStyle: {
				type: Object,
				default: () => {
					return {
						fontSize: 12, // 水印字体大小
						fontFamily: 'microsoft yahei', // 水印字体
						color: '#cccccc', // 水印字体颜色
						rotate: 60, // 水印旋转角度
						step: 2.2 // 步长,部分场景下可通过调节该参数来调整水印间距,建议为1.4-2.6左右
					}
				}
			},
			// 是否横屏
			horizontal: {
				type: Boolean,
				default: false
			},
			// 画笔样式
			penStyle: {
				type: Object,
				default: () => {
					return {
						lineWidth: 3, // 画笔线宽 建议1~5
						color: '#000000' // 画笔颜色
					}
				}
			},
			// 导出图片配置
			expFile: {
				type: Object,
				default: () => {
					return {
						fileType: 'png', // png/jpg (png不可压缩质量,支持透明;jpg可压缩质量,不支持透明)
						quality: 1 // 范围 0 - 1 (仅jpg支持)
					}
				}
			}
		},
		data() {
			return {
				canvasCtx: null, // canvascanvasWidth: 0, // canvas宽度
				canvasWidth: 0, // canvas宽度
				canvasHeight: 0, // canvas高度
				x0: 0, // 初始横坐标或上一段touchmove事件中触摸点的横坐标
				y0: 0, // 初始纵坐标或上一段touchmove事件中触摸点的纵坐标
				signFlag: false // 签名旗帜
			}
		},
		mounted() {
			this.$nextTick(() => {
				this.createCanvas()
			})
		},
		methods: {
			// 创建canvas实例
			createCanvas() {
				this.canvasCtx = uni.createCanvasContext('signCanvas', this)
				this.canvasCtx.setLineCap('round') // 向线条的每个末端添加圆形线帽

				// 获取canvas宽高
				const query = uni.createSelectorQuery().in(this)
				query
					.select('.sign-body')
					.boundingClientRect((data) => {
						this.canvasWidth = data.width
						this.canvasHeight = data.height
					})
					.exec(async () => {
						await this.drawBg()
						this.drawMark(this.markText)
					})
			},
			async drawBg() {
				if (this.bgImg) {
					const img = await uni.getImageInfo({
						src: this.bgImg
					})
					this.canvasCtx.drawImage(img.path, 0, 0, this.canvasWidth, this.canvasHeight)
				} else if (this.bgColor) {
					// 绘制底色填充,否则为透明
					this.canvasCtx.setFillStyle(this.bgColor)
					this.canvasCtx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
				}
			},
			// 绘制动态水印
			drawMark(textArray) {
				if (!this.showMark) {
					this.canvasCtx.draw()
					return
				}
				// 绘制背景
				this.drawBg()

				// 水印参数
				const markStyle = Object.assign({
						fontSize: 12, // 水印字体大小
						fontFamily: 'microsoft yahei', // 水印字体
						color: '#cccccc', // 水印字体颜色
						rotate: 60, // 水印旋转角度
						step: 2 // 步长,部分场景下可通过调节该参数来调整水印间距,建议为1.4-2.6左右
					},
					this.markStyle
				)
				this.canvasCtx.font = `${markStyle.fontSize}px ${markStyle.fontFamily}`
				this.canvasCtx.fillStyle = markStyle.color
				// 文字坐标
				const maxPx = Math.max(this.canvasWidth / 2, this.canvasHeight / 2)
				const stepPx = Math.floor(maxPx / markStyle.step)
				let arrayX = [0] // 初始水印位置 canvas坐标 0 0 点
				while (arrayX[arrayX.length - 1] < maxPx / 2) {
					arrayX.push(arrayX[arrayX.length - 1] + stepPx)
				}
				arrayX.push(
					...arrayX.slice(1, arrayX.length).map((item) => {
						return -item
					})
				)

				for (let i = 0; i < arrayX.length; i++) {
					for (let j = 0; j < arrayX.length; j++) {
						this.canvasCtx.save()
						this.canvasCtx.translate(this.canvasWidth / 2, this.canvasHeight / 2) // 画布旋转原点 移到 图片中心
						this.canvasCtx.rotate(Math.PI * (markStyle.rotate / 180))
						textArray.forEach((item, index) => {
							let offsetY = markStyle.fontSize * index
							this.canvasCtx.fillText(item, arrayX[i], arrayX[j] + offsetY)
						})
						this.canvasCtx.restore()
					}
				}

				this.canvasCtx.draw()
			},
			cancle() {
				//取消按钮事件
				this.$emit('cancle')
				this.reset()
				//uni.navigateBack()
			},
			async reset() {
				this.$emit('reset')
				this.signFlag = false
				this.canvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
				await this.drawBg()
				this.drawMark(this.markText)
			},
			async confirm() {
				this.$emit('confirm')
				// 确认按钮事件
				if (!this.signFlag) {
					uni.showToast({
						title: '请签名后再点击确定',
						icon: 'none',
						duration: 2000
					})
					return
				}

				uni.showModal({
					title: '确认',
					content: '确认签名无误吗',
					showCancel: true,
					success: async ({
						confirm
					}) => {
						if (confirm) {
							let tempFile
							if (this.horizontal) {
								tempFile = await this.saveHorizontalCanvas()
							} else {
								tempFile = await this.saveCanvas()
							}
							const base64 = await pathToBase64(tempFile)
							const path = await base64ToPath(base64)
							uni.$emit('getSignImg', {
								base64,
								path
							})
							//uni.navigateBack()
						}
					}
				})
			},
			signCanvasEnd(e) {
				// 签名抬起事件
				// console.log(e, 'signCanvasEnd')
				this.x0 = 0
				this.y0 = 0
			},
			signCanvasMove(e) {
				// 签名滑动事件
				// console.log(e, 'signCanvasMove')
				// #ifdef MP-WEIXIN
				let dx = e.touches[0].clientX - this.x0
				let dy = e.touches[0].clientY - this.y0
				// #endif
				// #ifndef MP-WEIXIN
				let dx = e.touches[0].x - this.x0
				let dy = e.touches[0].y - this.y0
				// #endif
				this.canvasCtx.moveTo(this.x0, this.y0)
				this.canvasCtx.lineTo(this.x0 + dx, this.y0 + dy)
				this.canvasCtx.setLineWidth(this.penStyle?.lineWidth || 4)
				this.canvasCtx.strokeStyle = this.penStyle?.color || '#000000'
				this.canvasCtx.stroke()
				this.canvasCtx.draw(true)
				// #ifdef MP-WEIXIN
				this.x0 = e.touches[0].clientX
				this.y0 = e.touches[0].clientY
				// #endif
				// #ifndef MP-WEIXIN
				this.x0 = e.touches[0].x
				this.y0 = e.touches[0].y
				// #endif
			},
			signCanvasStart(e) {
				// 签名按下事件 app获取的e不一样区分小程序app
				// console.log('signCanvasStart', e)
				if (!this.signFlag) {
					// 导出第一次开始触碰事件
					this.$emit('firstTouchStart')
				}
				this.signFlag = true
				// #ifdef MP-WEIXIN
				this.x0 = e.touches[0].clientX
				this.y0 = e.touches[0].clientY
				// #endif
				// #ifndef MP-WEIXIN
				this.x0 = e.touches[0].x
				this.y0 = e.touches[0].y
				// #endif
			},
			// 保存竖屏图片
			async saveCanvas() {
				return await new Promise((resolve, reject) => {
					uni.canvasToTempFilePath({
							canvasId: 'signCanvas',
							fileType: this.expFile.fileType, // 只支持png和jpg
							quality: this.expFile.quality, // 范围 0 - 1
							success: (res) => {
								if (!res.tempFilePath) {
									uni.showModal({
										title: '提示',
										content: '保存签名失败',
										showCancel: false
									})
									return
								}
								resolve(res.tempFilePath)
							},
							fail: (r) => {
								console.log('图片生成失败:' + r)
								resolve(false)
							}
						},
						this
					)
				})
			},
			// 保存横屏图片
			async saveHorizontalCanvas() {
				return await new Promise((resolve, reject) => {
					uni.canvasToTempFilePath({
							canvasId: 'signCanvas',
							fileType: this.expFile.fileType, // 只支持png和jpg
							success: (res) => {
								if (!res.tempFilePath) {
									uni.showModal({
										title: '提示',
										content: '保存签名失败',
										showCancel: false
									})
									return
								}

								// #ifdef APP
								uni.compressImage({
									src: res.tempFilePath,
									quality: this.expFile.quality * 100, // 范围 0 - 100
									rotate: 270,
									success: (r) => {
										console.log('==== compressImage :', r)
										resolve(r.tempFilePath)
									}
								})
								// #endif

								// #ifndef APP
								uni.getImageInfo({
									src: res.tempFilePath,
									success: (r) => {
										// console.log('==== getImageInfo :', r)
										// 将signCanvas的内容复制到hsignCanvas中
										const hcanvasCtx = uni.createCanvasContext(
											'hsignCanvas', this)
										// 横屏宽高互换
										hcanvasCtx.translate(this.canvasHeight / 2, this
											.canvasWidth / 2)
										hcanvasCtx.rotate(Math.PI * (-90 / 180))
										hcanvasCtx.drawImage(
											r.path,
											-this.canvasWidth / 2,
											-this.canvasHeight / 2,
											this.canvasWidth,
											this.canvasHeight
										)
										hcanvasCtx.draw(false, async () => {
											const hpathRes = await uni
												.canvasToTempFilePath({
														canvasId: 'hsignCanvas',
														fileType: this.expFile
															.fileType, // 只支持png和jpg
														quality: this.expFile
															.quality // 范围 0 - 1
													},
													this
												)
											let tempFile = ''
											if (Array.isArray(hpathRes)) {
												hpathRes.some((item) => {
													if (item) {
														tempFile = item
															.tempFilePath
														return
													}
												})
											} else {
												tempFile = hpathRes
													.tempFilePath
											}
											resolve(tempFile)
										})
									}
								})
								// #endif
							},
							fail: (err) => {
								console.log('图片生成失败:' + err)
								resolve(false)
							}
						},
						this
					)
				})
			}
		}
	}
</script>

<style scoped lang="scss">
	[v-cloak] {
		display: none !important;
	}

	.sign-page {
		height: 600rpx;
		width: 710rpx;
		padding: 20rpx;
		display: flex;
		flex-direction: column;

		.sign-body {
			margin-top: 50rpx;
			width: 100%;
			flex-grow: 1;
			background: #E5E5E5;

			.sign-canvas {
				width: 100%;
				height: 100%;
			}
		}

		.sign-footer {
			width: 100%;
			height: 80rpx;
			margin: 15rpx 0;
			display: flex;
			justify-content: space-evenly;
			align-items: center;

			.txt{
				color:#3894FF;
				padding-left:10rpx;
				font-size: 36rpx;
			}
		}

		.vertical-btns {
			.btn {
				width: 120rpx;
				height: 66rpx;
			}
		}

		.horizontal-btns {
			.btn {
				width: 66rpx;
				height: 120rpx;
				writing-mode: vertical-lr;
				transform: rotate(90deg);
			}
		}
	}

	:deep(.uvicon-close) {
		font-size: 48rpx
	}
</style>

signature.js

javascript 复制代码
function getLocalFilePath(path) {
    if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path.indexOf('_downloads') === 0) {
        return path
    }
    if (path.indexOf('file://') === 0) {
        return path
    }
    if (path.indexOf('/storage/emulated/0/') === 0) {
        return path
    }
    if (path.indexOf('/') === 0) {
        var localFilePath = plus.io.convertAbsoluteFileSystem(path)
        if (localFilePath !== path) {
            return localFilePath
        } else {
            path = path.substr(1)
        }
    }
    return '_www/' + path
}

function dataUrlToBase64(str) {
    var array = str.split(',')
    return array[array.length - 1]
}

var index = 0
function getNewFileId() {
    return Date.now() + String(index++)
}

function biggerThan(v1, v2) {
    var v1Array = v1.split('.')
    var v2Array = v2.split('.')
    var update = false
    for (var index = 0; index < v2Array.length; index++) {
        var diff = v1Array[index] - v2Array[index]
        if (diff !== 0) {
            update = diff > 0
            break
        }
    }
    return update
}

export function pathToBase64(path) {
    return new Promise(function(resolve, reject) {
        if (typeof window === 'object' && 'document' in window) {
            if (typeof FileReader === 'function') {
                var xhr = new XMLHttpRequest()
                xhr.open('GET', path, true)
                xhr.responseType = 'blob'
                xhr.onload = function() {
                    if (this.status === 200) {
                        let fileReader = new FileReader()
                        fileReader.onload = function(e) {
                            resolve(e.target.result)
                        }
                        fileReader.onerror = reject
                        fileReader.readAsDataURL(this.response)
                    }
                }
                xhr.onerror = reject
                xhr.send()
                return
            }
            var canvas = document.createElement('canvas')
            var c2x = canvas.getContext('2d')
            var img = new Image
            img.onload = function() {
                canvas.width = img.width
                canvas.height = img.height
                c2x.drawImage(img, 0, 0)
                resolve(canvas.toDataURL())
                canvas.height = canvas.width = 0
            }
            img.onerror = reject
            img.src = path
            return
        }
        if (typeof plus === 'object') {
            plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), function(entry) {
                entry.file(function(file) {
                    var fileReader = new plus.io.FileReader()
                    fileReader.onload = function(data) {
                        resolve(data.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'))
    })
}

export function base64ToPath(base64) {
    return new Promise(function(resolve, reject) {
        if (typeof window === 'object' && 'document' in window) {
            base64 = base64.split(',')
            var type = base64[0].match(/:(.*?);/)[1]
            var str = atob(base64[1])
            var n = str.length
            var array = new Uint8Array(n)
            while (n--) {
                array[n] = str.charCodeAt(n)
            }
            return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], { type: type })))
        }
        var extName = base64.split(',')[0].match(/data\:\S+\/(\S+);/)
        if (extName) {
            extName = extName[1]
        } else {
            reject(new Error('base64 error'))
        }
        var fileName = getNewFileId() + '.' + extName
        if (typeof plus === 'object') {
            var basePath = '_doc'
            var dirPath = 'uniapp_temp'
            var filePath = basePath + '/' + dirPath + '/' + fileName
            if (!biggerThan(plus.os.name === 'Android' ? '1.9.9.80627' : '1.9.9.80472', plus.runtime.innerVersion)) {
                plus.io.resolveLocalFileSystemURL(basePath, function(entry) {
                    entry.getDirectory(dirPath, {
                        create: true,
                        exclusive: false,
                    }, function(entry) {
                        entry.getFile(fileName, {
                            create: true,
                            exclusive: false,
                        }, function(entry) {
                            entry.createWriter(function(writer) {
                                writer.onwrite = function() {
                                    resolve(filePath)
                                }
                                writer.onerror = reject
                                writer.seek(0)
                                writer.writeAsBinary(dataUrlToBase64(base64))
                            }, reject)
                        }, reject)
                    }, reject)
                }, reject)
                return
            }
            var bitmap = new plus.nativeObj.Bitmap(fileName)
            bitmap.loadBase64Data(base64, function() {
                bitmap.save(filePath, {}, function() {
                    bitmap.clear()
                    resolve(filePath)
                }, function(error) {
                    bitmap.clear()
                    reject(error)
                })
            }, function(error) {
                bitmap.clear()
                reject(error)
            })
            return
        }
        if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
            var filePath = wx.env.USER_DATA_PATH + '/' + fileName
            wx.getFileSystemManager().writeFile({
                filePath: filePath,
                data: dataUrlToBase64(base64),
                encoding: 'base64',
                success: function() {
                    resolve(filePath)
                },
                fail: function(error) {
                    reject(error)
                }
            })
            return
        }
        reject(new Error('not support'))
    })
}
相关推荐
尚梦7 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
尚学教辅学习资料14 小时前
基于SSM+uniapp的营养食谱系统+LW参考示例
java·uni-app·ssm·菜谱
Bessie23414 小时前
微信小程序eval无法使用的替代方案
微信小程序·小程序·uni-app
qq22951165021 天前
小程序Android系统 校园二手物品交换平台APP
微信小程序·uni-app
qq22951165021 天前
微信小程序uniapp基于Android的流浪动物管理系统 70c3u
微信小程序·uni-app
qq22951165021 天前
微信小程序 uniapp+vue老年人身体监测系统 acyux
vue.js·微信小程序·uni-app
摇头的金丝猴2 天前
uniapp vue3 使用echarts-gl 绘画3d图表
前端·uni-app·echarts
小远yyds2 天前
跨平台使用高德地图服务
前端·javascript·vue.js·小程序·uni-app
qq22951165022 天前
uniapp+vue加油服务系统 微信小程序
vue.js·微信小程序·uni-app
重生之我是菜鸡程序员2 天前
uniapp 使用vue/pwa
javascript·vue.js·uni-app