279 lines
6.3 KiB
Vue
279 lines
6.3 KiB
Vue
<template>
|
|
<div class="signature-pad-wrapper">
|
|
<div class="signature-header">
|
|
<span class="signature-title">手写签名</span>
|
|
<el-button v-if="hasSignature" size="small" type="danger" :icon="Delete" @click="handleClear">清空</el-button>
|
|
</div>
|
|
<div class="signature-canvas-container">
|
|
<canvas
|
|
ref="canvasRef"
|
|
class="signature-canvas"
|
|
@mousedown="handleMouseDown"
|
|
@mousemove="handleMouseMove"
|
|
@mouseup="handleMouseUp"
|
|
@mouseleave="handleMouseUp"
|
|
@touchstart="handleTouchStart"
|
|
@touchmove="handleTouchMove"
|
|
@touchend="handleTouchEnd"
|
|
></canvas>
|
|
<div v-if="!hasSignature" class="signature-placeholder">请在此处签名</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
|
import { Delete } from '@element-plus/icons-vue';
|
|
|
|
interface Props {
|
|
modelValue?: string; // Base64签名数据
|
|
width?: number;
|
|
height?: number;
|
|
lineWidth?: number;
|
|
lineColor?: string;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
modelValue: '',
|
|
width: 500,
|
|
height: 200,
|
|
lineWidth: 2,
|
|
lineColor: '#000000'
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: string): void;
|
|
}>();
|
|
|
|
const canvasRef = ref<HTMLCanvasElement>();
|
|
const hasSignature = ref(false);
|
|
const isDrawing = ref(false);
|
|
let ctx: CanvasRenderingContext2D | null = null;
|
|
let resizeObserver: ResizeObserver | null = null;
|
|
|
|
onMounted(() => {
|
|
initCanvas();
|
|
// 如果有初始值,加载到画布上
|
|
if (props.modelValue) {
|
|
loadSignature(props.modelValue);
|
|
}
|
|
|
|
// 监听容器大小变化
|
|
if (canvasRef.value?.parentElement) {
|
|
resizeObserver = new ResizeObserver(() => {
|
|
handleResize();
|
|
});
|
|
resizeObserver.observe(canvasRef.value.parentElement);
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (resizeObserver) {
|
|
resizeObserver.disconnect();
|
|
}
|
|
});
|
|
|
|
// 监听外部值变化
|
|
watch(
|
|
() => props.modelValue,
|
|
(newVal) => {
|
|
if (newVal && !hasSignature.value) {
|
|
loadSignature(newVal);
|
|
}
|
|
}
|
|
);
|
|
|
|
const initCanvas = () => {
|
|
if (!canvasRef.value) return;
|
|
|
|
const canvas = canvasRef.value;
|
|
const container = canvas.parentElement;
|
|
|
|
// 获取容器的实际宽度
|
|
const containerWidth = container?.clientWidth || props.width;
|
|
|
|
// 设置画布的物理尺寸(实际像素)
|
|
canvas.width = containerWidth;
|
|
canvas.height = props.height;
|
|
|
|
// 同时设置CSS样式,确保显示尺寸与物理尺寸一致
|
|
canvas.style.width = `${containerWidth}px`;
|
|
canvas.style.height = `${props.height}px`;
|
|
|
|
ctx = canvas.getContext('2d');
|
|
if (ctx) {
|
|
ctx.strokeStyle = props.lineColor;
|
|
ctx.lineWidth = props.lineWidth;
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
}
|
|
};
|
|
|
|
// 处理容器大小变化
|
|
const handleResize = () => {
|
|
if (!canvasRef.value) return;
|
|
|
|
// 保存当前签名
|
|
const currentSignature = hasSignature.value ? canvasRef.value.toDataURL('image/png') : '';
|
|
|
|
// 重新初始化画布
|
|
initCanvas();
|
|
|
|
// 恢复签名
|
|
if (currentSignature && hasSignature.value) {
|
|
nextTick(() => {
|
|
loadSignature(currentSignature);
|
|
});
|
|
}
|
|
};
|
|
|
|
const startDrawing = (x: number, y: number) => {
|
|
if (!ctx || !canvasRef.value) return;
|
|
|
|
isDrawing.value = true;
|
|
const rect = canvasRef.value.getBoundingClientRect();
|
|
ctx.beginPath();
|
|
ctx.moveTo(x - rect.left, y - rect.top);
|
|
};
|
|
|
|
const draw = (x: number, y: number) => {
|
|
if (!isDrawing.value || !ctx || !canvasRef.value) return;
|
|
|
|
const rect = canvasRef.value.getBoundingClientRect();
|
|
ctx.lineTo(x - rect.left, y - rect.top);
|
|
ctx.stroke();
|
|
hasSignature.value = true;
|
|
};
|
|
|
|
const stopDrawing = () => {
|
|
if (!isDrawing.value) return;
|
|
|
|
isDrawing.value = false;
|
|
if (ctx) {
|
|
ctx.closePath();
|
|
}
|
|
|
|
// 导出签名数据
|
|
if (hasSignature.value && canvasRef.value) {
|
|
const signatureData = canvasRef.value.toDataURL('image/png');
|
|
emit('update:modelValue', signatureData);
|
|
}
|
|
};
|
|
|
|
// 鼠标事件处理
|
|
const handleMouseDown = (e: MouseEvent) => {
|
|
startDrawing(e.clientX, e.clientY);
|
|
};
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
draw(e.clientX, e.clientY);
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
stopDrawing();
|
|
};
|
|
|
|
// 触摸事件处理(移动端支持)
|
|
const handleTouchStart = (e: TouchEvent) => {
|
|
e.preventDefault();
|
|
const touch = e.touches[0];
|
|
startDrawing(touch.clientX, touch.clientY);
|
|
};
|
|
|
|
const handleTouchMove = (e: TouchEvent) => {
|
|
e.preventDefault();
|
|
const touch = e.touches[0];
|
|
draw(touch.clientX, touch.clientY);
|
|
};
|
|
|
|
const handleTouchEnd = (e: TouchEvent) => {
|
|
e.preventDefault();
|
|
stopDrawing();
|
|
};
|
|
|
|
// 清空签名
|
|
const handleClear = () => {
|
|
if (!canvasRef.value || !ctx) return;
|
|
|
|
ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
|
|
hasSignature.value = false;
|
|
emit('update:modelValue', '');
|
|
};
|
|
|
|
// 加载签名
|
|
const loadSignature = (base64Data: string) => {
|
|
if (!canvasRef.value || !ctx) return;
|
|
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
ctx!.clearRect(0, 0, canvasRef.value!.width, canvasRef.value!.height);
|
|
ctx!.drawImage(img, 0, 0);
|
|
hasSignature.value = true;
|
|
};
|
|
img.src = base64Data;
|
|
};
|
|
|
|
// 获取签名数据
|
|
const getSignature = (): string => {
|
|
if (!canvasRef.value || !hasSignature.value) return '';
|
|
return canvasRef.value.toDataURL('image/png');
|
|
};
|
|
|
|
// 对外暴露方法
|
|
defineExpose({
|
|
clear: handleClear,
|
|
getSignature,
|
|
hasSignature: () => hasSignature.value
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.signature-pad-wrapper {
|
|
width: 100%;
|
|
|
|
.signature-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
|
|
.signature-title {
|
|
font-size: 14px;
|
|
color: #606266;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
.signature-canvas-container {
|
|
position: relative;
|
|
width: 100%;
|
|
border: 1px solid #dcdfe6;
|
|
border-radius: 4px;
|
|
background-color: #ffffff;
|
|
overflow: hidden;
|
|
|
|
&:hover {
|
|
border-color: #c0c4cc;
|
|
}
|
|
|
|
.signature-canvas {
|
|
display: block;
|
|
width: 100%;
|
|
cursor: crosshair;
|
|
touch-action: none;
|
|
}
|
|
|
|
.signature-placeholder {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: #c0c4cc;
|
|
font-size: 14px;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
}
|
|
}
|
|
</style>
|