vue实现手动签名功能

分步讲解太麻烦了 我直接上全文件吧。看不懂的可以评论或者私聊我。给个赞和关注 谢谢老铁

<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>
    

功能说明

这个签名组件提供了以下核心功能:

  1. 基础签名功能

    • 支持鼠标和触摸设备(手机、平板)上的签名绘制
    • 线条平滑,带有圆角端点,提升签名质感
    • 响应式设计,适配不同屏幕尺寸
  2. 交互功能

    • 清除功能:一键清除当前签名
    • 撤销功能:撤销上一笔绘制
    • 保存功能:将签名保存为 PNG 图片
  3. 自定义选项

    • 可自定义签名区域的宽度和高度
    • 可调整线条颜色和粗细
    • 可设置背景颜色
    • 可自定义标题和副标题
  4. 状态反馈

    • 空状态提示:未签名时显示提示信息
    • 按钮状态管理:未签名时禁用保存和清除按钮
    • 签名预览:保存后显示签名预览

使用方法

  1. 首先确保项目中已引入 Font Awesome 图标库,用于显示按钮图标

  2. 在你的 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>

扩展建议

  1. 可以添加更多自定义选项,如支持多种线条样式、签名颜色选择器等
  2. 可以增加签名验证功能,判断签名是否有效(如是否过于简单)
  3. 可以添加保存为不同格式的选项(如 JPG、SVG)
  4. 可以实现签名的缩放和移动功能,方便在小屏幕上进行签名

这个组件轻量级且易于集成,适合在需要电子签名的场景中使用,如合同签署、表单确认等。

分享

在 Vue 中实现手动签名功能时,如何处理签名数据的存储和传输?

除了上述方法,还有哪些其他方式可以在 Vue 中实现手动签名功能?

如何在 Vue 中实现签名的验证和识别?

Vue.js项目中,通过企业微信手动授权通常是借助微信JS-SDK来完成的。以下是一个基本的步骤示例: 首先,在HTML模板里引入微信JS-SDK: ```html <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js" async></script> ``` 然后在Vue组件中,你可以创建一个方法处理授权请求: ```javascript export default { data() { return { wechatAuthCode: '', }; }, methods: { getWechatAuth() { // 获取用户点击授权后的code wx.config({ debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的jsApiList参数,可以在pc端打开,参数会在log下显示 appId: 'your_appid', // 必填,公众号的唯一标识 timestamp: '', // 必填,生成签名的时间戳 nonceStr: '', // 必填,生成签名的随机串 signature: '', // 必填,签名 jsApiList: ['checkJsApi'], // 必填,需要使用的JS接口列表 }); wx.ready(function () { // 用户授权成功后,会在这里触发 wx.checkJsApi({ jsApiList: ['snsapi_userinfo'], // 需要检测的JS接口列表 success: function(res) { if (res.checkResult.snsapi_userinfo) { // 可以使用wx.getUserInfo获取用户信息 wx.getUserInfo({ success: function(userInfo) { this.wechatAuthCode = userInfo.openId; // 存储openId用于后续服务器验证 console.log('授权成功,用户信息:', userInfo); } }); } else { alert('当前功能不支持'); } }, fail: function(err) { console.error('检查JSAPI失败:', err); } }); }); wx.error(function(res) { // 异步失败 console.error('config error:', res); }); }, } }; ``` 请注意,你需要替换`your_appid`为你在微信开发者平台申请的小程序的AppID,并根据实际授权流程填充相应的timestamp、nonceStr和signature。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值