135 lines
2.9 KiB
Vue
135 lines
2.9 KiB
Vue
<template>
|
|
<div class="signature-container">
|
|
<canvas
|
|
ref="canvasRef"
|
|
@mousedown="startDrawing"
|
|
@mousemove="draw"
|
|
@mouseup="stopDrawing"
|
|
@mouseleave="stopDrawing"
|
|
@touchstart.prevent="startDrawing"
|
|
@touchmove.prevent="draw"
|
|
@touchend.prevent="stopDrawing"
|
|
></canvas>
|
|
<div class="actions">
|
|
<el-button size="small" @click="clear">重签</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
|
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
const isDrawing = ref(false)
|
|
const ctx = ref<CanvasRenderingContext2D | null>(null)
|
|
|
|
// 初始化 Canvas
|
|
onMounted(() => {
|
|
if (canvasRef.value) {
|
|
const canvas = canvasRef.value
|
|
// 设置画布大小 (可以根据父容器调整)
|
|
canvas.width = canvas.offsetWidth
|
|
canvas.height = 300 // 固定高度
|
|
ctx.value = canvas.getContext('2d')
|
|
if (ctx.value) {
|
|
ctx.value.lineWidth = 3
|
|
ctx.value.lineCap = 'round'
|
|
ctx.value.strokeStyle = '#000'
|
|
}
|
|
}
|
|
})
|
|
|
|
// 获取坐标 (兼容鼠标和触摸)
|
|
const getPos = (e: MouseEvent | TouchEvent) => {
|
|
const canvas = canvasRef.value
|
|
if (!canvas) return { x: 0, y: 0 }
|
|
|
|
const rect = canvas.getBoundingClientRect()
|
|
let clientX, clientY
|
|
|
|
if ('touches' in e) {
|
|
clientX = e.touches[0].clientX
|
|
clientY = e.touches[0].clientY
|
|
} else {
|
|
clientX = (e as MouseEvent).clientX
|
|
clientY = (e as MouseEvent).clientY
|
|
}
|
|
|
|
return {
|
|
x: clientX - rect.left,
|
|
y: clientY - rect.top
|
|
}
|
|
}
|
|
|
|
const startDrawing = (e: MouseEvent | TouchEvent) => {
|
|
isDrawing.value = true
|
|
const { x, y } = getPos(e)
|
|
ctx.value?.beginPath()
|
|
ctx.value?.moveTo(x, y)
|
|
}
|
|
|
|
const draw = (e: MouseEvent | TouchEvent) => {
|
|
if (!isDrawing.value) return
|
|
const { x, y } = getPos(e)
|
|
ctx.value?.lineTo(x, y)
|
|
ctx.value?.stroke()
|
|
}
|
|
|
|
const stopDrawing = () => {
|
|
isDrawing.value = false
|
|
}
|
|
|
|
const clear = () => {
|
|
if (canvasRef.value && ctx.value) {
|
|
ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 导出签名为 File 对象
|
|
*/
|
|
const generateFile = (): Promise<File | null> => {
|
|
return new Promise((resolve) => {
|
|
if (!canvasRef.value) {
|
|
resolve(null)
|
|
return
|
|
}
|
|
canvasRef.value.toBlob((blob) => {
|
|
if (blob) {
|
|
const file = new File([blob], `sign_${Date.now()}.png`, { type: 'image/png' })
|
|
resolve(file)
|
|
} else {
|
|
resolve(null)
|
|
}
|
|
}, 'image/png')
|
|
})
|
|
}
|
|
|
|
// 暴露方法给父组件
|
|
defineExpose({
|
|
clear,
|
|
generateFile
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.signature-container {
|
|
border: 1px solid #dcdfe6;
|
|
border-radius: 4px;
|
|
background: #f5f7fa;
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
width: 100%; /* 响应式宽度 */
|
|
height: 300px;
|
|
cursor: crosshair;
|
|
background: #fff;
|
|
}
|
|
.actions {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
right: 10px;
|
|
}
|
|
</style> |