今天学习一下如何手打一个电子签名组件。
效果图
思路
- 使用
canvas
创建一个画布,绘制路径 - 然后把
canvas
输出为一张 base64图片
实现
1. 创建一个 canvas
SignCanvas.vue
html
<script setup>
const size = reactive({
width: 0,
height: 0
})
const canvasBox = useTemplateRef('canvasBox')
const canvasEl = useTemplateRef('canvasEl')
onMounted(() => {
size.width = canvasBox.value.clientWidth
size.height = canvasBox.value.clientHeight
...
})
</script>
<template>
<div class="sign-canvas" ref="canvasBox">
<canvas ref="canvasEl" :width="size.width" :height="size.height"></canvas>
</div>
</template>
2. 定义一个画笔方法。
js
function brush(beginX, beginY, stopX, stopY) {
context.beginPath() // 开启一条新路径
context.lineWidth = 3 // 设置线宽
// context.globalAlpha = 1 // 设置图片的透明度
// context.strokeStyle = 'black' // 设置路径颜色
// context.linePressure = 1.2 // 设置笔触压力
// context.smoothness = 30 // 设置笔触大小变化的平滑度
context.moveTo(beginX, beginY) // 从(beginX, beginY)这个坐标点开始画图
context.lineTo(stopX, stopY) // 定义从(beginX, beginY)到(stopX, stopY)的线条(该方法不会创建线条)
context.closePath() // 结束该条路径 必须!!
context.stroke() // 实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径。默认颜色是黑色。
}
3. 监听画板画笔事件
js
function init() {
let beginX, beginY
// context = canvasEl.value.getContext('2d')
canvasEl.value.addEventListener('touchstart', function (event) {
event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
beginX = event.touches[0].clientX - this.offsetLeft
beginY = event.touches[0].pageY - this.offsetTop
})
canvasEl.value.addEventListener('touchmove', (event) => {
event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
event = event.touches[0]
let stopX = event.clientX - canvasEl.value.offsetLeft
let stopY = event.pageY - canvasEl.value.offsetTop
brush(beginX, beginY, stopX, stopY)
beginX = stopX // 这一步很关键,需要不断更新起点,否则画出来的是射线簇
beginY = stopY
})
}
4. 重置方法
js
function resetHandle() {
context.clearRect(0, 0, size.width, size.height) // 清除区域内所有痕迹
}
5. 非空校验
js
function isEmpty() {
const blank = document.createElement('canvas') // 获取一个空 canvas 对象
blank.width = size.width
blank.height = size.height
return canvasEl.value.toDataURL() == blank.toDataURL() // 比较值相等则为空
}
- 文字旋转(记录)
js
function rotateText() {
context.save() // 关键!!! 保持原始状态
context.translate(size.width / 2, size.height / 2) // 设置旋转中心点为正中心
context.rotate(Math.PI / 2) // 逆时针旋转 90°
context.font = 'bold 48px MiSans'
context.textAlign = 'center'
context.textBaseline = 'middle'
context.fillStyle = 'rgba(0, 0, 0, 0.1)'
context.fillText('请在此区域签字', 0, 0)
context.restore() // 恢复到原始状态
}
- base64 转 file对象(记录)
js
function base64ToFile(base64String, fileName) {
const arr = base64String.split(',') // 解析data URL
const mime = arr[0].match(/:(.*?);/)[1]
const bstr = 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: mime })
}
源码
html
<script setup>
import { base64ToFile } from '@/utils/jsUtils'
import { service } from '@/request/request'
const router = useRouter()
const size = reactive({
width: 0,
height: 0
})
const canvasBox = useTemplateRef('canvasBox')
const canvasEl = useTemplateRef('canvasEl')
onMounted(() => {
size.width = canvasBox.value.clientWidth
size.height = canvasBox.value.clientHeight
init()
})
onBeforeUnmount(() => {
canvasEl.value.removeEventListener('touchstart', touchstart)
canvasEl.value.removeEventListener('touchmove', touchmove)
})
let context = null
let brushAttr = { beginX: 0, beginY: 0 }
// 监听事件
function init() {
context = canvasEl.value.getContext('2d')
canvasEl.value.addEventListener('touchstart', touchstart)
canvasEl.value.addEventListener('touchmove', touchmove)
}
function touchstart(event) {
event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
brushAttr.beginX = event.touches[0].clientX - this.offsetLeft
brushAttr.beginY = event.touches[0].pageY - this.offsetTop
}
function touchmove(event) {
event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
event = event.touches[0]
const stopX = event.clientX - canvasEl.value.offsetLeft
const stopY = event.pageY - canvasEl.value.offsetTop
brush(brushAttr.beginX, brushAttr.beginY, stopX, stopY)
brushAttr.beginX = stopX // 这一步很关键,需要不断更新起点,否则画出来的是射线簇
brushAttr.beginY = stopY
}
// 笔刷
function brush(beginX, beginY, stopX, stopY) {
context.beginPath() // 开启一条新路径
context.lineWidth = 3 // 设置线宽
// context.globalAlpha = 1 // 设置图片的透明度
// context.strokeStyle = 'black' // 设置路径颜色
// context.linePressure = 1.2 // 设置笔触压力
// context.smoothness = 30 // 设置笔触大小变化的平滑度
context.moveTo(beginX, beginY) // 从(beginX, beginY)这个坐标点开始画图
context.lineTo(stopX, stopY) // 定义从(beginX, beginY)到(stopX, stopY)的线条(该方法不会创建线条)
context.closePath() // 创建该条路径
context.stroke() // 实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径。默认颜色是黑色。
}
// 旋转90°后导出图片
function printImage() {
if (isEmpty()) return showToast('请先完成签字确认')
const rotatedCanvas = document.createElement('canvas')
// 设置旋转后的画布尺寸(宽高互换)
rotatedCanvas.width = size.height
rotatedCanvas.height = size.width
const ctx = rotatedCanvas.getContext('2d')
ctx.fillStyle = '#FFF' // 设置画布的填充颜色
ctx.fillRect(0, 0, canvasEl.value.height, canvasEl.value.width) // 在画布上绘制一个填充矩形
// 旋转并绘制内容
ctx.translate(rotatedCanvas.width / 2, rotatedCanvas.height / 2) // 设置旋转中心点为正中心
ctx.rotate(-Math.PI / 2) // 逆时针旋转 90°
ctx.drawImage(canvasEl.value, -size.width / 2, -size.height / 2, size.width, size.height)
const base64 = rotatedCanvas.toDataURL('image/png')
console.log('base64', base64)
return base64
}
function isEmpty() {
const blank = document.createElement('canvas') // 系统获取一个空canvas对象
blank.width = size.width
blank.height = size.height
return canvasEl.value.toDataURL() == blank.toDataURL() // 比较值相等则为空
}
// 重置
function resetHandle() {
context.clearRect(0, 0, size.width, size.height)
}
</script>
<template>
<div class="container">
<van-nav-bar
safe-area-inset-top
fixed
placeholder
title="签字确认"
left-arrow
@click-left="router.back"
/>
<div class="content">
<div class="sign-canvas tip" ref="canvasBox">
<!-- <canvas ref="canvasEl"></canvas> -->
<canvas ref="canvasEl" :width="size.width" :height="size.height"></canvas>
</div>
<div class="btns">
<div class="btn plain_blue" @click="resetHandle">重签</div>
<div class="btn primary_blue" @click="printImage">确认</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.container {
height: calc(100vh - 48px);
background-color: #f9f9f9;
.content {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
.sign-canvas {
flex: 1;
background-color: #fff;
&.tip::after {
position: absolute;
top: 46%;
content: '请在此区域签字';
color: rgba($color: #000000, $alpha: 0.1);
font-size: 48px;
font-weight: 600;
transform: rotate(90deg);
pointer-events: none;
}
}
.btns {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-top: 16px;
.btn {
display: flex;
align-items: center;
justify-content: center;
height: 48px;
border-radius: 6px;
}
.primary_blue {
color: #fff;
background-color: #165dff;
}
.plain_blue {
color: #165dff;
background-color: #e3eafa;
}
}
}
}
</style>