在 Vue 3 中实现电子签名组件
在 Vue 3 中实现一个简单的电子签名组件,并解决一个常见问题:当签名组件放在弹窗内时,鼠标绘制会出现偏移的问题。
项目环境:
- Vue 3:前端框架
- Element Plus:UI 组件库
电子签名组件功能
- 画布绘制:用户可以在画布上使用鼠标进行签名。
- 画笔设置:支持调整画笔的颜色和大小。
- 提交签名:将画布内容提交,生成图片并回传给父组件。
- 清除签名:可以清除当前画布内容。
源代码实现
下面是完整的 Vue 3 电子签名组件代码:
1. template
模板部分
<template>
<!-- 电子签名组件 -->
<div>
<!-- 画布对象 -->
<div
ref="grapDiv"
style="width: 800px; height: 400px; border: 2px dotted #ddd; border-radius: 10px;"
v-show="!isCommit"
>
<canvas
ref="grapCvs"
id="container"
@mousedown="mousedown"
@mousemove="mousemove"
/>
</div>
<!-- 设置面板 -->
<div
ref="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
在页面中的位置,从而调整鼠标绘制的准确性。
const rect = canvas.value.getBoundingClientRect();
curMouseX.value = e.clientX - rect.left;
curMouseY.value = e.clientY - rect.top;
getBoundingClientRect()
方法返回元素的大小及其相对于视口的位置,因此可以精确计算出鼠
标相对于 canvas
的位置,确保绘制效果不会偏移。