181 lines
3.9 KiB
Vue
181 lines
3.9 KiB
Vue
|
<template>
|
||
|
<div class="signature-pad-container">
|
||
|
<canvas
|
||
|
ref="canvasRef"
|
||
|
class="signature-pad"
|
||
|
:style="{
|
||
|
width: '100%',
|
||
|
height: '100%',
|
||
|
backgroundColor: '#fff',
|
||
|
border: '1px solid #e5e5e5',
|
||
|
borderRadius: '4px'
|
||
|
}"
|
||
|
@touchstart="handleStart"
|
||
|
@touchmove="handleMove"
|
||
|
@touchend="handleEnd"
|
||
|
@mousedown="handleStart"
|
||
|
@mousemove="handleMove"
|
||
|
@mouseup="handleEnd"
|
||
|
@mouseleave="handleEnd"
|
||
|
></canvas>
|
||
|
<div class="signature-controls">
|
||
|
<van-button
|
||
|
type="default"
|
||
|
size="small"
|
||
|
@click="clearCanvas"
|
||
|
>清除</van-button>
|
||
|
<van-button
|
||
|
type="primary"
|
||
|
size="small"
|
||
|
@click="handleConfirm"
|
||
|
>确认</van-button>
|
||
|
</div>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script setup>
|
||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||
|
|
||
|
const props = defineProps({
|
||
|
modelValue: {
|
||
|
type: String,
|
||
|
default: ''
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||
|
|
||
|
const canvasRef = ref(null)
|
||
|
const ctx = ref(null)
|
||
|
const isDrawing = ref(false)
|
||
|
const lastX = ref(0)
|
||
|
const lastY = ref(0)
|
||
|
const LINE_WIDTH = 2 // 固定画笔粗细
|
||
|
|
||
|
// 初始化画布
|
||
|
const initCanvas = () => {
|
||
|
const canvas = canvasRef.value
|
||
|
const dpr = window.devicePixelRatio || 1
|
||
|
const rect = canvas.getBoundingClientRect()
|
||
|
|
||
|
// 设置画布的实际大小
|
||
|
canvas.width = rect.width * dpr
|
||
|
canvas.height = rect.height * dpr
|
||
|
|
||
|
ctx.value = canvas.getContext('2d')
|
||
|
|
||
|
// 缩放画布以匹配设备像素比
|
||
|
ctx.value.scale(dpr, dpr)
|
||
|
ctx.value.lineCap = 'round'
|
||
|
ctx.value.lineJoin = 'round'
|
||
|
ctx.value.strokeStyle = '#000'
|
||
|
ctx.value.lineWidth = LINE_WIDTH
|
||
|
}
|
||
|
|
||
|
// 开始绘制
|
||
|
const handleStart = (e) => {
|
||
|
e.preventDefault() // 防止页面滚动
|
||
|
isDrawing.value = true
|
||
|
const point = getPoint(e)
|
||
|
lastX.value = point.x
|
||
|
lastY.value = point.y
|
||
|
}
|
||
|
|
||
|
// 绘制中
|
||
|
const handleMove = (e) => {
|
||
|
if (!isDrawing.value) return
|
||
|
e.preventDefault() // 防止页面滚动
|
||
|
|
||
|
const point = getPoint(e)
|
||
|
ctx.value.beginPath()
|
||
|
ctx.value.moveTo(lastX.value, lastY.value)
|
||
|
ctx.value.lineTo(point.x, point.y)
|
||
|
ctx.value.stroke()
|
||
|
|
||
|
lastX.value = point.x
|
||
|
lastY.value = point.y
|
||
|
}
|
||
|
|
||
|
// 结束绘制
|
||
|
const handleEnd = () => {
|
||
|
isDrawing.value = false
|
||
|
}
|
||
|
|
||
|
// 获取触点坐标
|
||
|
const getPoint = (e) => {
|
||
|
const canvas = canvasRef.value
|
||
|
const rect = canvas.getBoundingClientRect()
|
||
|
const dpr = window.devicePixelRatio || 1
|
||
|
const event = e.touches ? e.touches[0] : e
|
||
|
|
||
|
// 计算实际的触点位置
|
||
|
const x = (event.clientX - rect.left)
|
||
|
const y = (event.clientY - rect.top)
|
||
|
|
||
|
return {
|
||
|
x: x,
|
||
|
y: y
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 清除画布
|
||
|
const clearCanvas = () => {
|
||
|
const canvas = canvasRef.value
|
||
|
ctx.value.clearRect(0, 0, canvas.width, canvas.height)
|
||
|
emit('update:modelValue', '')
|
||
|
emit('change', '')
|
||
|
}
|
||
|
|
||
|
// 确认并生成图片
|
||
|
const handleConfirm = () => {
|
||
|
const canvas = canvasRef.value
|
||
|
const imageData = canvas.toDataURL('image/png')
|
||
|
emit('update:modelValue', imageData)
|
||
|
emit('change', imageData)
|
||
|
}
|
||
|
|
||
|
// 监听屏幕旋转
|
||
|
const handleResize = () => {
|
||
|
const canvas = canvasRef.value
|
||
|
const imageData = canvas.toDataURL('image/png')
|
||
|
initCanvas()
|
||
|
// 恢复之前的内容
|
||
|
const img = new Image()
|
||
|
img.onload = () => {
|
||
|
ctx.value.drawImage(img, 0, 0, canvas.width, canvas.height)
|
||
|
}
|
||
|
img.src = imageData
|
||
|
}
|
||
|
|
||
|
onMounted(() => {
|
||
|
initCanvas()
|
||
|
window.addEventListener('resize', handleResize)
|
||
|
})
|
||
|
|
||
|
onBeforeUnmount(() => {
|
||
|
window.removeEventListener('resize', handleResize)
|
||
|
})
|
||
|
</script>
|
||
|
|
||
|
<style scoped>
|
||
|
.signature-pad-container {
|
||
|
width: 100%;
|
||
|
height: 100%;
|
||
|
display: flex;
|
||
|
flex-direction: column;
|
||
|
gap: 10px;
|
||
|
}
|
||
|
|
||
|
.signature-pad {
|
||
|
flex: 1;
|
||
|
touch-action: none;
|
||
|
}
|
||
|
|
||
|
.signature-controls {
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
justify-content: flex-end;
|
||
|
gap: 16px;
|
||
|
padding: 8px;
|
||
|
}
|
||
|
</style>
|