实现效果图:


实现代码,复制即用:
<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>