|
@@ -1,335 +0,0 @@
|
|
|
-<template>
|
|
|
- <div class="signature-pad-wrapper" :style="{ width: width, height: height }">
|
|
|
- <canvas
|
|
|
- ref="signatureCanvas"
|
|
|
- class="signature-pad"
|
|
|
- @mousedown="startDrawing"
|
|
|
- @mousemove="draw"
|
|
|
- @mouseup="stopDrawing"
|
|
|
- @mouseleave="stopDrawing"
|
|
|
- @touchstart="handleTouchStart"
|
|
|
- @touchmove="handleTouchMove"
|
|
|
- @touchend="stopDrawing"
|
|
|
- @touchcancel="stopDrawing"
|
|
|
- />
|
|
|
-
|
|
|
- <div v-if="showControls" class="signature-controls">
|
|
|
- <button
|
|
|
- class="signature-btn clear-btn"
|
|
|
- type="button"
|
|
|
- @click="clearSignature"
|
|
|
- >
|
|
|
- {{ clearBtnText }}
|
|
|
- </button>
|
|
|
- <button
|
|
|
- class="signature-btn save-btn"
|
|
|
- type="button"
|
|
|
- @click="saveSignature"
|
|
|
- >
|
|
|
- {{ saveBtnText }}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup lang="ts">
|
|
|
-import { ref, onMounted, onUnmounted, watch } from 'vue';
|
|
|
-
|
|
|
-// 定义组件属性
|
|
|
-const props = defineProps({
|
|
|
- // 样式相关
|
|
|
- width: {
|
|
|
- type: String,
|
|
|
- default: '100%',
|
|
|
- },
|
|
|
- height: {
|
|
|
- type: String,
|
|
|
- default: '200px',
|
|
|
- },
|
|
|
- lineWidth: {
|
|
|
- type: Number,
|
|
|
- default: 2,
|
|
|
- },
|
|
|
- lineColor: {
|
|
|
- type: String,
|
|
|
- default: '#000',
|
|
|
- },
|
|
|
- backgroundColor: {
|
|
|
- type: String,
|
|
|
- default: '',
|
|
|
- },
|
|
|
- // 功能控制
|
|
|
- showControls: {
|
|
|
- type: Boolean,
|
|
|
- default: true,
|
|
|
- },
|
|
|
- clearBtnText: {
|
|
|
- type: String,
|
|
|
- default: '清除',
|
|
|
- },
|
|
|
- saveBtnText: {
|
|
|
- type: String,
|
|
|
- default: '保存',
|
|
|
- },
|
|
|
- // 初始值
|
|
|
- value: {
|
|
|
- type: String,
|
|
|
- default: '',
|
|
|
- },
|
|
|
-});
|
|
|
-
|
|
|
-// 定义事件
|
|
|
-const emit = defineEmits<{
|
|
|
- 'update:value': [value: string];
|
|
|
- save: [value: string];
|
|
|
- clear: [];
|
|
|
-}>();
|
|
|
-
|
|
|
-// 画布和上下文引用
|
|
|
-const signatureCanvas = ref<HTMLCanvasElement | null>(null);
|
|
|
-let ctx: CanvasRenderingContext2D | null = null;
|
|
|
-
|
|
|
-// 状态变量
|
|
|
-const isDrawing = ref(false);
|
|
|
-const signatureData = ref<string>(props.value);
|
|
|
-
|
|
|
-// 监听value属性变化
|
|
|
-watch(
|
|
|
- () => props.value,
|
|
|
- (newValue) => {
|
|
|
- if (newValue !== signatureData.value) {
|
|
|
- signatureData.value = newValue;
|
|
|
- loadSignatureFromData();
|
|
|
- }
|
|
|
- },
|
|
|
-);
|
|
|
-
|
|
|
-// 从base64数据加载签名
|
|
|
-const loadSignatureFromData = () => {
|
|
|
- if (!ctx || !signatureCanvas.value || !signatureData.value) return;
|
|
|
-
|
|
|
- const img = new Image();
|
|
|
- img.onload = () => {
|
|
|
- if (!ctx || !signatureCanvas.value) return;
|
|
|
- ctx.clearRect(
|
|
|
- 0,
|
|
|
- 0,
|
|
|
- signatureCanvas.value.width,
|
|
|
- signatureCanvas.value.height,
|
|
|
- );
|
|
|
- ctx.drawImage(img, 0, 0);
|
|
|
- };
|
|
|
- img.src = signatureData.value;
|
|
|
-};
|
|
|
-
|
|
|
-// 初始化画布
|
|
|
-const initCanvas = () => {
|
|
|
- if (!signatureCanvas.value) return;
|
|
|
-
|
|
|
- // 设置画布尺寸
|
|
|
- const canvas = signatureCanvas.value;
|
|
|
- const container = canvas.parentElement as HTMLElement;
|
|
|
- canvas.width = container.clientWidth;
|
|
|
- canvas.height = container.clientHeight;
|
|
|
-
|
|
|
- // 获取绘图上下文
|
|
|
- ctx = canvas.getContext('2d');
|
|
|
- if (!ctx) return;
|
|
|
-
|
|
|
- // 设置绘图样式
|
|
|
- ctx.lineWidth = props.lineWidth;
|
|
|
- ctx.lineCap = 'round';
|
|
|
- ctx.lineJoin = 'round';
|
|
|
- ctx.strokeStyle = props.lineColor;
|
|
|
-
|
|
|
- // 设置背景色
|
|
|
- if (props.backgroundColor) {
|
|
|
- ctx.fillStyle = props.backgroundColor;
|
|
|
- ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
- }
|
|
|
-
|
|
|
- // 加载初始签名数据
|
|
|
- if (signatureData.value) {
|
|
|
- loadSignatureFromData();
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-// 开始绘制
|
|
|
-const startDrawing = (e: MouseEvent) => {
|
|
|
- if (!ctx) return;
|
|
|
-
|
|
|
- isDrawing.value = true;
|
|
|
- ctx.beginPath();
|
|
|
- ctx.moveTo(e.offsetX, e.offsetY);
|
|
|
-};
|
|
|
-
|
|
|
-// 绘制
|
|
|
-const draw = (e: MouseEvent) => {
|
|
|
- if (!isDrawing.value || !ctx) return;
|
|
|
-
|
|
|
- ctx.lineTo(e.offsetX, e.offsetY);
|
|
|
- ctx.stroke();
|
|
|
-};
|
|
|
-
|
|
|
-// 处理触摸开始事件
|
|
|
-const handleTouchStart = (e: TouchEvent) => {
|
|
|
- if (!ctx || !signatureCanvas.value) return;
|
|
|
- e.preventDefault();
|
|
|
-
|
|
|
- const touch = e.touches[0];
|
|
|
- const rect = signatureCanvas.value.getBoundingClientRect();
|
|
|
- const offsetX = touch.clientX - rect.left;
|
|
|
- const offsetY = touch.clientY - rect.top;
|
|
|
-
|
|
|
- isDrawing.value = true;
|
|
|
- ctx.beginPath();
|
|
|
- ctx.moveTo(offsetX, offsetY);
|
|
|
-};
|
|
|
-
|
|
|
-// 处理触摸移动事件
|
|
|
-const handleTouchMove = (e: TouchEvent) => {
|
|
|
- if (!isDrawing.value || !ctx || !signatureCanvas.value) return;
|
|
|
- e.preventDefault();
|
|
|
-
|
|
|
- const touch = e.touches[0];
|
|
|
- const rect = signatureCanvas.value.getBoundingClientRect();
|
|
|
- const offsetX = touch.clientX - rect.left;
|
|
|
- const offsetY = touch.clientY - rect.top;
|
|
|
-
|
|
|
- ctx.lineTo(offsetX, offsetY);
|
|
|
- ctx.stroke();
|
|
|
-};
|
|
|
-
|
|
|
-// 停止绘制
|
|
|
-const stopDrawing = () => {
|
|
|
- if (isDrawing.value) {
|
|
|
- isDrawing.value = false;
|
|
|
- updateSignatureData();
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-// 更新签名数据
|
|
|
-const updateSignatureData = () => {
|
|
|
- if (!signatureCanvas.value) return;
|
|
|
-
|
|
|
- try {
|
|
|
- const dataUrl = signatureCanvas.value.toDataURL('image/png');
|
|
|
- signatureData.value = dataUrl;
|
|
|
- emit('update:value', dataUrl);
|
|
|
- } catch (error) {
|
|
|
- console.error('更新签名数据失败:', error);
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-// 清除签名
|
|
|
-const clearSignature = () => {
|
|
|
- if (!ctx || !signatureCanvas.value) return;
|
|
|
-
|
|
|
- ctx.clearRect(
|
|
|
- 0,
|
|
|
- 0,
|
|
|
- signatureCanvas.value.width,
|
|
|
- signatureCanvas.value.height,
|
|
|
- );
|
|
|
-
|
|
|
- // 重新设置背景色
|
|
|
- if (props.backgroundColor) {
|
|
|
- ctx.fillStyle = props.backgroundColor;
|
|
|
- ctx.fillRect(
|
|
|
- 0,
|
|
|
- 0,
|
|
|
- signatureCanvas.value.width,
|
|
|
- signatureCanvas.value.height,
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- signatureData.value = '';
|
|
|
- emit('update:value', '');
|
|
|
- emit('clear');
|
|
|
-};
|
|
|
-
|
|
|
-// 保存签名
|
|
|
-const saveSignature = () => {
|
|
|
- if (!signatureCanvas.value) return;
|
|
|
-
|
|
|
- try {
|
|
|
- const dataUrl = signatureCanvas.value.toDataURL('image/png');
|
|
|
- signatureData.value = dataUrl;
|
|
|
- emit('update:value', dataUrl);
|
|
|
- emit('save', dataUrl);
|
|
|
- } catch (error) {
|
|
|
- console.error('保存签名失败:', error);
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-// 提供给父组件的方法
|
|
|
-defineExpose({
|
|
|
- clear: clearSignature,
|
|
|
- save: saveSignature,
|
|
|
- isEmpty: () => !signatureData.value,
|
|
|
-});
|
|
|
-
|
|
|
-// 窗口大小改变时重新调整画布
|
|
|
-const handleResize = () => {
|
|
|
- initCanvas();
|
|
|
- if (signatureData.value) {
|
|
|
- loadSignatureFromData();
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-onMounted(() => {
|
|
|
- initCanvas();
|
|
|
- window.addEventListener('resize', handleResize);
|
|
|
-});
|
|
|
-
|
|
|
-onUnmounted(() => {
|
|
|
- window.removeEventListener('resize', handleResize);
|
|
|
-});
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-.signature-pad-wrapper {
|
|
|
- position: relative;
|
|
|
- border: 1px solid #ddd;
|
|
|
- border-radius: 4px;
|
|
|
- background-color: #fff;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-
|
|
|
-.signature-pad {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- cursor: crosshair;
|
|
|
-}
|
|
|
-
|
|
|
-.signature-controls {
|
|
|
- position: absolute;
|
|
|
- bottom: 10px;
|
|
|
- right: 10px;
|
|
|
- display: flex;
|
|
|
- gap: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.signature-btn {
|
|
|
- padding: 6px 12px;
|
|
|
- border: none;
|
|
|
- border-radius: 4px;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 14px;
|
|
|
- color: white;
|
|
|
- opacity: 0.7;
|
|
|
- transition: opacity 0.2s;
|
|
|
-}
|
|
|
-
|
|
|
-.signature-btn:hover {
|
|
|
- opacity: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.clear-btn {
|
|
|
- background-color: #f44336;
|
|
|
-}
|
|
|
-
|
|
|
-.save-btn {
|
|
|
- background-color: #4caf50;
|
|
|
-}
|
|
|
-</style>
|