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();});// 初始化签名canvasconst 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) {// 清除canvassignatureCtx.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>