分步讲解太麻烦了 我直接上全文件吧。看不懂的可以评论或者私聊我。给个赞和关注 谢谢老铁
<template>
<div class="signature-pad-container">
<div class="signature-header">
<h3>{{ title }}</h3>
<p class="subtitle" v-if="subtitle">{{ subtitle }}</p>
</div>
<!-- 签名画布区域 -->
<div class="signature-canvas-wrapper">
<canvas
ref="signatureCanvas"
class="signature-canvas"
:width="canvasWidth"
:height="canvasHeight"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
@touchstart="startDrawing"
@touchmove="draw"
@touchend="stopDrawing"
@touchcancel="stopDrawing"
></canvas>
<!-- 空状态提示 -->
<div class="empty-state" v-if="isEmpty && !isDrawing">
<i class="fas fa-pen"></i>
<p>请在此区域签名</p>
</div>
</div>
<!-- 控制按钮 -->
<div class="signature-controls">
<button
class="control-btn reset-btn"
@click="clearSignature"
:disabled="isEmpty"
>
<i class="fas fa-eraser"></i>
<span>清除</span>
</button>
<button
class="control-btn undo-btn"
@click="undoLastStroke"
:disabled="strokeHistory.length === 0"
>
<i class="fas fa-undo"></i>
<span>撤销</span>
</button>
<button
class="control-btn save-btn"
@click="saveSignature"
:disabled="isEmpty"
>
<i class="fas fa-save"></i>
<span>保存</span>
</button>
</div>
<!-- 预览区域 -->
<div class="signature-preview" v-if="signatureImage && showPreview">
<h4>签名预览</h4>
<img :src="signatureImage" alt="签名预览" class="preview-image">
</div>
</div>
</template>
<script>
export default {
name: 'SignaturePad',
props: {
// 签名区域标题
title: {
type: String,
default: '电子签名'
},
// 签名区域副标题
subtitle: {
type: String,
default: '请使用鼠标或手指在下方区域签名'
},
// 画布宽度
width: {
type: Number,
default: 600
},
// 画布高度
height: {
type: Number,
default: 300
},
// 线条颜色
lineColor: {
type: String,
default: '#000000'
},
// 线条宽度
lineWidth: {
type: Number,
default: 2
},
// 是否显示预览
showPreview: {
type: Boolean,
default: true
},
// 背景颜色
backgroundColor: {
type: String,
default: '#ffffff'
}
},
data() {
return {
// 画布上下文
ctx: null,
// 是否正在绘制
isDrawing: false,
// 上一个点的坐标
lastX: 0,
lastY: 0,
// 签名图像数据
signatureImage: null,
// 是否为空签名
isEmpty: true,
// 笔触历史记录,用于撤销功能
strokeHistory: [],
// 当前笔触路径
currentPath: []
};
},
computed: {
// 计算画布宽度(考虑响应式)
canvasWidth() {
// 在实际应用中,可以根据屏幕尺寸动态调整
return Math.min(this.width, window.innerWidth - 40);
},
canvasHeight() {
return this.height;
}
},
mounted() {
// 初始化画布
this.initCanvas();
},
methods: {
// 初始化画布
initCanvas() {
const canvas = this.$refs.signatureCanvas;
this.ctx = canvas.getContext('2d');
// 设置画布样式
this.ctx.strokeStyle = this.lineColor;
this.ctx.lineWidth = this.lineWidth;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
// 清空画布
this.clearSignature();
},
// 开始绘制
startDrawing(e) {
e.preventDefault(); // 防止触摸设备上的默认行为
this.isDrawing = true;
// 获取相对于画布的坐标
const { offsetX, offsetY } = this.getCanvasCoordinates(e);
this.lastX = offsetX;
this.lastY = offsetY;
// 开始记录新的笔触路径
this.currentPath = [{ x: offsetX, y: offsetY }];
},
// 绘制中
draw(e) {
if (!this.isDrawing) return;
e.preventDefault();
const { offsetX, offsetY } = this.getCanvasCoordinates(e);
// 绘制线条
this.ctx.beginPath();
this.ctx.moveTo(this.lastX, this.lastY);
this.ctx.lineTo(offsetX, offsetY);
this.ctx.stroke();
// 更新上一个点的坐标
this.lastX = offsetX;
this.lastY = offsetY;
// 记录当前路径点
this.currentPath.push({ x: offsetX, y: offsetY });
// 标记为非空签名
this.isEmpty = false;
},
// 停止绘制
stopDrawing() {
if (!this.isDrawing) return;
this.isDrawing = false;
// 将当前笔触添加到历史记录
if (this.currentPath.length > 1) {
this.strokeHistory.push([...this.currentPath]);
this.currentPath = [];
}
},
// 清除签名
clearSignature() {
const canvas = this.$refs.signatureCanvas;
// 清空画布
this.ctx.fillStyle = this.backgroundColor;
this.ctx.fillRect(0, 0, canvas.width, canvas.height);
// 重置状态
this.isEmpty = true;
this.signatureImage = null;
this.strokeHistory = [];
this.currentPath = [];
// 触发清除事件
this.$emit('cleared');
},
// 撤销上一笔
undoLastStroke() {
if (this.strokeHistory.length === 0) return;
// 移除最后一笔
this.strokeHistory.pop();
// 清除画布并重新绘制所有历史笔触
this.clearSignature();
this.isEmpty = this.strokeHistory.length === 0;
if (!this.isEmpty) {
this.strokeHistory.forEach(path => {
this.redrawPath(path);
});
}
},
// 重新绘制路径
redrawPath(path) {
if (path.length < 2) return;
this.ctx.beginPath();
this.ctx.moveTo(path[0].x, path[0].y);
for (let i = 1; i
for (let i = 1; i < path.length; i++) {
this.ctx.lineTo(path[i].x, path[i].y);
}
this.ctx.stroke();
},
// 保存签名
saveSignature() {
if (this.isEmpty) return;
// 获取画布数据URL
const canvas = this.$refs.signatureCanvas;
this.signatureImage = canvas.toDataURL('image/png');
// 触发保存事件,传递签名图片数据
this.$emit('saved', this.signatureImage);
},
// 获取相对于画布的坐标(兼容鼠标和触摸事件)
getCanvasCoordinates(e) {
const canvas = this.$refs.signatureCanvas;
const rect = canvas.getBoundingClientRect();
let offsetX, offsetY;
// 处理触摸事件
if (e.type.includes('touch')) {
offsetX = e.touches[0].clientX - rect.left;
offsetY = e.touches[0].clientY - rect.top;
} else {
// 处理鼠标事件
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
}
return { offsetX, offsetY };
}
},
watch: {
// 监听线条颜色变化
lineColor(newVal) {
if (this.ctx) {
this.ctx.strokeStyle = newVal;
}
},
// 监听线条宽度变化
lineWidth(newVal) {
if (this.ctx) {
this.ctx.lineWidth = newVal;
}
},
// 监听背景颜色变化
backgroundColor(newVal) {
if (this.ctx) {
// 保存当前签名
const tempImage = this.isEmpty ? null : this.$refs.signatureCanvas.toDataURL();
// 重新设置背景
this.ctx.fillStyle = newVal;
this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
// 重新绘制签名
if (tempImage && !this.isEmpty) {
const img = new Image();
img.src = tempImage;
img.onload = () => {
this.ctx.drawImage(img, 0, 0);
};
}
}
}
}
};
</script>
<style scoped>
.signature-pad-container {
font-family: 'Arial', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
}
.signature-header {
text-align: center;
margin-bottom: 20px;
}
.signature-header h3 {
margin: 0 0 10px 0;
color: #333;
font-size: 24px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 14px;
}
.signature-canvas-wrapper {
position: relative;
border: 2px dashed #ccc;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.signature-canvas {
width: 100%;
height: 100%;
cursor: crosshair;
}
.empty-state {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #aaa;
pointer-events: none;
}
.empty-state i {
font-size: 48px;
margin-bottom: 15px;
}
.empty-state p {
margin: 0;
font-size: 16px;
}
.signature-controls {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 20px;
flex-wrap: wrap;
}
.control-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.control-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
background-color: #f87171;
color: white;
}
.reset-btn:hover:not(:disabled) {
background-color: #ef4444;
}
.undo-btn {
background-color: #fbbf24;
color: white;
}
.undo-btn:hover:not(:disabled) {
background-color: #f59e0b;
}
.save-btn {
background-color: #34d399;
color: white;
}
.save-btn:hover:not(:disabled) {
background-color: #10b981;
}
.signature-preview {
margin-top: 30px;
text-align: center;
}
.signature-preview h4 {
margin: 0 0 15px 0;
color: #333;
font-size: 18px;
}
.preview-image {
max-width: 100%;
border: 1px solid #eee;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* 响应式调整 */
@media (max-width: 600px) {
.signature-controls {
flex-direction: column;
}
.control-btn {
width: 100%;
justify-content: center;
}
}
</style>
功能说明
这个签名组件提供了以下核心功能:
-
基础签名功能:
- 支持鼠标和触摸设备(手机、平板)上的签名绘制
- 线条平滑,带有圆角端点,提升签名质感
- 响应式设计,适配不同屏幕尺寸
-
交互功能:
- 清除功能:一键清除当前签名
- 撤销功能:撤销上一笔绘制
- 保存功能:将签名保存为 PNG 图片
-
自定义选项:
- 可自定义签名区域的宽度和高度
- 可调整线条颜色和粗细
- 可设置背景颜色
- 可自定义标题和副标题
-
状态反馈:
- 空状态提示:未签名时显示提示信息
- 按钮状态管理:未签名时禁用保存和清除按钮
- 签名预览:保存后显示签名预览
使用方法
-
首先确保项目中已引入 Font Awesome 图标库,用于显示按钮图标
-
在你的 Vue 组件中引入并使用签名组件:
<template>
<div>
<SignaturePad
title="请签名确认"
subtitle="同意以上条款并签名"
:lineWidth="3"
:lineColor="'#2c3e50'"
:height="200"
@saved="handleSignatureSaved"
@cleared="handleSignatureCleared"
/>
</div>
</template>
<script>
import SignaturePad from './SignaturePad.vue';
export default {
components: {
SignaturePad
},
methods: {
// 处理签名保存事件
handleSignatureSaved(imageData) {
console.log('签名已保存', imageData);
// 这里可以将签名数据发送到服务器或进行其他处理
},
// 处理签名清除事件
handleSignatureCleared() {
console.log('签名已清除');
}
}
};
</script>
扩展建议
- 可以添加更多自定义选项,如支持多种线条样式、签名颜色选择器等
- 可以增加签名验证功能,判断签名是否有效(如是否过于简单)
- 可以添加保存为不同格式的选项(如 JPG、SVG)
- 可以实现签名的缩放和移动功能,方便在小屏幕上进行签名
这个组件轻量级且易于集成,适合在需要电子签名的场景中使用,如合同签署、表单确认等。
分享
在 Vue 中实现手动签名功能时,如何处理签名数据的存储和传输?
除了上述方法,还有哪些其他方式可以在 Vue 中实现手动签名功能?
如何在 Vue 中实现签名的验证和识别?
2524

被折叠的 条评论
为什么被折叠?



