uniapp vue3 canvas实现手写签名

发布于:2025-08-30 ⋅ 阅读:(27) ⋅ 点赞:(0)

在这里插入图片描述

userSign.vue

<template>
	<view class="signature">
		<view class="btn-box" v-if="orientation === 'abeam'">
			<button @click="clearClick">重签</button>
			<button @click="finish">完成签名</button>
		</view>

		<canvas id="canvas" canvas-id="canvas" :disable-scroll="true" @touchmove="move" @touchstart="start" @error="error"
			@touchend="touchend" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }">
		</canvas>

		<view class="btn-box" v-if="orientation === 'portrait'">
			<button type="warn" @click="clearClick">重签</button>
			<button type="primary" @click="finish">完成签名</button>
		</view>
	</view>
</template>

<script setup>
	import {
		ref,
		onMounted,
		getCurrentInstance
	} from 'vue'

	const props = defineProps({
		// 分享
		orientation: {
			type: String,
			default: "portrait", // 竖向
		},

		width: {
			type: Number,
			default: 0,
		},

		height: {
			type: Number,
			default: 0,
		},

		// 字体粗细
		lineWidth: {
			type: Number,
			default: 3,
		},

		// 字体颜色
		strokeStyle: {
			type: String,
			default: "black",
		},
	});

	const emit = defineEmits(["finish", "clear"]);

	const instance = getCurrentInstance().proxy;

	const ctx = ref("");
	const pr = ref(0);
	const canvasWidth = ref("");
	const canvasHeight = ref("");

	const points = ref([]);

	onMounted(() => {
		getSystemInfo();
		createCanvas();
	});

	// 触摸开始
	const start = (e) => {
		points.value.push({
			X: e.touches[0].x,
			Y: e.touches[0].y
		});
		ctx.value.beginPath();
	};

	// 开始移动
	const move = (e) => {
		points.value.push({
			X: e.touches[0].x,
			Y: e.touches[0].y
		}); //存点
		draw(); //绘制路径
	};

	const touchend = () => {
		points.value = [];
	};

	const draw = () => {
		const point1 = points.value[0];
		const point2 = points.value[1];
		points.value.shift();
		ctx.value.moveTo(point1.X, point1.Y);
		ctx.value.lineTo(point2.X, point2.Y);
		ctx.value.stroke();
		ctx.value.draw(true);
	};

	const createCanvas = () => {
		ctx.value = uni.createCanvasContext("canvas", instance, {
			willReadFrequently: true,
		});
		ctx.value.lineGap = "round";
		ctx.value.lineJoin = "round";
		ctx.value.lineWidth = props.lineWidth; // 字体粗细
		ctx.value.strokeStyle = props.strokeStyle; // 字体颜色
	};

	const canvasW = ref(300);
	const canvasH = ref(300);
	// 获取系统信息
	const getSystemInfo = () => {
		uni.getSystemInfo({
			success: (res) => {
				pr.value = res.pixelRatio;
				if (props.orientation == "portrait") {
					if (props.width > res.windowWidth || props.width == 0) {
						canvasWidth.value = res.windowWidth;
					} else {
						canvasWidth.value = props.width;
					}
					if (props.height > res.windowHeight - 70 || props.height == 0) {
						canvasHeight.value = res.windowHeight - 70;
					} else {
						canvasHeight.value = props.height;
					}
				} else if (props.orientation == "abeam") {
					if (props.width > res.windowWidth - 70 || props.width == 0) {
						canvasWidth.value = res.windowWidth - 70;
					} else {
						canvasWidth.value = props.width;
					}
					if (props.height > res.windowHeight || props.height == 0) {
						canvasHeight.value = res.windowHeight;
					} else {
						canvasHeight.value = props.height;
					}
				}

				// 我写死的
				canvasHeight.value = 300;

				const rate = canvasHeight.value / canvasWidth.value;
				canvasW.value = 300;
				canvasH.value = 300 / rate;
			},
		});
	};

	// canvas 的error
	const error = (e) => {
		console.log("画出错了" + e);
	};

	// 重签
	const clearClick = () => {
		ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value);
		ctx.value.draw(true);
		emit("clear");
	};

	// 点击完成签名
	const finish = () => {
		uni.canvasToTempFilePath({
			canvasId: "canvas",
			success: (res) => {
				const path = res.tempFilePath;
				emit("finish", path);
			},
		});
	};
	// 如果想要base64格式
	const finish = () => {
		uni.canvasToTempFilePath({
			canvasId: "canvas",
			x: 0,
			y: 0,
			width: canvasWidth.value,
			height: canvasHeight.value,
			destWidth: canvasWidth.value * pr.value, // 乘以像素比保证高清
			destHeight: canvasHeight.value * pr.value,
			fileType: 'png',
			quality: 1, // 最高质量
			success: (res) => {
				// 通过文件系统读取 base64
				const base64 = uni.getFileSystemManager().readFileSync(res.tempFilePath, 'base64')
				console.log('base64=', `data:image/png;base64,${base64}`);
				emit('finish', `data:image/png;base64,${base64}`)
			},
			fail: (err) => {
				console.error('生成签名失败:', err)
			}
		}, instance);
	};

	defineExpose({
		clearClick,
	});
</script>

<style scoped lang="scss">
	canvas {
		background-color: white;
	}

	.signature {
		width: 100%;
		height: 100%;
		display: flex;
		flex-wrap: wrap;
		align-items: flex-end;
		// background-color: #e7e5e7 !important;
	}

	.btn-box {
		width: 100%;
		display: flex;
		text-align: center;
		padding: 20rpx 0;
		border-top: 1px solid #bbb;
	}
</style>

使用它

	.popup-title {
		text-align: center;
		font-weight: 500;
		font-weight: bold;
		padding: 40rpx 0;
		border-bottom: 1px solid #bbb;
	}

	<u-popup ref="popupRef" mode="center" title="考试签名" background-color="#fff">
		<view class="popup-title">
			<text>考试签名</text>
		</view>
		<view>
			<userSign></userSign>
		</view>
	</u-popup>


	const popupRef = ref()
	// 签名弹出层
	const togglePopup = () => {
		console.log('悬浮球 - 弹框出答题卡');
		popupRef.value.open('center')
	}