在 Vue 3 中实现一个简单的电子签名组件,并解决一个常见问题:当签名组件放在弹窗内时,鼠标绘制会出现偏移的问题。
项目环境:
- Vue 3:前端框架
- Element Plus:UI 组件库
电子签名组件功能
- 画布绘制:用户可以在画布上使用鼠标进行签名。
- 画笔设置:支持调整画笔的颜色和大小。
- 提交签名:将画布内容提交,生成图片并回传给父组件。
- 清除签名:可以清除当前画布内容。
源代码实现
下面是完整的 Vue 3 电子签名组件代码:
1. template
模板部分
<template><!-- 电子签名组件 --><div><!-- 画布对象 --><divref="grapDiv"style="width: 800px; height: 400px; border: 2px dotted #ddd; border-radius: 10px;"v-show="!isCommit"><canvasref="grapCvs"id="container"@mousedown="mousedown"@mousemove="mousemove"/></div><!-- 设置面板 --><divref="setControlDiv"style="width: 280px; height: 200px; background-color: #474747; border: 1px solid #ddd; border-radius: 10px; margin-top: -203px; margin-left: 1px; position: absolute;"v-show="ifSetController"><div style="width: 100%; height: 30%; margin-top: 10px"><span style="width: 100%; height: 30%"><label style="float: left; color: white; margin-left: 14px; margin-top: 15px;">字体大小</label><label style="float: right; color: white; margin-right: 14px; margin-top: 15px;">{{ fontSize }}</label></span><el-slider v-model="fontSize" style="width: 89%; margin-top: 20px; margin-left: 17px" :min="1" :max="10" /></div><div style="width: 100%; height: 45%; margin-top: 15px; padding: 7px"><li@click="setImgColor"v-for="(item, index) in fontColorArray":key="index":style="'list-style: none;width: 38px;height: 38px;float: left;background: ' + item"></li></div></div><!-- 提交的画布对象 --><div style="width: 800px; height: 400px; border: 2px solid green; border-radius: 10px;" v-show="isCommit"><img :src="content" /></div><!-- 操作按钮 --><div style="width: 800px; padding-top: 10px"><el-button @click="set()" v-show="!isCommit">画笔设置</el-button><el-button @click="clear()" v-show="!isCommit">清除</el-button><el-button type="primary" @click="commit()" v-show="!isCommit">提交</el-button><el-button @click="goback()" v-show="isCommit">返回</el-button></div></div>
</template>
2. script
部分
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
const emit = defineEmits(["commitDatas"]);// 定义变量
const canvas = ref(null);
const graphics = ref(null);
const isDrawing = ref(false);
const curMouseX = ref(null);
const curMouseY = ref(null);
const isCommit = ref(false);
const content = ref(null);
const ifGraph = ref(false);
const ifSetController = ref(false);
const fontSize = ref(1);
const fontColorArray = ref(["#F59999", "#E86262", "#AA4446", "#6B4849", "#34231E", "#435772", "#2DA4A8", "#EFDCD3", "#FEAA3A", "#FD6041", "#CF2257", "#404040", "#92BEE2", "#2286D8"]);
const fontColor = ref("#000000");// 选择画笔颜色
const setImgColor = (curIndex) => {let liArray = curIndex.currentTarget.parentElement.children;for (let child of liArray) {child.className = "";}curIndex.currentTarget.className = "activeteLi";fontColor.value = curIndex.currentTarget.style.background;
};// 鼠标按下事件处理
const mousedown = (e) => {const rect = canvas.value.getBoundingClientRect();isDrawing.value = true;curMouseX.value = e.clientX - rect.left;curMouseY.value = e.clientY - rect.top;graphics.value.beginPath();graphics.value.moveTo(curMouseX.value, curMouseY.value);
};// 鼠标移动事件处理
const mousemove = (e) => {if (isDrawing.value) {const rect = canvas.value.getBoundingClientRect();graphics.value.strokeStyle = fontColor.value;graphics.value.lineWidth = fontSize.value;curMouseX.value = e.clientX - rect.left;curMouseY.value = e.clientY - rect.top;graphics.value.lineTo(curMouseX.value, curMouseY.value);graphics.value.stroke();ifGraph.value = true;}
};// 清除画布
const clear = () => {ifGraph.value = false;canvas.value.width = canvas.value.width;
};// 将 DataURL 转换为文件
const dataURLtoFile = (dataurl, filename) => {const arr = dataurl.split(",");const bstr = window.atob(arr[1]);let n = bstr.length;const u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}return new File([u8arr], filename, { type: "image/png" });
};// 提交签名
const commit = () => {if (!ifGraph.value) {ElMessage({ message: "没有可提交的内容!", type: "error", duration: 2000 });return;}ElMessageBox({title: "操作提示",message: "确定提交?",confirmButtonText: "确定",cancelButtonText: "取消",showCancelButton: true,closeOnClickModal: false,type: "warning",}).then(() => {isCommit.value = true;content.value = canvas.value.toDataURL();emit("commitDatas", dataURLtoFile(content.value, "签名"));});
};// 返回编辑模式
const goback = () => {ifGraph.value = false;canvas.value.width = canvas.value.width;isCommit.value = false;
};// 初始化画布
onMounted(() => {const cvs = document.getElementById("container");cvs.width = 800;cvs.height = 400;graphics.value = cvs.getContext("2d");canvas.value = cvs;document.addEventListener("mouseup", () => {isDrawing.value = false;graphics.value.closePath();});
});
</script>
3. 样式部分
<style>
.activeteLi {box-shadow: 0 0 3px rgb(0 0 0 / 95%);transform: scale(1.2);
}
</style>
解决鼠标和画笔偏移问题
当我们将签名组件放到 el-dialog
弹窗中时,出现鼠标点击位置与实际绘制位置不符的偏移问题。这是由于 canvas
相对于窗口的位置发生了变化。为了解决这个问题,我们使用了 getBoundingClientRect()
来动态计算 canvas
在页面中的位置,从而调整鼠标绘制的准确性。
javascript">const rect = canvas.value.getBoundingClientRect();
curMouseX.value = e.clientX - rect.left;
curMouseY.value = e.clientY - rect.top;
getBoundingClientRect()
方法返回元素的大小及其相对于视口的位置,因此可以精确计算出鼠
标相对于 canvas
的位置,确保绘制效果不会偏移。