Files
hzdyjs/web/src/components/SignaturePad/index.vue
T

248 lines
5.4 KiB
Vue

<template>
<div class="signature-pad-container">
<div class="signature-canvas-wrapper">
<canvas
ref="canvasRef"
class="signature-canvas"
:width="width"
:height="height"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
@touchstart="handleTouch"
@touchmove="handleTouch"
@touchend="stopDrawing"
/>
</div>
<div class="signature-buttons">
<el-button size="small" @click="clearSignature">清除</el-button>
<el-button v-if="showSaveButton" size="small" type="primary" @click="saveSignature">保存</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { ElMessage } from 'element-plus';
// Props
interface Props {
modelValue?: string; // base64图片数据
width?: number;
height?: number;
showSaveButton?: boolean; // 是否显示保存按钮
autoSave?: boolean; // 是否自动保存(绘制时实时保存)
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
width: 400,
height: 150,
showSaveButton: true,
autoSave: true
});
// Emits
const emit = defineEmits<{
'update:modelValue': [value: string];
'save': [value: string];
}>();
// Refs
const canvasRef = ref<HTMLCanvasElement>();
const isDrawing = ref(false);
const lastPoint = ref({ x: 0, y: 0 });
// Methods
const startDrawing = (event: MouseEvent) => {
if (!canvasRef.value) return;
isDrawing.value = true;
const rect = canvasRef.value.getBoundingClientRect();
lastPoint.value = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
};
const draw = (event: MouseEvent) => {
if (!isDrawing.value || !canvasRef.value) return;
const ctx = canvasRef.value.getContext('2d');
if (!ctx) return;
const rect = canvasRef.value.getBoundingClientRect();
const currentPoint = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
// 绘制线条
ctx.beginPath();
ctx.moveTo(lastPoint.value.x, lastPoint.value.y);
ctx.lineTo(currentPoint.x, currentPoint.y);
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
lastPoint.value = currentPoint;
// 自动保存
if (props.autoSave) {
saveToModel();
}
};
const stopDrawing = () => {
isDrawing.value = false;
// 如果不是自动保存,在停止绘制时保存
if (!props.autoSave) {
saveToModel();
}
};
// 触摸事件处理
const handleTouch = (event: TouchEvent) => {
event.preventDefault();
const touch = event.touches[0];
if (!touch || !canvasRef.value) return;
const rect = canvasRef.value.getBoundingClientRect();
const mouseEvent = new MouseEvent(
event.type === 'touchstart' ? 'mousedown' : 'mousemove',
{
clientX: touch.clientX,
clientY: touch.clientY
}
);
if (event.type === 'touchstart') {
startDrawing(mouseEvent);
} else if (event.type === 'touchmove') {
draw(mouseEvent);
}
};
// 清除签名
const clearSignature = () => {
if (!canvasRef.value) return;
const ctx = canvasRef.value.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
emit('update:modelValue', '');
};
// 保存到 v-model
const saveToModel = () => {
if (!canvasRef.value) return;
try {
const dataURL = canvasRef.value.toDataURL('image/png');
emit('update:modelValue', dataURL);
} catch (error) {
console.error('保存签名失败:', error);
}
};
// 手动保存(触发save事件)
const saveSignature = () => {
if (!canvasRef.value) return;
try {
const dataURL = canvasRef.value.toDataURL('image/png');
emit('update:modelValue', dataURL);
emit('save', dataURL);
ElMessage.success('签名已保存');
} catch (error) {
console.error('保存签名失败:', error);
ElMessage.error('保存签名失败');
}
};
// 从base64恢复签名
const restoreSignature = (base64Image: string) => {
if (!canvasRef.value || !base64Image) return;
const ctx = canvasRef.value.getContext('2d');
if (!ctx) return;
const img = new Image();
img.onload = () => {
if (!canvasRef.value) return;
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
ctx.drawImage(img, 0, 0);
};
img.src = base64Image;
};
// 监听 modelValue 变化,恢复签名
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
restoreSignature(newValue);
} else {
clearSignature();
}
}
);
// 组件挂载时恢复签名
onMounted(() => {
if (props.modelValue) {
restoreSignature(props.modelValue);
}
});
// 暴露方法供父组件调用
defineExpose({
clearSignature,
saveSignature
});
</script>
<style scoped lang="scss">
.signature-pad-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: 100%;
}
.signature-canvas-wrapper {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #fafafa;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px;
}
.signature-canvas {
border: 1px dashed #dcdfe6;
border-radius: 4px;
cursor: crosshair;
background-color: #fff;
touch-action: none; /* 防止触摸时页面滚动 */
}
.signature-canvas:hover {
border-color: #409eff;
}
.signature-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
</style>