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>

相关推荐
2501_9160088914 小时前
iOS 跨平台开发实战指南,从框架选择到开心上架(Appuploader)跨系统免 Mac 发布全流程解析
android·macos·ios·小程序·uni-app·iphone·webview
QuantumLeap丶1 天前
《uni-app跨平台开发完全指南》- 06 - 页面路由与导航
前端·vue.js·uni-app
用户9714171814271 天前
uniapp页面路由
vue.js·uni-app
Kingsaj1 天前
uni-app打包app -- 在用户首次启动 App 时,强制弹出一个“用户协议与隐私政策”的确认对话框。
服务器·ubuntu·uni-app
Vue10241 天前
uniapp 使用renderjs 封装 video-player 视频播放器, html5视频播放器-解决视频层级、覆盖、播放卡顿
uni-app·音视频·html5
钱端工程师2 天前
uniapp封装uni.request请求,实现重复接口请求中断上次请求(防抖)
前端·javascript·uni-app
茶憶2 天前
uni-app app移动端实现纵向滑块功能,并伴随自动播放
javascript·vue.js·uni-app·html·scss
dcloud_jibinbin2 天前
【uniapp】解决小程序分包下的json文件编译后生成到主包的问题
前端·性能优化·微信小程序·uni-app·vue·json
茶憶2 天前
uniapp移动端实现触摸滑动功能:上下滑动展开收起内容,左右滑动删除列表
前端·javascript·vue.js·uni-app
蒲公英源码2 天前
uniapp开源ERP多仓库管理系统
mysql·elementui·uni-app·php