手写签名功能(vue3)

手写签名功能(vue3)

效果

显示效果

在这里插入图片描述

签名版效果

在这里插入图片描述

代码

  1. 代码引入
    写成子组件形式,直接引入即可
<signature-features />
  1. 代码结构
    在这里插入图片描述
    signatureFeatures:签名的显示效果
    vueEsign:画板
    xnSignName:打开签名后的modal框
  2. 文件代码
    signatureFeatures:
<template>
	<div>
		<div class="mb-2" v-if="userInfo.signature">
			<a-image :src="userInfo.signature" style="height: 120px; border: 1px solid rgb(236 236 236)" />
		</div>
		<a-button @click="XnSignNameRef.show()">打开签名板</a-button>
		<xn-sign-name ref="XnSignNameRef" :image="userInfo.signature" @successful="signSuccess" />
	</div>
</template>

<script setup>
	import { ref } from 'vue'
	import tool from '@/utils/tool'
	import userCenterApi from '@/api/sys/userCenterApi'
	import store from '@/store'
	import XnSignName from '@/views/sys/user/userTab/signatureFeatures/xnSignName.vue'
	const userInfo = ref(tool.data.get('USER_INFO'))
	const XnSignNameRef = ref()
	// 签名板组件回调
	const signSuccess = (value) => {
		const param = {
			signature: value
		}
		userCenterApi.userUpdateSignature(param).then(() => {
			userInfo.value.signature = value
			// 更新缓存
			tool.data.set('USER_INFO', userInfo.value)
			store.commit('SET_userInfo', userInfo.value)
		})
	}
</script>

<style scoped></style>

xnSignName:

<template>
	<a-modal ref="signModel" v-model:visible="visible" :width="600" title="电子签名" @cancel="handleClear" @ok="handleOk">
		<a-row :gutter="5">
			<a-col :span="15">
				<div style="border: 1px solid rgb(236 236 236)">
					<vue-esign
						ref="esign"
						v-model:bgColor="bgColor"
						:width="800"
						:height="400"
						:is-crop="isCrop"
						:line-width="lineWidth"
						:line-color="lineColor"
					/>
				</div>
			</a-col>
			<a-col :span="9">
				<div style="height: 90px; width: auto">
					<img :src="resultImg" style="height: 90px; width: 100%; border: 1px solid rgb(236 236 236)" />
				</div>
			</a-col>
		</a-row>
		<div style="margin-top: 10px">
			<a-space>
				<a-form>
					<a-row :gutter="16">
						<a-col :span="12">
							<a-form-item label="画笔粗细:">
								<a-input-number v-model:value="lineWidth" :min="1" :max="20" />
							</a-form-item>
						</a-col>
						<a-col :span="12">
							<a-form-item>
								<div style="padding-right: 50px">是否裁剪:<a-checkbox v-model:checked="isCrop"></a-checkbox></div>
							</a-form-item>
						</a-col>
					</a-row>
				</a-form>
				<a-button type="primary" @click="handleGenerate">预览</a-button>
				<a-button @click="handleReset">清屏</a-button>
			</a-space>
		</div>
	</a-modal>
</template>

<script setup>
	import { message } from 'ant-design-vue'
	import VueEsign from '@/views/sys/user/userTab/signatureFeatures/vueEsign.vue'
	const signModel = ref(false)
	const visible = ref(false)
	const esign = ref(false)
	const resultImg = ref('')
	const isCrop = ref(false)
	const lineWidth = ref(6)
	const lineColor = ref('#000000')
	const bgColor = ref('')
	const props = defineProps(['image'])
	// eslint-disable-next-line vue/no-setup-props-destructure
	resultImg.value = props.image
	const emit = defineEmits({ successful: null })
	const show = () => {
		visible.value = true
	}
	const handleReset = () => {
		esign.value.reset()
		resultImg.value = ''
	}
	const handleGenerate = () => {
		esign.value
			.generate()
			.then((res) => {
				resultImg.value = res
			})
			.catch(() => {
				message.warning('无任何签字')
			})
	}
	const handleClear = () => {
		visible.value = false
	}
	const handleOk = () => {
		esign.value
			.generate()
			.then((res) => {
				emit('successful', res)
				handleClear()
			})
			.catch(() => {
				message.warning('无任何签字')
			})
	}
	defineExpose({
		show
	})
</script>

<style scoped>
	.ant-form-item {
		margin-bottom: 0px !important;
	}
</style>

vueEsign:

<!--
本插件来源于:https://github.com/JaimeCheng/vue-esign#readme
因为集成进来跟我的Vue版本不一致,打包出问题,所以集成源码方式,感谢作者的源码
-->
<template>
	<canvas
		ref="canvas"
		@mousedown="mouseDown"
		@mousemove="mouseMove"
		@mouseup="mouseUp"
		@touchstart="touchStart"
		@touchmove="touchMove"
		@touchend="touchEnd"
	></canvas>
</template>

<script>
	export default {
		props: {
			width: {
				type: Number,
				default: 800
			},
			height: {
				type: Number,
				default: 300
			},
			lineWidth: {
				type: Number,
				default: 4
			},
			lineColor: {
				type: String,
				default: '#000000'
			},
			bgColor: {
				type: String,
				default: ''
			},
			isCrop: {
				type: Boolean,
				default: false
			},
			isClearBgColor: {
				type: Boolean,
				default: true
			},
			format: {
				type: String,
				default: 'image/png'
			},
			quality: {
				type: Number,
				default: 1
			}
		},
		data() {
			return {
				hasDrew: false,
				resultImg: '',
				points: [],
				canvasTxt: null,
				startX: 0,
				startY: 0,
				isDrawing: false,
				sratio: 1
			}
		},
		computed: {
			ratio() {
				return this.height / this.width
			},
			stageInfo() {
				return this.$refs.canvas.getBoundingClientRect()
			},
			myBg() {
				return this.bgColor ? this.bgColor : 'rgba(255, 255, 255, 0)'
			}
		},
		watch: {
			myBg: function (newVal) {
				this.$refs.canvas.style.background = newVal
			}
		},
		beforeMount() {
			window.addEventListener('resize', this.$_resizeHandler)
		},
		// eslint-disable-next-line vue/no-deprecated-destroyed-lifecycle
		beforeDestroy() {
			window.removeEventListener('resize', this.$_resizeHandler)
		},
		mounted() {
			const canvas = this.$refs.canvas
			canvas.height = this.height
			canvas.width = this.width
			canvas.style.background = this.myBg
			this.$_resizeHandler()
			// 在画板以外松开鼠标后冻结画笔
			document.onmouseup = () => {
				this.isDrawing = false
			}
		},
		methods: {
			$_resizeHandler() {
				const canvas = this.$refs.canvas
				canvas.style.width = this.width + 'px'
				const realw = parseFloat(window.getComputedStyle(canvas).width)
				canvas.style.height = this.ratio * realw + 'px'
				this.canvasTxt = canvas.getContext('2d')
				this.canvasTxt.scale(Number(this.sratio), Number(this.sratio))
				this.sratio = realw / this.width
				this.canvasTxt.scale(1 / this.sratio, 1 / this.sratio)
			},
			// pc
			mouseDown(e) {
				e = e || event
				e.preventDefault()
				this.isDrawing = true
				this.hasDrew = true
				let obj = {
					x: e.offsetX,
					y: e.offsetY
				}
				this.drawStart(obj)
			},
			mouseMove(e) {
				e = e || event
				e.preventDefault()
				if (this.isDrawing) {
					let obj = {
						x: e.offsetX,
						y: e.offsetY
					}
					this.drawMove(obj)
				}
			},
			mouseUp(e) {
				e = e || event
				e.preventDefault()
				let obj = {
					x: e.offsetX,
					y: e.offsetY
				}
				this.drawEnd(obj)
				this.isDrawing = false
			},
			// mobile
			touchStart(e) {
				e = e || event
				e.preventDefault()
				this.hasDrew = true
				if (e.touches.length === 1) {
					let obj = {
						x: e.targetTouches[0].clientX - this.$refs.canvas.getBoundingClientRect().left,
						y: e.targetTouches[0].clientY - this.$refs.canvas.getBoundingClientRect().top
					}
					this.drawStart(obj)
				}
			},
			touchMove(e) {
				e = e || event
				e.preventDefault()
				if (e.touches.length === 1) {
					let obj = {
						x: e.targetTouches[0].clientX - this.$refs.canvas.getBoundingClientRect().left,
						y: e.targetTouches[0].clientY - this.$refs.canvas.getBoundingClientRect().top
					}
					this.drawMove(obj)
				}
			},
			touchEnd(e) {
				e = e || event
				e.preventDefault()
				if (e.touches.length === 1) {
					let obj = {
						x: e.targetTouches[0].clientX - this.$refs.canvas.getBoundingClientRect().left,
						y: e.targetTouches[0].clientY - this.$refs.canvas.getBoundingClientRect().top
					}
					this.drawEnd(obj)
				}
			},
			// 绘制
			drawStart(obj) {
				this.startX = obj.x
				this.startY = obj.y
				this.canvasTxt.beginPath()
				this.canvasTxt.moveTo(this.startX, this.startY)
				this.canvasTxt.lineTo(obj.x, obj.y)
				this.canvasTxt.lineCap = 'round'
				this.canvasTxt.lineJoin = 'round'
				this.canvasTxt.lineWidth = this.lineWidth * this.sratio
				this.canvasTxt.stroke()
				this.canvasTxt.closePath()
				this.points.push(obj)
			},
			drawMove(obj) {
				this.canvasTxt.beginPath()
				this.canvasTxt.moveTo(this.startX, this.startY)
				this.canvasTxt.lineTo(obj.x, obj.y)
				this.canvasTxt.strokeStyle = this.lineColor
				this.canvasTxt.lineWidth = this.lineWidth * this.sratio
				this.canvasTxt.lineCap = 'round'
				this.canvasTxt.lineJoin = 'round'
				this.canvasTxt.stroke()
				this.canvasTxt.closePath()
				this.startY = obj.y
				this.startX = obj.x
				this.points.push(obj)
			},
			drawEnd(obj) {
				this.canvasTxt.beginPath()
				this.canvasTxt.moveTo(this.startX, this.startY)
				this.canvasTxt.lineTo(obj.x, obj.y)
				this.canvasTxt.lineCap = 'round'
				this.canvasTxt.lineJoin = 'round'
				this.canvasTxt.stroke()
				this.canvasTxt.closePath()
				this.points.push(obj)
				this.points.push({ x: -1, y: -1 })
			},
			// 操作
			generate(options) {
				let imgFormat = options && options.format ? options.format : this.format
				let imgQuality = options && options.quality ? options.quality : this.quality
				const pm = new Promise((resolve, reject) => {
					if (!this.hasDrew) {
						reject(`Warning: Not Signned!`)
						return
					}
					var resImgData = this.canvasTxt.getImageData(0, 0, this.$refs.canvas.width, this.$refs.canvas.height)
					this.canvasTxt.globalCompositeOperation = 'destination-over'
					this.canvasTxt.fillStyle = this.myBg
					this.canvasTxt.fillRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height)
					this.resultImg = this.$refs.canvas.toDataURL(imgFormat, imgQuality)
					var resultImg = this.resultImg
					this.canvasTxt.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height)
					this.canvasTxt.putImageData(resImgData, 0, 0)
					this.canvasTxt.globalCompositeOperation = 'source-over'
					if (this.isCrop) {
						const crop_area = this.getCropArea(resImgData.data)
						var crop_canvas = document.createElement('canvas')
						const crop_ctx = crop_canvas.getContext('2d')
						crop_canvas.width = crop_area[2] - crop_area[0]
						crop_canvas.height = crop_area[3] - crop_area[1]
						const crop_imgData = this.canvasTxt.getImageData(...crop_area)
						crop_ctx.globalCompositeOperation = 'destination-over'
						crop_ctx.putImageData(crop_imgData, 0, 0)
						crop_ctx.fillStyle = this.myBg
						crop_ctx.fillRect(0, 0, crop_canvas.width, crop_canvas.height)
						resultImg = crop_canvas.toDataURL(imgFormat, imgQuality)
						crop_canvas = null
					}
					resolve(resultImg)
				})
				return pm
			},
			reset() {
				this.canvasTxt.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height)
				if (this.isClearBgColor) {
					this.$emit('update:bgColor', '')
					this.$refs.canvas.style.background = 'rgba(255, 255, 255, 0)'
				}
				this.points = []
				this.hasDrew = false
				this.resultImg = ''
			},
			getCropArea(imgData) {
				var topX = this.$refs.canvas.width
				var btmX = 0
				var topY = this.$refs.canvas.height
				var btnY = 0
				for (var i = 0; i < this.$refs.canvas.width; i++) {
					for (var j = 0; j < this.$refs.canvas.height; j++) {
						var pos = (i + this.$refs.canvas.width * j) * 4
						if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
							btnY = Math.max(j, btnY)
							btmX = Math.max(i, btmX)
							topY = Math.min(j, topY)
							topX = Math.min(i, topX)
						}
					}
				}
				topX++
				btmX++
				topY++
				btnY++
				const data = [topX, topY, btmX, btnY]
				return data
			}
		}
	}
</script>

<style scoped>
	canvas {
		max-width: 100%;
		display: block;
	}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/552058.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu修改DNS

【永久修改DNS】 临时修改DNS的方法是在 /etc/resolv.conf 添加&#xff1a;nameserver 8.8.8.8 nameserver 8.8.8.8 注意到/etc/resolv.conf最上面有这么一行&#xff1a; DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN 说明重启之后这个文件会被自动…

关于系统数据缓存的思考以及设计

文章目录 引言案例A项目B项目 分析我的实现总结 引言 缓存&#xff0c;这是一个经久不衰的话题&#xff0c;它通过“空间换时间”的战术不仅能够极大提升处理查询性能还能很好的保护底层资源。最近针对系统数据缓存的优化后&#xff0c;由于这是一个通用的场景并且有了一点心得…

力扣练习题(2024/4/15)

1打家劫舍 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警。 给定一个代表每个房屋…

java实现论文查重,文本查重方案 采用 ansj 分词法

需求要求实现一个文本查重&#xff0c;重复率超过70% 就不让用户新增文本。固研究实现基于java的文本查重工具&#xff0c;分享出来方便大家使用&#xff5e; ansj 分词法介绍 Ansj 是一个开源的 Java 中文分词工具&#xff0c;基于中科院的 ictclas 中文分词算法&#xff0c…

THS6.0.1.0开启健康检查(by lqw)

可以在节点管理器或者分组管理的编辑配置里添加以下信息&#xff1a; 之后点监控,点击实时指标&#xff0c;点击HTTP集群统计&#xff1a; 下图是配置并生效的效果&#xff1a; 也可以使用頁面配置&#xff1a; 推荐使用tcp形式&#xff0c;有的应用后端可能不支持http…

代理知识科普:为什么有的代理IP速度比较慢呢?

代理IP在跨境业务中被广泛的应用&#xff0c;今天我们将一同深入探讨一个问题&#xff1a;“为什么有的IP代理速度比较慢&#xff1f;”随着数字化时代的不断发展&#xff0c;代理服务成为了许多网络操作的关键环节。然而&#xff0c;有时我们可能会遇到IP代理速度慢的问题&…

MT3026 砍玉米

样例1&#xff1a; 输入&#xff1a; 6 1 3 4 2 5 1 7 8 19 10 30 2 输出&#xff1a; 6 其中1<n<10^5,1<xi,hi<10^9 思路&#xff1a;贪心&#xff1a;从左到右或者从右到左依次判断每一棵玉米是否可以倒下 &#xff08;以从左到右为例&#xff1a;先往左倒&…

[论文笔记]Root Mean Square Layer Normalization

引言 今天带来论文Root Mean Square Layer Normalization的笔记&#xff0c;论文题目是均方根层归一化。 本篇工作提出了RMSNorm&#xff0c;认为可以省略重新居中步骤。 简介 层归一化对Transformer等模型非常重要&#xff0c;它可以帮助稳定训练并提升模型收敛性&#xf…

生成对抗网络GAN的扩展应用理解

注&#xff1a;本文仅个人学习理解&#xff0c;如需详细内容请看原论文&#xff01; 一、cycleGAN 1.整体架构&#xff1a; 将图片A通过生成器生成图片B&#xff0c;然后通过判别器判别图片B的真假&#xff1b;并且图片B通过另一个生成器生成A‘&#xff0c;让A和A’近似。 2…

Python这十大特征,堪称“圆满”!

当你犹豫是否要开始 Python 学习之前&#xff0c;可以先详尽了解一下这门编程语言。 软件开发者 Guido Van Rossum 于 1991 年创建了 Python&#xff0c;旨在使程序员的工作更加简单。Python 是目前全球比较流行且产业急需的程序设计语言&#xff0c;也是一门跨平台、开源、免…

2024-8.python作用域+函数其他

变量的作用域 讲到了函数就必须介绍变量的作用域相关。 作用域指的是变量的有效范围。变量并不是在哪个位置都可以访问的&#xff0c;访问权限取决于这个变量是在哪里赋值的&#xff0c;也就是在哪个作用域内赋的值。变量在哪个作用域内赋值&#xff0c;则表示该变量的作用域就…

POST 为什么会发送两次请求?

本文目录 同源策略 什么是同源策略 CORS 简单请求 预检请求 附带身份凭证的请求与通配符 完整的请求流程图 总结 前言 最近博主在字节面试中遇到这样一个面试题&#xff0c;这个问题也是前端面试的高频问题&#xff0c;因为在前端开发的日常开发中我们总是会与post请求…

【数据结构】-- 栈和队列

&#x1f308; 个人主页&#xff1a;白子寰 &#x1f525; 分类专栏&#xff1a;python从入门到精通&#xff0c;魔法指针&#xff0c;进阶C&#xff0c;C语言&#xff0c;C语言题集&#xff0c;C语言实现游戏&#x1f448; 希望得到您的订阅和支持~ &#x1f4a1; 坚持创作博文…

Mybatis-plus中的分页操作

Mybatis-plus中的分页操作 1.导入Mybatis-plus依赖2.创建mybatis配置类3.参数 1.导入Mybatis-plus依赖 因为是一个springboot项目&#xff0c;其中的pom.xml文件内容如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns&q…

ping命令的使用

一、实验环境 同实验案例分析ARP解析过程环境。 二、需求描述 熟悉 ping 命令的用法并熱悉 ping 命令的各种参数 三、推荐步骤 分别 ping 一个存在的和不存在的IP地址&#xff0c;观察返回的信息分别测试 ping 命令的相关参数。 四、实验步骤 1.ping 一个存在的和不存在…

数据加密、文档加密为什么都选择安企神软件

数据加密、文档加密为什么都选择安企神软件 免费试用安企神 在数据加密和文件加密领域&#xff0c;有众多优秀的软件&#xff0c;他们功能各异、价格不同、效果也大相径庭&#xff0c;经过对比使用、用户口碑和技术网站评判&#xff0c;安企神在各方面都稳坐第一把交易。其原…

ECA-Net:深度卷积神经网络中的高效通道注意力机制【原理讲解及代码!!!】

ECA-Net&#xff1a;深度卷积神经网络中的高效通道注意力机制 在深度学习领域&#xff0c;特别是在深度卷积神经网络&#xff08;DCNN&#xff09;中&#xff0c;注意力机制已经成为提升模型性能的关键技术之一。其中&#xff0c;ECA模块&#xff08;Efficient Channel Attent…

Nginx常用配置,开箱即用

经常遇到Nginx安装和配置的问题。这里笔者将常用配置统统写在下面&#xff0c;方便咱们日常使用。这里本着开箱即用的原则&#xff0c;所以大多数时候不会解释为什么要这样去配置&#xff0c;也不涉及Nginx的安装步骤。下面的所有配置&#xff0c;都可以直接复制后粘贴使用&…

虚幻引擎源码版安装下载,点击GenerateProjectFiles.bat报错 error NU1101NuGet包问题解决参考方案

开发环境配置与源码安装使用 安装VS2022 按照官方文档安装需要的vs配置 虚幻引擎源代码下载 Epic里面下载的引擎与源代码引擎区别&#xff1a;Epic里面下载的引擎是已经编译过的它的源代码访问权限不完整&#xff0c;源代码版本提供比较完整引擎代码&#xff0c;并且可以修…

顺序表(增删减改)+通讯录项目(数据结构)+顺序表专用题型

什么是顺序表 顺序表和数组的区别 顺序表本质就是数组 结构体初阶进阶 系统化的学习-CSDN博客 简单解释一下&#xff0c;就像大家去吃饭&#xff0c;然后左边是苍蝇馆子&#xff0c;右边是修饰过的苍蝇馆子&#xff0c;但是那个好看的苍蝇馆子一看&#xff0c;这不行啊&a…