uniapp canvas实现手写签字功能(包括重签,撤回等按钮)

实现效果图:

实现代码,复制即用:

<template>

<view class="grouped-list">

<view class="sign-content">

<view class="signature-container">

<view class="canvas-wrapper">

<canvas id="signatureCanvas" canvas-id="signatureCanvas" :disable-scroll="true"

@touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"

class="signature-canvas">

</canvas>

<view v-if="!hasSignatureText" class="placeholder-text">请在该区域签字</view>

</view>

<view class="btn-arr">

<button class="common-btn" @click="clearSignature">重签</button>

<button class="common-btn" @click="undoSignature">撤回</button>

<button class="button-c common-btn" @click="completeSignature">

完成

</button>

</view>

</view>

</view>

</view>

</template>

相关js代码:

复制代码
<script setup>
	import {
		onReady
	} from "@dcloudio/uni-app";
	import {
		ref,
		getCurrentInstance
	} from "vue";
	// import {
	// 	uploadToOss
	// } from "@/utils/functions/oss.js";

	// 签名相关变量
	const instance = getCurrentInstance().proxy;
	const signatureCtx = ref(null);
	//是否有实际签字
	const hasSignature = ref(false);
	//是否显示签字文本
	const hasSignatureText = ref(false);
	const isDrawing = ref(false);
	const lastPoint = ref({
		x: 0,
		y: 0
	});
	const signaturePaths = ref([]);
	const currentPath = ref([]);
	onReady(() => {
		initSignatureCanvas();
	});
	// 初始化签名canvas
	const initSignatureCanvas = () => {
		// 获取canvas实际尺寸
		const query = uni.createSelectorQuery().in(instance);
		query
			.select(".signature-canvas")
			.boundingClientRect((data) => {
				if (data) {
					// 初始化canvas上下文
					signatureCtx.value = uni.createCanvasContext(
						"signatureCanvas",
						instance
					);
					// 设置白色背景
					signatureCtx.value.setFillStyle("#ffffff");
					signatureCtx.value.fillRect(0, 0, data.width, data.height);
					// 设置线条样式 - 确保是实线
					signatureCtx.value.setLineWidth(4); // 线条宽度
					signatureCtx.value.setStrokeStyle("#000000"); // 黑色线条
					signatureCtx.value.setLineCap("round"); // 圆形线帽
					signatureCtx.value.setLineJoin("round"); // 圆形连接
					if (signatureCtx.value.setGlobalCompositeOperation) {
						signatureCtx.value.setGlobalCompositeOperation("source-over");
					}
					// signatureCtx.value.setGlobalCompositeOperation('source-over'); // 正常绘制模式
					// 渲染初始背景
					signatureCtx.value.draw(true);
				}
			})
			.exec();
	};

	// 触摸开始
	const onTouchStart = (e) => {
		isDrawing.value = true;
		hasSignatureText.value = true;
		const touch = e.touches[0];
		lastPoint.value = {
			x: touch.x,
			y: touch.y
		};
		currentPath.value = [{
			x: touch.x,
			y: touch.y
		}];
	};

	// 触摸移动
	const onTouchMove = (e) => {
		if (!isDrawing.value) return;

		const touch = e.touches[0];
		const currentPoint = {
			x: touch.x,
			y: touch.y
		};

		// 每次绘制前重新设置线条样式,确保是实线
		signatureCtx.value.setLineWidth(4);
		signatureCtx.value.setStrokeStyle("#000000");
		signatureCtx.value.setLineCap("round");
		signatureCtx.value.setLineJoin("round");

		// 绘制从上一点到当前点的线段
		signatureCtx.value.beginPath();
		signatureCtx.value.moveTo(lastPoint.value.x, lastPoint.value.y);
		signatureCtx.value.lineTo(currentPoint.x, currentPoint.y);
		signatureCtx.value.stroke();
		signatureCtx.value.draw(true);
		// 添加这一段,判断用户是否实际移动了手指
		if (!hasSignature.value) {
			const distance = Math.sqrt(
				Math.pow(currentPoint.x - lastPoint.value.x, 2) +
				Math.pow(currentPoint.y - lastPoint.value.y, 2)
			);
			// 如果移动距离大于阈值,认为用户确实在签名
			if (distance > 2) {
				hasSignature.value = true;
			}
		}
		currentPath.value.push(currentPoint);
		lastPoint.value = currentPoint;
	};

	// 触摸结束
	const onTouchEnd = () => {
		if (isDrawing.value) {
			signaturePaths.value.push([...currentPath.value]);
			currentPath.value = [];
		}
		isDrawing.value = false;
	};

	// 清除签名
	const clearSignature = () => {
		// 获取canvas实际尺寸进行清除
		const query = uni.createSelectorQuery().in(instance);
		query
			.select(".signature-canvas")
			.boundingClientRect((data) => {
				if (data) {
					// 清除canvas内容
					signatureCtx.value.clearRect(0, 0, data.width, data.height);

					// 重新设置白色背景
					signatureCtx.value.setFillStyle("#ffffff");
					signatureCtx.value.fillRect(0, 0, data.width, data.height);

					// 重新设置线条样式
					signatureCtx.value.setLineWidth(4);
					signatureCtx.value.setStrokeStyle("#000000");
					signatureCtx.value.setLineCap("round");
					signatureCtx.value.setLineJoin("round");

					signatureCtx.value.draw(true);
					hasSignatureText.value = false
				}
			})
			.exec();

		hasSignature.value = false;
		signaturePaths.value = [];
		currentPath.value = [];
	};

	// 撤回上一笔
	const undoSignature = () => {
		if (signaturePaths.value.length > 0) {
			signaturePaths.value.pop();
			// 先检查是否还有签名路径
			if (signaturePaths.value.length === 0) {
				hasSignature.value = false;
				hasSignatureText.value = false;
			}
			redrawSignature();
		}
	};
	// 重绘签名
	const redrawSignature = () => {
		const query = uni.createSelectorQuery().in(instance);
		query
			.select(".signature-canvas")
			.boundingClientRect((data) => {
				if (data) {
					// 清除canvas
					signatureCtx.value.clearRect(0, 0, data.width, data.height);
					// 设置白色背景
					signatureCtx.value.setFillStyle("#ffffff");
					signatureCtx.value.fillRect(0, 0, data.width, data.height);

					if (signaturePaths.value.length === 0) {
						hasSignature.value = false;
						hasSignatureText.value = false; // 没有签名时,隐藏签名文本
						signatureCtx.value.draw(true);
						return;
					}
					// 重新设置绘制样式,确保重绘时也是实线
					signatureCtx.value.setLineWidth(4);
					signatureCtx.value.setStrokeStyle("#000000");
					signatureCtx.value.setLineCap("round");
					signatureCtx.value.setLineJoin("round");
					signaturePaths.value.forEach((path) => {
						if (path.length > 0) {
							signatureCtx.value.beginPath();
							signatureCtx.value.moveTo(path[0].x, path[0].y);

							// 绘制连续的实线路径
							for (let i = 1; i < path.length; i++) {
								signatureCtx.value.lineTo(path[i].x, path[i].y);
							}


							signatureCtx.value.stroke();
						}
					});

					signatureCtx.value.draw(true);
				}
			})
			.exec();
	};

	// 完成签名并上传
	const completeSignature = async () => {
		if (!hasSignature.value) {
			uni.showToast({
				title: "请签字",
				icon: "none",
			});
			return;
		}
		try {
			// 将canvas转为临时文件
			const res = await new Promise((resolve, reject) => {
				uni.canvasToTempFilePath({
					canvasId: "signatureCanvas",
					success: resolve,
					fail: reject,
				});
			});
			console.log('签名文件===', res);
			// // 上传到OSS
			// const ossPath = "signature/examPromise/";
			// const uploadResult = await uploadToOss(ossPath, res.tempFilePath);
			// const signature_url = uploadResult.url.split(/\.com\//)[1];
			// evaluationApi
			// 	.signDataSubmit({
			// 		operation_examiner_id: examiner_id.value,
			// 		signature_url: signature_url,
			// 	})
			// 	.then((res) => {
			// 		if (res.code == 0) {
			// 			uni.hideLoading();
			// 			if (!examInfoData.value.exam_stu_pra_id && is_main.value == 1) { //没有考试信息 并且是主考评员
			// 				uni.redirectTo({ // 跳转学生选择页面
			// 					url: '/pages/evaluation/studentSelect/index'
			// 				})
			// 			} else {
			// 				const {
			// 					stu_pra_examiner_id,
			// 					exam_stu_pra_id,
			// 					is_ready,
			// 					is_direct
			// 				} = examInfoData.value
			// 				evaluationStore.setStu_pra_examiner_id(stu_pra_examiner_id) //有考场信息 保存
			// 				uni.redirectTo({ // 跳转赛前准备页面
			// 					url: '/pages/evaluation/readiness/index?pra_id=' + exam_stu_pra_id +
			// 						'&isReady=' + is_ready + '&isMain=' + is_direct
			// 				})
			// 			}
			// 		}
			// 	})
			// 	.catch((err) => {
			// 		uni.$star.showToast("签名保存失败,请重试!");
			// 		uni.hideLoading();
			// 	});
		} catch (error) {
			console.error("签名保存失败:", error);
			uni.hideLoading();
			uni.showToast({
				title: "签名保存失败",
				icon: "error",
			});
		}
	};
</script>

自定义样式:

<style lang="scss" scoped>

.grouped-list {

height: 100vh;

overflow: hidden;

padding-top: 200rpx;

.sign-content {

width: 100%;

padding: 30rpx;

box-sizing: border-box;

}

.signature-container {

background-color: #fff;

border-radius: 14rpx;

width: 100%;

}

.canvas-wrapper {

position: relative;

width: 100%;

height: 400rpx;

border: 2rpx dashed #d3dcfd;

border-radius: 14rpx;

margin-bottom: 30rpx;

background-color: #fafafa;

overflow: hidden;

}

.signature-canvas {

width: 100%;

height: 100%;

border-radius: 14rpx;

background-color: #f8f8f8;

}

.btn-arr {

display: flex;

align-items: center;

justify-content: center;

}

.common-btn {

width: 220rpx;

height: 60rpx;

line-height: 60rpx;

font-weight: normal;

}

.placeholder-text {

position: absolute;

top: 50%;

left: 50%;

transform: translate(-50%, -50%);

color: #869de9;

font-size: 28rpx;

pointer-events: none;

}

}

</style>

相关推荐
雪芽蓝域zzs4 小时前
uniapp 修改android包名
android·uni-app
爱折腾的小码农4 小时前
uni-app 小程序开发避坑:诡异的 `module ‘...‘ is not defined` 错误与我的解决方案
uni-app
Q_Q19632884754 小时前
python+uniapp基于微信小程序的助眠小程序
spring boot·python·小程序·django·flask·uni-app·node.js
不知名的前端专家5 小时前
UniApp USB存储设备U盘操作、读写原生插件
uni-app
coldriversnow6 小时前
uni-app从后端返回的富文本中的视频截取一帧为封面
uni-app
2501_915918417 小时前
iOS 混淆与 IPA 加固一页式行动手册(多工具组合实战 源码成品运维闭环)
android·运维·ios·小程序·uni-app·iphone·webview
Q_Q51100828521 小时前
python+uniapp基于微信小程序团购系统
spring boot·python·微信小程序·django·uni-app·node.js·php
炒毛豆21 小时前
uniapp微信小程序+vue3基础内容介绍~(含标签、组件生命周期、页面生命周期、条件编译(一码多用)、分包))
vue.js·微信小程序·uni-app
盛夏绽放1 天前
关于 uni-app 与原生微信小程序中的生命周期 —— 一次“生命旅程”的解读
微信小程序·小程序·uni-app