<template><el-dialogv-model="visible"width="900"title="签名"append-to-body@close="closeListDialog"><div class="signature-container" pb-10px><!-- 签名区域 --><div class="signature-content"><!-- 画布区域 --><div class="canvas-wrapper"><canvasref="canvasRef":width="canvasWidth":height="canvasHeight"class="signature-canvas"></canvas><div class="signature-prompt" v-if="!hasSignature">请在此处签名</div></div><!-- <div v-if="signatureImage" style="margin-top: 20px"><p>预览(实际背景透明):</p><img :src="signatureImage" /></div> --></div><!-- 颜色选择 --><div class="color-palette" items-center h-50px><divv-for="color in colors":key="color.value"class="color-option !mr-10px":class="{ active: currentColor === color.value }":style="{ backgroundColor: color.value }"@click="setColor(color.value)"></div><div flex-1></div><el-button link size="small" class="btn undo" @click="undo">撤销</el-button><el-buttonlinksize="small"my-30pxclass="btn clear"@click="clearCanvas">清除</el-button><el-buttonsize="small"type="danger"class="btn"@click="closeListDialog">取消</el-button><el-button size="small" class="btn confirm" @click="saveSignature">完成</el-button></div></div></el-dialog>
</template><script setup lang="ts">
const emit = defineEmits<{(e: "ok", value: any): void;
}>();
const visible = ref(false);const closeListDialog = () => {clearCanvas();visible.value = false;
};const open = () => {visible.value = true;nextTick(() => {initCanvas();window.addEventListener("resize", handleResize); });
};
defineExpose({open,
});
const canvasWidth = ref(850);
const canvasHeight = ref(400);
const canvasRef: any = ref(null);
const signatureImage = ref("");
const hasSignature = ref(false);
const currentColor = ref("#000000");
const colors = ref([{ value: "#000000", name: "黑色" },{ value: "#FF0000", name: "红色" },{ value: "#0066FF", name: "蓝色" },
]);
let ctx: any = null;
let isDrawing = false;
let lastX = 0;
let lastY = 0;
let drawingHistory: any = [];const initCanvas = () => {const canvas: any = canvasRef.value;ctx = canvas.getContext("2d");ctx.fillStyle = "rgba(0, 0, 0, 0)";ctx.fillRect(0, 0, canvas.width, canvas.height);setColor(currentColor.value);setupTouchEvents(canvas);
};
const setColor = (color: any) => {currentColor.value = color;ctx.strokeStyle = color;ctx.lineWidth = 6;ctx.lineCap = "round";ctx.lineJoin = "round";
};
const setupTouchEvents = (canvas: any) => {canvas.addEventListener("touchstart", handleTouchStart);canvas.addEventListener("touchmove", handleTouchMove);canvas.addEventListener("touchend", handleTouchEnd);canvas.addEventListener("mousedown", handleMouseDown);canvas.addEventListener("mousemove", handleMouseMove);canvas.addEventListener("mouseup", handleMouseUp);
};
const handleTouchStart = (e: any) => {e.preventDefault();const touch = getTouchPos(e);startDrawing(touch.x, touch.y);
};const handleTouchMove = (e: any) => {e.preventDefault();const touch = getTouchPos(e);draw(touch.x, touch.y);
};const handleTouchEnd = () => {endDrawing();
};
const handleMouseDown = (e: any) => {const pos = getMousePos(e);startDrawing(pos.x, pos.y);
};const handleMouseMove = (e: any) => {if (!isDrawing) return;const pos = getMousePos(e);draw(pos.x, pos.y);
};const handleMouseUp = () => {endDrawing();
};
const startDrawing = (x: any, y: any) => {isDrawing = true;lastX = x;lastY = y;ctx.beginPath();ctx.moveTo(x, y);saveDrawingState();
};
const draw = (x: any, y: any) => {if (!isDrawing) return;ctx.lineTo(x, y);ctx.stroke();lastX = x;lastY = y;hasSignature.value = true;
};
const endDrawing = () => {isDrawing = false;
};
const saveDrawingState = () => {const canvas = canvasRef.value;drawingHistory.push(canvas.toDataURL());if (drawingHistory.length > 20) {drawingHistory.shift();}
};
const undo = () => {if (drawingHistory.length > 0) {const lastState = drawingHistory.pop();const img = new Image();img.onload = () => {ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);ctx.drawImage(img, 0, 0);hasSignature.value = drawingHistory.length > 0;};img.src = lastState;}
};
const clearCanvas = () => {ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);drawingHistory = [];hasSignature.value = false;
};
const saveSignature = () => {if (!hasSignature.value) {alert("请先签名");return;}const tempCanvas = document.createElement("canvas");tempCanvas.width = canvasRef.value.width;tempCanvas.height = canvasRef.value.height;const tempCtx: any = tempCanvas.getContext("2d");tempCtx.drawImage(canvasRef.value, 0, 0);signatureImage.value = tempCanvas.toDataURL("image/png");console.log("签名图片:", signatureImage.value);emit("ok", signatureImage.value);closeListDialog();
};
const handleResize = () => {canvasWidth.value = window.innerWidth * 0.8;canvasHeight.value = window.innerHeight * 0.6;
};
const getTouchPos = (e: any) => {const canvas: any = canvasRef.value;const rect = canvas.getBoundingClientRect();const touch = e.touches[0] || e.changedTouches[0];const scaleX = canvas.width / rect.width;const scaleY = canvas.height / rect.height;return {x: (touch.clientX - rect.left) * scaleX,y: (touch.clientY - rect.top) * scaleY,};
};
const getMousePos = (e: any) => {const canvas: any = canvasRef.value;const rect = canvas.getBoundingClientRect();const scaleX = canvas.width / rect.width;const scaleY = canvas.height / rect.height;return {x: (e.clientX - rect.left) * scaleX,y: (e.clientY - rect.top) * scaleY,};
};
</script><style scoped>
.signature-container {position: relative;background-color: #f5f5f5;display: flex;flex-direction: column;font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,Ubuntu, Cantarell, sans-serif;
}.status-bar {display: flex;justify-content: space-between;align-items: center;padding: 5px 15px;background-color: white;font-size: 14px;
}.signature-content {display: flex;flex: 1;padding: 10px;flex-direction: column;
}.color-palette {display: flex;flex-direction: row;align-items: center;background-color: white;border-radius: 10px;margin-inline: 10px;padding-inline: 10px;
}.color-option {width: 30px;height: 30px;border-radius: 50%;margin: 8px 0;cursor: pointer;border: 2px solid #eee;
}.color-option.active {border-color: #0066ff;
}.canvas-wrapper {
}.signature-canvas {display: block;background-color: white;
}.signature-prompt {position: absolute;top: calc(50% - 18px);left: calc(50% - 50px);color: #999;font-size: 16px;
}.action-buttons {display: flex;justify-content: space-around;padding: 15px;background-color: white;
}.btn {border: none;border-radius: 20px;font-size: 14px;cursor: pointer;
}.undo {color: #333;
}.clear {color: #333;
}.confirm {background-color: #0066ff;color: white;
}.address-bar {padding: 8px 15px;background-color: white;text-align: center;font-size: 12px;color: #0066ff;border-top: 1px solid #eee;
}
</style>使用:```javascript
<signature ref="signatureRef" @ok="getsignature" />import signature from "./signature.vue"const signatureRef: any = ref(null);
const addSignature = () => {signatureRef.value?.open();
};
