开源分享: 基于vue3的电子签名组件

开发 开发工具
今天又到了分享时间. 之前和大家分享我开源的轻量级电子签名组件——react-sign2. 今天继续和大家分享一下小伙伴极客恰恰 贡献的vue3版电子签名组件vue3-sign.

图片

github地址: https://github.com/open-vue3/vue3-sign

hello, 大家好, 我是徐小夕, 今天又到了分享时间. 之前和大家分享我开源的轻量级电子签名组件——react-sign2. 今天继续和大家分享一下小伙伴极客恰恰​ 贡献的vue3​版电子签名组件vue3-sign.

图片

我们可以使用它轻松的实现电子签名, 比如说常用的合同签字, 文稿签名, 艺术签名等, 并支持一键将签名保存为本地图片.

基本属性介绍

图片

事件

图片

实现思路

按照笔者之前的习惯, 在设计组件之前都会先明确组件的设计需求, 然后根据健壮组件的设计原则来落地组件, 这里给大家分享一下我总结的几条组件设计经验:

  • 对组件进行严格的属性设计, 保证业务层能低成本使用组件, 并保持一定的可配性
  • 组件内外部类型约定(ts规范), 并提供对逻辑的兼容性
  • 可读性(代码格式统一清晰,注释完整,代码结构层次分明,编程范式使用得当)
  • 可用性(代码功能完整,在不同场景都能很好兼容,业务逻辑覆盖率)
  • 复用性(代码可以很好的被其他业务模块复用)
  • 可维护性(代码易于维护和扩展,并有一定的向下/向上兼容性)
  • 高性能(组件具有一定的性能, 如复杂场景的渲染, 计算等)

对于电子签名组件, 我们最小化的需求就是能满足用户的线上签名, 并能保存签名数据.

图片

实现代码

由于代码使用vue3​实现, 这里主要分校一下核心js​实现, 详细代码可以参考 github : https://github.com/open-vue3/vue3-sign.

<script lang="ts" setup>
import { ref, watch, onMounted,onUnmounted } from "vue";
interface IProps {
    /**
     * @description   画布宽度
     * @default       400
     */
     width?: number;
     /**
      * @description   画布高度
      * @default       200
      */
     height?: number;
     /**
      * @description   线宽
      * @default       4
     */
     lineWidth?: number;
     /**
      * @description   线段颜色
      * @default       'red'
     */
     strokeColor?: string;
     /**
      * @description   设置线条两端圆角
      * @default       'round'
     */
     lineCap?: string;
     /**
      * @description   线条交汇处圆角
      * @default       'round'
     */
     lineJoin?: string;
     /**
      * @description   画布背景颜色
      * @default       'transparent'
     */
     bgColor?: string;
     /**
      * @description   true
     */
     showBtn?: boolean;
     /**
     * @description   当保存时的回调, blob为生成的图片bob
     * @default       -
     */
     onSave?: (blob: Blob) => void;
    /**
     * @description   当画布清空时的回调, 参数为画布的上下文对象,可以直接使用canvas的api
     * @default       -
     */
     onClear?: (canvasContext: CanvasRenderingContext2D) => void;
     /**
     * @description   当画布结束时的回调
     * @default       -
     */
     onDrawEnd?: (canvas: HTMLCanvasElement) => void;
  }

const props = withDefaults(defineProps<IProps>(), {
  width: 400,
  height: 200,
  lineWidth:4,
  strokeColor:'green',
  lineCap:'round',
  lineJoin:'round',
  bgColor:'transparent',
  showBtn:true
});

const {
  width,
  height,
  lineWidth,
  strokeColor,
  lineCap,
  lineJoin,
  bgColor,
  showBtn,
  onSave,
  onClear,
  onDrawEnd
} = props;

   const canvasRef = ref<any>(null);
    const ctxRef = ref<any>(null);

   // 保存上次绘制的 坐标及偏移量
   const client = ref<any>({
              offsetX: 0, // 偏移量
              offsetY: 0,
              endX: 0, // 坐标
              endY: 0
          })
  

 // 判断是否为移动端
 const mobileStatus = (/Mobile|Android|iPhone/i.test(navigator.userAgent));

   // 取消-清空画布
   const cancel = () => {
    // 清空当前画布上的所有绘制内容
    if(canvasRef.value) {
      const canvasCtx = canvasRef.value.getContext("2d");
      canvasCtx.clearRect(0, 0, width, height);
      
      onClear && onClear(canvasRef.value)
    }
  }

  // 保存-将画布内容保存为图片
  const save = () => {
    // 将canvas上的内容转成blob流
    canvasRef.value.toBlob((blob: any) => {
        // 获取当前时间并转成字符串,用来当做文件名
        const date = Date.now().toString()
        // 创建一个 a 标签
        const a = document.createElement('a')
        // 设置 a 标签的下载文件名
        a.download = `${date}.png`
        // 设置 a 标签的跳转路径为 文件流地址
        a.href = URL.createObjectURL(blob)
        // 手动触发 a 标签的点击事件
        a.click()
        // 移除 a 标签
        a.remove()

        onSave && onSave(blob);
    })
  }

   // 绘制
   const draw = (event: { changedTouches?: any; pageX?: any; pageY?: any; }) => {
        // 获取当前坐标点位
        const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event
        // 获取canvas 实例
        const canvas:HTMLCanvasElement = canvasRef.value as any;
        
        const { x, y } = canvas.getBoundingClientRect();
        // 修改最后一次绘制的坐标点
        client.value.endX = pageX
        client.value.endY = pageY
        // 根据坐标点位移动添加线条
        ctxRef.value.lineTo(pageX - x, pageY - y)

        // 绘制
        ctxRef.value .stroke()
    };

   // 初始化
   const init = (event: { changedTouches?: any; offsetX?: any; offsetY?: any; pageX?: any; pageY?: any; }) => {
        // 获取偏移量及坐标
        const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event;
        const canvas:HTMLCanvasElement = canvasRef.value as any;

        const { x, y } = canvas.getBoundingClientRect();
     


        client.value.offsetX = offsetX
        client.value.offsetY = offsetY
        client.value.endX = pageX
        client.value.endY = pageY

        // 清除以上一次 beginPath 之后的所有路径,进行绘制
        ctxRef.value.beginPath()
        // 根据配置文件设置相应配置
        ctxRef.value.lineWidth = lineWidth
        ctxRef.value.strokeStyle = strokeColor
        ctxRef.value.lineCap = lineCap
        ctxRef.value.lineJoin = lineJoin
        // 设置画线起始点位
        ctxRef.value.moveTo(client.value.endX - x, client.value.endY - y)
        // 监听 鼠标移动或手势移动
        window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw)
    };
  // 结束绘制
  const closeDraw = () => {
         console.log(ctxRef.value);
        // 结束绘制
        ctxRef.value.closePath()
        // 移除鼠标移动或手势移动监听器
        window.removeEventListener("mousemove", draw)
        onDrawEnd && onDrawEnd(canvasRef.current)
    };
  const initCanvas =()=>{
       // 获取canvas 实例
       const canvas:HTMLCanvasElement = canvasRef.value as any;
          // 设置宽高
          canvas.width = width;
          canvas.height = height;
          // 创建上下文
          const ctx:any = canvas.getContext('2d');
          ctxRef.value = ctx;
          // 设置填充背景色
          ctxRef.value.fillStyle = bgColor;
          // 绘制填充矩形
          ctxRef.value.fillRect(
              0, // x 轴起始绘制位置
              0, // y 轴起始绘制位置
              width, // 宽度
              height // 高度
          );
  }
  const  addEventListener=()=>{
     // 创建鼠标/手势按下监听器
     window.addEventListener(mobileStatus ? "touchstart" : "mousedown", init);
      // 创建鼠标/手势 弹起/离开 监听器
    window.addEventListener(mobileStatus ? "touchend" : "mouseup", closeDraw);
    
  }
  const  removeEventListener=()=>{
     // 创建鼠标/手势按下监听器
     window.removeEventListener(mobileStatus ? "touchstart" : "mousedown", init);
      // 创建鼠标/手势 弹起/离开 监听器
    window.removeEventListener(mobileStatus ? "touchend" : "mouseup", closeDraw);
    
  }
  
const initEsign=()=>{
     initCanvas();
     addEventListener();
    
  }

  onMounted(() => {
    initEsign();
});

onUnmounted(()=>{
  removeEventListener();
});

</script>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.

后期规划

图片

欢迎大家共建.

参考资料

https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API

https://juejin.cn/post/7174251833773752350

责任编辑:武晓燕 来源: 趣谈前端
相关推荐

2023-11-24 08:02:28

2020-12-01 08:34:31

Vue3组件实践

2021-12-01 08:11:44

Vue3 插件Vue应用

2021-05-18 07:51:37

Suspense组件Vue3

2022-08-10 10:57:35

Vue3开发插件

2021-11-30 08:19:43

Vue3 插件Vue应用

2023-11-28 09:03:59

Vue.jsJavaScript

2023-04-27 11:07:24

Setup语法糖Vue3

2022-07-29 11:03:47

VueUni-app

2022-09-20 11:00:14

Vue3滚动组件

2022-12-16 17:09:57

2021-12-02 05:50:35

Vue3 插件Vue应用

2024-08-13 09:26:07

2021-11-19 09:29:25

项目技术开发

2022-05-09 11:19:12

CSS函数开源

2022-06-15 11:51:14

Vue3开发避坑

2020-09-19 21:15:26

Composition

2021-03-22 10:05:25

开源技术 项目

2022-07-27 08:40:06

父子组件VUE3

2020-11-09 09:23:43

Vue组织架构
点赞
收藏

51CTO技术栈公众号