uniapp微信小程序电子签名

news2025/1/10 10:36:34

先上效果图,不满意可以直接关闭这页签


新建成单独的组件,然后具体功能引入,具体功能点击签名按钮,把当前功能页面用样式隐藏掉,v-show和v-if也行,然后再把这个组件显示出来。


【签名-撤销】原理是之前绘画时把全部轨迹路径都记录下来,然后点击撤销时,清空画布,路径数组去掉最后一次绘画动作,然后再把剩余路径又全部画上去,这么干后会路径会出现锯齿,我也莫得办法了,将就着用了。


【签名-完成】点击完成会判断路径数组是否有值,如果没有则说明没有签名,有则把画布保存成图片,然后再把这个图片以指定尺寸画入另一个画布,避免保存下来的图片分辨率过大,导致文件太大。画上另一个画布之后,这个画布再保存成图片,然后图片再转成base64格式返回给主页面,要是不想转base64可以把具体代码去掉。生成的图片是垂直,应该以逆时针旋转90度保存的,奈何前端实力不过关,怎么都处理不好这个逆时针旋转90度,只能在上传到后端后,用后端旋转了(丢人).........如果有人能处理好这个,麻烦评论留下代码


主页面引入组件并注册,然后用v-show控制是否显示,主页面样式自己调整好,让电子签名可以覆盖整个页面。


[具体组件代码]

<template>
    <view class="panel" :style="{top: `${top}px`}">
        <canvas canvas-id="signCanvas" class="sign-canvas" @touchstart="handleTouchStart"
            @touchmove="handleTouchMove" @touchend="handleTouchEnd"></canvas>
        <!-- 另一个画布,用来缩小签名图片尺寸的,加个样式让他飞到外太空去 -->
        <canvas canvas-id="signCanvasReduce" class="sign-canvas-reduce"></canvas>
        <view class="panel-bottom">
            <view class="panel-bottom-btn btn-gray" @click="onCancel">取消</view>
            <view class="panel-bottom-btn btn-gray" @click="onUndo">撤销</view>
            <view class="panel-bottom-btn btn-gray" @click="onClearRect(true)">重写</view>
            <view class="panel-bottom-btn" @click="onSaveSign">完成</view>
        </view>
    </view>
</template>

<script>
	export default {
		data() {
			return {
                // 距离顶部高度
                top: void (0),
				isDrawing: false,
				startX: 0,
				startY: 0,
				strokes: [],
				canvasWidth: 0,
				canvasHeight: 0
			}
		},

		mounted() {
            // 获取手机状态栏和导航栏高度,和手机屏幕可用宽高度
            const _this = this
            uni.getSystemInfo({
                success: function(res) {
                    _this.canvasWidth = res.windowWidth
                    _this.canvasHeight = res.windowHeight
                    const custom = uni.getMenuButtonBoundingClientRect()
                    // 导航栏胶囊高度 + (胶囊距离顶部高度 - 状态栏高度) * 2 + 状态栏高度 + 20内边距
					_this.top = custom.height + (custom.top - res.statusBarHeight) * 2 + res.statusBarHeight + 4
                }
            })
            // 创建画布
			const ctx = uni.createCanvasContext('signCanvas', this)
			ctx.setStrokeStyle('#000')
			ctx.setLineWidth(4)
			ctx.setLineCap('round')
			ctx.setLineJoin('round')
			ctx.draw()
			this.canvasContext = ctx
		},

		methods: {

			handleTouchStart(e) {
				// 阻止默认滚动行为
				e.preventDefault()
				const touch = e.touches[0]
				this.isDrawing = true
				this.startX = touch.x
				this.startY = touch.y
				this.strokes.push({
                    type: 'start',
					x: touch.x,
					y: touch.y
				})
			},

			handleTouchMove(e) {
				e.preventDefault() // 阻止默认滚动行为
				if (!this.isDrawing) {
					return
				}
				const touch = e.touches[0]
				this.canvasContext.moveTo(this.startX, this.startY)
				this.canvasContext.lineTo(touch.x, touch.y)
				this.canvasContext.stroke()
				this.canvasContext.draw(true)
				this.startX = touch.x
				this.startY = touch.y
				this.strokes.push({
                    type: 'move',
					x: touch.x,
					y: touch.y
				})
			},

			handleTouchEnd(e) {
				e.preventDefault() // 阻止默认滚动行为
				this.isDrawing = false
			},

            // 撤销
            onUndo () {
                // 先清空当前画布
                this.onClearRect(false)
                if (this.strokes.length) {
                    // 去掉最后一次绘画的路径
                    while (this.strokes.pop().type !== 'start'){}
                    // 剩余路径全部绘制到画布上
                    for (let i = 0; i < this.strokes.length; i++) {
                        const item = this.strokes[i]
                        if(item.type === 'start') {
                            // 绘制起始点
                            this.canvasContext.beginPath()
                            this.canvasContext.moveTo(item.x, item.y)
                        } else if(item.type === 'move') {
                            // 绘制线条
                            this.canvasContext.lineTo(item.x, item.y)
                            this.canvasContext.stroke()
                        }
                    }
                    this.canvasContext.draw(true)
                }
            },

            // 清空
            onClearRect (clearLine) {
                this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
                this.canvasContext.draw(true)
                clearLine && (this.strokes = [])
            },

            // 取消
            onCancel () {
                this.onClearRect(true)
                this.$emit('cancel')
            },

            // 保存签名 signFlag 是否已签名
            onSaveSign() {
                if (!this.strokes.length) {
                    // 未签名
                    this.$emit('ok', { signFlag: false })
                    return
                }
                // 签名保存为图片
				uni.canvasToTempFilePath({
					canvasId: 'signCanvas',
                    quality: 0.1,
					success: (res) => {
                        const tempPath = res.tempFilePath
                        // 然后写入另一个画布
                        const signCanvas = uni.createCanvasContext('signCanvasReduce', this)
                        signCanvas.translate(0, 0) // 修改原点坐标(这里是已左上角为坐标原点)
                        signCanvas.drawImage(tempPath, 0, 0, 80, 160)
                        signCanvas.draw(false, () => {
                            setTimeout(() => {
                                // 另一个画布再保存为图片,目的就是缩小图片的尺寸
                                uni.canvasToTempFilePath({
                                    canvasId: 'signCanvasReduce',
                                    quality: 0.2,
                                    success: (res) => {
                                        // 清空画布
                                        this.onClearRect(true)
                                        // 转成base64
                                        this.imagePathToBase64(res.tempFilePath).then(base64 => {
                                            this.$emit('ok', { base64, signFlag: true })
                                        })
                                    },
                                    fail: () => {
                                        // toast('生成签名图片失败')
                                    }
                                }, this)
                            }, 200)
                        })
					},
                    fail: (res) => {
                        // toast('生成签名失败')
                    }
				}, this)
			},

            // 根据上传后的图片转成Base64格式
            imagePathToBase64(url) {
                if (!url) {
                    return url
                }
                return new Promise((resolve, reject) => {
                    uni.getFileSystemManager().readFile({
                        filePath: url,
                        encoding: 'base64',
                        success: fileRes => {
                            const base64 = 'data:image/png;base64,' + fileRes.data
                            resolve(base64)
                        },
                        fail: err => {
                            // toast('生成签名失败')
                            resolve()
                        }
                    })
                })
            }

		}
	}
</script>

<style lang="scss" scoped>
    .panel {
        width: 100%;
        position: absolute;
        left: 0;
        bottom: 0;
        right: 0;
        top: 0;
        overflow-y: hidden;
        background-color: #FFF;
    }
    .sign-canvas {
        width: 100%;
        height: 85%;
    }
    .sign-canvas-reduce {
        width: 80px;
        height: 160px;
        position: absolute;
        top: -10000rpx;
    }
    .panel-bottom {
        height: 15%;
        display: flex;
        justify-content: center;
        padding-top: 50rpx;
    }
    .panel-bottom-btn {
        transform: rotate(90deg);
        height: 40rpx;
        padding: 14rpx 36rpx;
        font-size: 30rpx;
        border-radius: 20rpx;
        color: #FFF;
        background: linear-gradient(90deg, rgba(250, 197, 22, 1), rgba(255, 141, 26, 1));
    }
    .btn-gray {
        background: #d4d4d4;
    }
</style>
<style>
    page {
		overflow-y: hidden;
	}
</style>

继续加班了.....

码字不易,于你有利,勿忘点赞 

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全球首款搭载Google Gemini和GPT-4o的智能眼镜发布

智能眼镜仍然是一个尚未完全成熟的未来概念&#xff0c;但生成式人工智能的到来显著提升了这些设备的能力。Meta 的 Ray-Ban 智能眼镜被许多人视为当今最好的选择之一&#xff0c;而现在 Solos AirGo Vision 正在为其带来竞争&#xff0c;这款眼镜还集成了 Google Gemini 支持。…

burpsuite 设置监听窗口 火狐利用插件快速切换代理状态

一、修改burpsuite监听端口 1、首先打开burpsuite&#xff0c;点击Proxy下的Options选项&#xff1a; 2、可以看到默认的监听端口为8080&#xff0c;首先选中我们想要修改的监听&#xff0c;点击Edit进行编辑 3、将端口改为9876&#xff0c;并保存 4、可以看到监听端口修改成功…

JUC基础学习

1.Java JUC简介 2.volatile关键字-内存可见性 3.原子变量-CAS算法 4.ConcurrentHashMap锁分段机制

【Unity Linux】模型导致的Unity项目崩溃

模型需勾选Strip Bones。如不勾选&#xff0c;则开启项目崩溃。 也可以删除有问题模型的.meta文件。 &#xff08;Unity默认会自动勾选&#xff0c;所以不会崩溃&#xff09; 或打开.meta文件&#xff0c;将optimizeBones的值&#xff0c;由0改为1。&#xff08;对应面板上的…

Top 5 免费 PDF 转 Word 转换工具

PDF 是可移植文档格式的缩写&#xff0c;是一种文件格式&#xff0c;用于独立于软件、硬件或操作系统可靠地呈现和交换文档。PDF 不是为编辑而设计的&#xff0c;因此如果您想更改某些内容&#xff0c;可能需要将 PDF 转换为 Word/Doc 转换器。 Top 5 免费 PDF 转 Word 转换工具…

【Python实战因果推断】12_线性回归的不合理效果2

目录 Adjusting with Regression Adjusting with Regression 为了了解回归的威力&#xff0c;让我带您回到最初的例子&#xff1a;估计信贷额度对违约的影响。银行数据通常是这样的&#xff0c;其中有很多列客户特征&#xff0c;这些特征可能表明客户的信贷价值&#xff0c;比…

1972计算机毕业设计asp.net毕业生就业信息管理系统VS开发access数据库web结构c#编程计算机网页源码项目

一、源码特点 asp.net毕业生就业信息管理系统 是一套完善的web设计管理系统&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 asp.net毕业生就业信息管理系统 二、功能介绍 基于ASP.NET的毕业生就业信息管理系统主要满足以下几个方面的…

SQL 注入联合查询之为什么要 and 1=2

在 SQL 注入联合查询中&#xff0c;将 id 先置为假&#xff08;如 id-1 或其他使查询结果为空的条件&#xff09;&#xff0c;通常是为了让前面的查询语句查询不到结果&#xff0c;从而使联合查询中后面的语句结果能够显示在回显位上

【数据结构】栈和队列-相互实现OJ题

前言&#xff1a; 本题目是关于栈和队列的OJ题目&#xff0c;需对栈和队列有一定了解再进行做题&#xff0c;若不了解可以根据我之前这篇文章进行学习&#xff1a;【数据结构】栈和队列-CSDN博客,题中需要的栈和队列的实现也在该文章中有源代码 目录 前言&#xff1a; 一.用…

转运机器人帮助物流行业实现无人化运输,自动分拣

在物流行业日新月异的今天&#xff0c;智能化、无人化已成为大势所趋。富唯智能转运机器人凭借其卓越的性能和广泛的应用场景&#xff0c;正引领着物流行业的新一轮变革。 1、高效转运&#xff0c;轻松应对 富唯智能转运机器人&#xff0c;拥有高达1000kg的负载能力&#xff…

django学习入门系列之第三点《BootSrap初了解》

文章目录 初识BootStrap往期回顾 初识BootStrap BootSrap是什么&#xff1f; 是别人帮我们已写好的CSS样式&#xff0c;我们如果想要使用这个BootSrap&#xff1a; 下载BootStrap使用 在页面上引入BootStrap编写HTML时&#xff0c;按照BootStrap的规定来编写 自定制 官网&…

GoogleAI大动作:发布Gemma 2,开启新篇章

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

window上部署sql server改动端口、和sqlserver的一些还原、批量插入存储过程的命令

1.端口的查看和启动 --windows上安装上sql server数据库后&#xff0c;搜索界面搜索sql&#xff0c;会出现配置管理器&#xff0c;点击进入 --进入后再次选择配置管理器 2. sqlserver数据库还原图形化 sqlserver还原数据库时会使数据库进入一个restore的还原状态&#xff0c;…

20240701在飞凌的OK3588-C开发板的Android12系统下使用i2cdetect确认I2C总线

console:/ # i2cdetect -y -r 0 console:/ # i2cdetect -l console:/ # i2cdetect -F 0 20240701在飞凌的OK3588-C开发板的Android12系统下使用i2cdetect确认I2C总线 2024/7/1 11:30 在CAM1、CAM2挂载OV13850。 在CAM3、CAM4和CAM5挂载OV5645了。 console:/ # i2 i2cdetect i2…

超声波气象站的科技创新

在现代科技的快速发展下&#xff0c;气象监测设备也在不断革新&#xff0c;其中超声波气象站以其独特的优势&#xff0c;成为了气象监测领域的新宠。这款体积小巧、重量轻盈的气象站&#xff0c;凭借其高精度、高稳定性和长寿命&#xff0c;为气象数据的采集提供了强有力的支持…

JMeter--定时执行的方法

原文网址&#xff1a;JMeter--定时执行的方法_IT利刃出鞘的博客-CSDN博客 简介 本文介绍JMeter如何使用定时器定时执行测试任务。 Java技术星球&#xff1a;way2j.com 方法 第一步&#xff1a;新建定时器 右键测试任务> Add > Timer> Constant Timer 如下图所示…

Android平台崩溃和 ANR 问题进行符号化解析、解析崩溃日志的内存地址

使用Android Logcat Stacktrace Utility | Android Logcat | 1.2.3 1.设置so库路径 2.打开Stacktrace Utility工具 3.在Original粘贴报错内存地址 4.点击Resolve Stacktraces,就会解析出内存地址 如果是红色,解析失败了,缺少原生so库,可以在第一步添加so库文件再次尝试…

2-22 基于matlab的NSGA-2求解多目标柔性车间调度算法

基于matlab的NSGA-2求解多目标柔性车间调度算法,计算最大完工时间、计算总延期时长、计算调度方案的总能耗、计算设备总负荷。输出四项结果&#xff0c;多次运行可寻找最佳的调度计划。程序已调通&#xff0c;可直接运行。 2-22 NSGA-2求解多目标柔性车间调度算法 - 小红书 (xi…

python sklearn机械学习模型-分类

&#x1f308;所属专栏&#xff1a;【机械学习】✨作者主页&#xff1a; Mr.Zwq✔️个人简介&#xff1a;一个正在努力学技术的Python领域创作者&#xff0c;擅长爬虫&#xff0c;逆向&#xff0c;全栈方向&#xff0c;专注基础和实战分享&#xff0c;欢迎咨询&#xff01; 您…

常用的限流算法有哪些?你听说过几种?

限流&#xff0c;就是指限制流量请求的频次。 在高并发情况下&#xff0c;它是一种保护系统的策略&#xff0c;避免了在流量高峰时系统崩溃&#xff0c;造成系统的不可用。 常见的限流算法有&#xff1a; 计数器限流算法滑动窗口限流算法漏桶限流算法令牌桶限流算法 1. 计数器…