引言
在移动端应用中,手写签名功能常见于电子合同、审批流程等场景。本文将通过 Vue3 Composition API 结合 HTML5 Canvas 技术,实现一个高性能的移动端手写签名组件,支持触摸绘制、图片生成、OSS 上传等核心功能。
技术实现要点
1. 核心实现逻辑
(1) Canvas 初始化
vue
<template>
<div ref="canvasBox" class="canvas-box">
<canvas
ref="board"
@touchstart.prevent="handleTouchStart"
@touchmove.prevent="handleTouchMove"
@touchend.prevent="handleTouchEnd"
/>
<van-icon class="fullscreen-icon" @click="emit('fullscreen')" name="expand" />
</div>
</template>
(2) 响应式状态管理(Composition API)
javascript
import { ref, reactive, onMounted } from 'vue'
import AliOSS from 'ali-oss'
const props = defineProps({
id: { type: [Number, String], default: '' },
parentName: { type: String, default: '' }
})
const emit = defineEmits(['stopScroll', 'fullscreen'])
// 响应式状态
const canvasBox = ref(null)
const board = ref(null)
const ctx = ref(null)
const isDrawing = ref(false)
const showHintText = ref(true)
const coordinates = reactive({ x: 0, y: 0 })
(3) 画布初始化(生命周期处理)
javascript
onMounted(() => {
initCanvas()
initOSS()
})
const initCanvas = () => {
const { offsetWidth: width, offsetHeight: height } = canvasBox.value
board.value.width = width
board.value.height = height
ctx.value = board.value.getContext('2d')
ctx.value.strokeStyle = '#000'
ctx.value.lineWidth = 3
drawHintText()
}
const drawHintText = () => {
ctx.value.font = '30px Arial'
ctx.value.fillStyle = '#ccc'
ctx.value.textAlign = 'center'
ctx.value.fillText('签 名 区', board.value.width/2, board.value.height/2)
}
2. 核心交互实现
(1) 触摸事件处理
javascript
const getCanvasPosition = (clientX, clientY) => {
const rect = canvasBox.value.getBoundingClientRect()
return {
x: clientX - rect.left,
y: clientY - rect.top
}
}
const handleTouchStart = (e) => {
emit('stopScroll', true)
if(showHintText.value) clearCanvas()
const { x, y } = getCanvasPosition(
e.touches[0].clientX,
e.touches[0].clientY
)
coordinates.x = x
coordinates.y = y
ctx.value.beginPath()
isDrawing.value = true
}
const handleTouchMove = (e) => {
if(!isDrawing.value) return
const { x, y } = getCanvasPosition(
e.touches[0].clientX,
e.touches[0].clientY
)
ctx.value.moveTo(coordinates.x, coordinates.y)
ctx.value.lineTo(x, y)
ctx.value.stroke()
coordinates.x = x
coordinates.y = y
}
const handleTouchEnd = () => {
emit('stopScroll', false)
isDrawing.value && ctx.value.closePath()
isDrawing.value = false
}
(2) 画布操作方法
javascript
const clearCanvas = () => {
ctx.value.clearRect(0, 0, board.value.width, board.value.height)
showHintText.value = false
}
const convertToImage = async () => {
if(showHintText.value || isCanvasEmpty()) {
showToast('请先完成签名')
return null
}
return new Promise(resolve => {
board.value.toBlob(blob => {
const file = new File([blob], 'signature.png', {
type: 'image/png',
lastModified: Date.now()
})
resolve(file)
}, 'image/png')
})
}
3. 进阶功能实现
(1) 画布空白检测
javascript
const isCanvasEmpty = () => {
const blankCanvas = document.createElement('canvas')
blankCanvas.width = board.value.width
blankCanvas.height = board.value.height
return board.value.toDataURL() === blankCanvas.toDataURL()
}
(2) OSS 上传处理
javascript
let ossInstance = null
const initOSS = () => {
// 此处得换成自己的Ali-oss配置
ossInstance = new AliOSS({
region: 'oss-cn-shanghai',
stsURL: `${import.meta.env.VITE_API_COMMON}/sts/security`,
bucket: 'test01'
})
}
const uploadSignature = async (file) => {
try {
const { url } = await ossInstance.put(file.name,file)
await submitSignResult(url)
} catch(error) {
console.error('上传失败:', error)
}
}
4.完整组件代码
vue
<script setup name="CanvasSign">
import { ref, reactive, onMounted } from 'vue'
import { Toast } from 'vant'
import AliOSS from 'ali-oss'
const props = defineProps({
id: { type: [Number, String], default: '' },
parentName: { type: String, default: '' }
})
const emit = defineEmits(['stopScroll', 'fullscreen'])
// Canvas 相关引用
const canvasBox = ref(null)
const board = ref(null)
const ctx = ref(null)
// 响应式状态
const isDrawing = ref(false)
const showHintText = ref(true)
const coordinates = reactive({ x: 0, y: 0 })
let ossInstance = null
// 初始化画布
onMounted(() => {
initCanvas()
initOSS()
})
const initCanvas = () => {
const { offsetWidth, offsetHeight } = canvasBox.value
board.value.width = offsetWidth
board.value.height = offsetHeight
ctx.value = board.value.getContext('2d')
ctx.value.strokeStyle = '#000'
ctx.value.lineWidth = 3
ctx.value.lineCap = 'round'
drawHintText()
}
const drawHintText = () => {
ctx.value.font = '30px Arial'
ctx.value.fillStyle = '#ccc'
ctx.value.textAlign = 'center'
ctx.value.textBaseline = 'middle'
ctx.value.fillText(
'签 名 区',
board.value.width / 2,
board.value.height / 2
)
}
// 触摸事件处理
const getCanvasPosition = (clientX, clientY) => {
const rect = canvasBox.value.getBoundingClientRect()
return {
x: clientX - rect.left,
y: clientY - rect.top
}
}
const handleTouchStart = (e) => {
emit('stopScroll', true)
if (showHintText.value) clearCanvas()
const pos = getCanvasPosition(
e.touches[0].clientX,
e.touches[0].clientY
)
coordinates.x = pos.x
coordinates.y = pos.y
ctx.value.beginPath()
isDrawing.value = true
}
const handleTouchMove = (e) => {
if (!isDrawing.value) return
const pos = getCanvasPosition(
e.touches[0].clientX,
e.touches[0].clientY
)
ctx.value.moveTo(coordinates.x, coordinates.y)
ctx.value.lineTo(pos.x, pos.y)
ctx.value.stroke()
coordinates.x = pos.x
coordinates.y = pos.y
}
const handleTouchEnd = () => {
emit('stopScroll', false)
if (isDrawing.value) {
ctx.value.closePath()
isDrawing.value = false
}
}
// 画布操作
const clearCanvas = () => {
ctx.value.clearRect(0, 0, board.value.width, board.value.height)
showHintText.value = false
}
// 图片生成
const generateSignatureImage = async () => {
if (showHintText.value || isCanvasEmpty()) {
Toast('请先完成签名')
return null
}
return new Promise(resolve => {
board.value.toBlob(blob => {
const file = new File([blob], 'signature.png', {
type: 'image/png',
lastModified: Date.now()
})
resolve(file)
}, 'image/png', 0.9)
})
}
// OSS 上传
const initOSS = () => {
// 此处需自行配置ali-oss 直接去官方文档跟着配置就好,然后使用这个ali-oss的包就好了
ossInstance = new AliOSS({
region: 'oss-cn-shanghai',
stsURL: `${import.meta.env.VITE_API_COMMON}/sts/security`,
bucket: 'rdts',
path: 'rdts-admin/'
})
}
const uploadSignature = async () => {
try {
const file = await generateSignatureImage()
if (!file) return
const { url } = await ossInstance.put(file.name,file)
await submitSignResult(url)
Toast.success('上传成功')
} catch (error) {
Toast.fail('上传失败')
console.error('Upload error:', error)
}
}
// 辅助方法
const isCanvasEmpty = () => {
const blankCanvas = document.createElement('canvas')
blankCanvas.width = board.value.width
blankCanvas.height = board.value.height
return board.value.toDataURL() === blankCanvas.toDataURL()
}
const submitSignResult = async (signUrl) => {
// 调用API提交签名结果
// const res = await createSign({ signUrl, id: props.id })
// 处理业务逻辑...
}
</script>
<template>
<div ref="canvasBox" class="canvas-box">
<canvas
ref="board"
@touchstart.prevent="handleTouchStart"
@touchmove.prevent="handleTouchMove"
@touchend.prevent="handleTouchEnd"
/>
<van-icon
class="fullscreen-icon"
@click="emit('fullscreen')"
name="expand"
/>
</div>
</template>
<style lang="less" scoped>
.canvas-box {
width: 343px;
height: 210px;
position: relative;
background: #ebedf0;
border-radius: 8px;
}
canvas {
width: 100%;
height: 100%;
}
.fullscreen-icon {
position: absolute;
top: 8px;
right: 8px;
font-size: 18px;
color: #666;
z-index: 1;
}
</style>
优化实践建议
- 性能优化 :使用
requestAnimationFrame
优化绘制过程 - 跨端适配:添加鼠标事件支持实现 PC 端兼容
- 笔锋效果:通过速度计算动态调整线条宽度
- 撤销功能:使用历史记录栈实现多步撤销
结语
通过 Vue3 的组合式 API,我们实现了高内聚低耦合的签名组件。该方案具有以下优势:
- 清晰的响应式状态管理
- 良好的触摸事件兼容性
- 灵活的扩展能力
- 完整的错误处理机制