VUE 实现滑块验证 ①

@作者 : SYFStrive

 
VUE 实现滑块验证 ①_第1张图片

@博客首页 : HomePage

微信小程序

个人社区(欢迎大佬们加入)社区链接

觉得文章不错可以点点关注专栏连接

感谢支持,学累了可以先看小段由小胖给大家带来的街舞

请添加图片描述

相关专栏

VUE专栏()

目录

  • V u e j s Vuejs Vuejs
  • 滑块图示
  • 结构框架
  •   Html 结构
  •   Css 结构
  •   JS 结构
  •   完整代码
  •   实现效果
  • 总结

                    ⡖⠒⠒⠒⠤⢄⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸   ⠀⠀⠀⡼⠀⠀⠀⠀ ⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢶⣲⡴⣗⣲⡦⢤⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠋⠉⠉⠓⠛⠿⢷⣶⣦⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠇⠀⠀⠀⠀⠀⠀⠘⡇⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡞⠀⠀⠀⠀⠀⠀⠀⢰⠇⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⡴⠊⠉⠳⡄⠀⢀⣀⣀⡀⠀⣸⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠰⠆⣿⡞⠉⠀⠀⠉⠲⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⢧⡀⣀⡴⠛⡇⠀⠈⠃⠀⠀⡗⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣱⠃⡴⠙⠢⠤⣀⠤⡾⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⡇⣇⡼⠁⠀⠀⠀⠀⢰⠃⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣸⢠⣉⣀⡴⠙⠀⠀⠀⣼⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⡏⠀⠈⠁⠀⠀⠀⠀⢀⡇⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠀⠀⠀⠀⠀⠀⡼⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⣰⠃⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣀⠤⠚⣶⡀⢠⠄⡰⠃⣠⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢀⣠⠔⣋⣷⣠⡞⠀⠉⠙⠛⠋⢩⡀⠈⠳⣄⠀⠀⠀⠀⠀⠀⠀
⠀⡏⢴⠋⠁⠀⣸⠁⠀⠀⠀⠀⠀ ⠀⣹⢦⣶⡛⠳⣄⠀⠀⠀⠀⠀
⠀⠙⣌⠳⣄⠀⡇   不能   ⡏⠀⠀  ⠈⠳⡌⣦⠀⠀⠀⠀
⠀⠀⠈⢳⣈⣻⡇   白嫖 ⢰⣇⣀⡠⠴⢊⡡⠋⠀⠀⠀⠀
⠀⠀⠀⠀⠳⢿⡇⠀⠀⠀⠀⠀⠀⢸⣻⣶⡶⠊⠁⠀⠀
⠀⠀⠀⠀⠀⢠⠟⠙⠓⠒⠒⠒⠒⢾⡛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⠏⠀⣸⠏⠉⠉⠳⣄⠀⠙⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⡰⠃⠀⡴⠃⠀⠀⠀⠀⠈⢦⡀⠈⠳⡄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣸⠳⣤⠎⠀⠀⠀⠀⠀⠀⠀⠀⠙⢄⡤⢯⡀⠀⠀⠀⠀⠀⠀
⠀⠐⡇⠸⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⡆⢳⠀⠀⠀⠀⠀⠀
⠀⠀⠹⡄⠹⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣇⠸⡆⠀⠀⠀⠀⠀
⠀⠀⠀⠹⡄⢳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡀⣧⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢹⡤⠳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣷⠚⣆⠀⠀⠀⠀
⠀⠀⠀⡠⠊⠉⠉⢹⡀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡎⠉⠀⠙⢦⡀⠀
⠀⠀⠾⠤⠤⠶⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠒⠲⠤⠽   

提示:以下是本篇文章正文内容

V u e j s Vuejs Vuejs


简介 : Vue 是一套用于构建用户界面的 渐进式 框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

Canvas参考链接 :https://blog.csdn.net/u01246837

滑块图示

VUE 实现滑块验证 ①_第2张图片

VUE 实现滑块验证 ①_第3张图片

结构框架

  Html 结构

 <!--滑块验证模块包裹-->
  <div class="slide-authCode-wrap" v-show="isOpen">
    <!--底下小箭头-->
    <div class="arrow"></div>
    <!--关闭按钮-->
    <div class="close" @click="Close">
      <span class="iconfont icon-chacha"></span>
    </div>

    <!--滑块主要容器-->
    <div class="validate-wrap">
      <!--header 头部部分-->
      <div class="refresh">
        <div class="refresh-text">完成拼图验证</div>
        <!--刷新数据按钮-->
        <div class="refresh-icon" @click="Refresh">
          <!--刷新按钮Icon-->
          <span class="icon iconfont icon-gengxin" ref="iconRotate"></span>
          <span>换一张</span>
        </div>
      </div>
      <!--滑块区域-->
      <div class="slider-main-container">
        <!-- 画布容器Box -->
        <div id="captcha" ref="captcha" style="position: relative">
          <!-- 画布bg -->
          <canvas ref="canvas_bg" width="364" height="142"
          >浏览器版本过低,请升级浏览器
          </canvas
          >
          <!-- 滑块box -->
          <canvas ref="blockBox" width="364" height="142" class="block"
          >浏览器版本过低,请升级浏览器
          </canvas
          >
          <!--用来加载图片标签 不显示-->
          <img ref="img" src="" style="display: none" width="0" height="0"/>
          <!-- <img
            ref="img"
            src="./images/722-300x150.jpg"
            width="0"
            height="0"
            style="display: none"
          /> -->
          <!-- 拖动容器Box -->
          <div
              class="slider-container"
              :class="slideVerifyStatus === 4 ? 'slider-container-fail' : ''"
          >
            <div class="slide-bg">
              <div class="left"></div>
              <div class="center">拖动滑块完成拼图,进行账号验证</div>
              <div class="right"></div>
            </div>
            <!-- 拖动遮罩 -->
            <div ref="slider_mask" class="slider-mask">
              <!-- 拖动块 -->
              <div
                  ref="slider"
                  class="slider"
                  @mousedown="SliderMousedownEvent"
              >
                <!-- 拖动Icon -->
                <span
                    class="slider-icon iconfont"
                    :class="[
                    slideVerifyStatus === 0 && 'icon-tubiao-xiaoshou',
                    slideVerifyStatus === 2 && 'icon-gouxuan',
                    slideVerifyStatus === 4 && 'icon-close',
                  ]"
                >
                </span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  Css 结构


<style scoped lang="less">
//滑块验证
.slide-authCode-wrap {
  position: absolute;
  left: 0;
  z-index: 110;
  bottom: 65px;
  width: 364px;
  height: 216.5px;
  padding: 12px 12px 12px 20px;
  border: 1px solid #eee;
  box-shadow: 0 0 2px 2px #eee;
  background-color: #fff;

  //关闭验证
  .close {
    cursor: pointer;
    z-index: 100;
    position: absolute;
    right: 10px;
    top: 10px;
    display: block;
    width: 20px;
    height: 20px;
    line-height: 20px;

    span {
      font-size: 20px;

      &:active {
        color: #a4a4a4;
      }
    }
  }

  //箭头
  .arrow {
    display: block;
    position: absolute;
    background-image: url("./images/tips.gif");
    background-repeat: no-repeat;
    width: 16px;
    height: 8px;
    background-position: 0 -8px;
    overflow: hidden;
    bottom: -8px;
    left: 190px;
  }

  //滑块主要容器
  .validate-wrap {
    //提醒区域
    .refresh {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-right: 30px;
      font-family: Helvetica, Tahoma, Arial, "Microsoft YaHei", "微软雅黑",
      sans-serif;

      .refresh-text {
        font-size: 15px;
        color: #666;
      }

      .refresh-icon {
        cursor: pointer;
        color: #06c;

        .icon {
          display: inline-block;
          margin-right: 4px;
          vertical-align: revert;
          transition: 0.6s linear;
        }

        span {
          font-size: 15px;
        }
      }
    }

    //滑块组要容器
    .slider-main-container {
      margin-top: 5px;
      //画布容器Box
      #captcha {
        display: flex;
        justify-content: center;
        flex-direction: column;
        /* 小拼图 */

        .block {
          position: absolute;
          left: 0;
          top: 0;
        }

        /* 滑动条 */

        .slider-container {
          position: relative;
          margin: 10px auto 0;
          opacity: 1;
          font-size: 14px;
          visibility: visible;
          width: 364px;
          height: 40px;
          line-height: 40px;
          text-align: center;
          color: #05a4ea;

          //滑块Bg
          .slide-bg {
            .left {
              float: left;
              width: 40px;
              height: 40px;
              background: url("./images/slide-left-icon2.png") no-repeat;
            }

            .center {
              background-image: url("./images/slide-center-bg.png");
              margin-left: 40px;
              margin-right: 40px;
              overflow: hidden;
              white-space: nowrap;
              user-select: none;
              -moz-user-select: none;
              -ms-user-select: none;
              -webkit-user-select: none;
            }

            .right {
              width: 40px;
              height: 40px;
              background: url("./images/slide-right-icon2.png") no-repeat;
              position: absolute;
              right: 0;
              top: 0;
            }
          }

          /* 拖动遮罩容器 */

          .slider-mask {
            position: absolute;
            top: 0;
            left: 0;
            width: 364px;
            height: 40px;
            border-radius: 36px;
            border: 0px solid #1991fa;
            background: linear-gradient(#33b5fb, #8fdfff);
          }

          /* 拖动块 */

          .slider {
            position: absolute;
            left: -3px;
            top: -3px;
            width: 45px;
            height: 45px;
            background: #fff;
            box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
            cursor: pointer;
            transition: background 0.2s linear;
            border-radius: 50%;
          }

          .slider:hover {
            background: #aed6ff;
            color: #06c;
          }

          /* 拖动Icon */

          .slider-icon {
            font-size: 25px;
            font-weight: 700;
            vertical-align: middle;
          }
        }

        //活动状态CSS

        /* 滑动条失败态 */

        .slider-container-fail {
          .slider-mask {
            background: linear-gradient(#ff5e5e, #ffb3b3);
          }

          .slider {
            padding-top: 2px;
            box-sizing: border-box;
          }

          .slider-icon {
            color: red;
          }
        }
      }
    }
  }
}
</style>

  JS 结构

<script>
import {
  computed,
  getCurrentInstance,
  onBeforeUnmount,
  onMounted,
  reactive,
  ref,
  toRefs,
} from "vue";
//第三方模块
import throttle from "lodash/throttle"; // 引入节流会防抖插件
import { ElMessage } from "element-plus";
//自定义模块
import { mapActionsFun } from "@/hooks/VueX";
//获取两个值之间的随机数
import { GetRandomNumberByRange } from "@/utils/Random";
// //回去两个值之间的随机数
// function GetRandomNumberByRange(start, end) {
//     return Math.round(Math.random() * (end - start) + start);
// }
import GetGlobalData from "@/utils/Global/GetGlobalData";

export default {
  name: "SliderVerification",
  emits: ["successCallback", "failCallback"],
  setup(props, context) {
    const data = reactive({
      ll: 0,
      slideVerifyStatus: 0, //控制滑块的状态Icon
      isOpen: false,
    });

    const slidingData = {
      l: 35, //去掉突出的部分滑块总边长
      r: 7.5, //滑块突出的小圆圈半径
      w: 364, //canvas宽度
      h: 142, //canvas高度
      PI: Math.PI, //2PI = 360
      ll: 0, //滑块的实际边长(包括突出部分)
    };
    data.ll = computed(() => {
      return slidingData.l + slidingData.r * 2; //滑块的实际边长(包括突出部分)
    });
    const thisData = {
      x: 0,
      y: 0,
      captcha: null,
      canvas_bg: null,
      blockBox: null,
      img: null,
      slider_mask: null,
      slider: null,
      url: "",
      iconRotate: null,
      currentGlobalData: null,
    };
    const ctx = {
      canvasCtx: null,
      blockBoxCtx: null,
    };
    const methods = {
      Refresh: null,
      close: null,
      SliderMousedownEvent: null,
    };
    const eventData = {
      originX: 0,
      originY: 0,
      trail: [],
      isMouseDown: false,
    };

    //#region 生命周期
    const currentInstance = getCurrentInstance();
    data.currentGlobalData = GetGlobalData();
    onMounted(async () => {
      await GetElement();
      await EventGlobal();
    });

    onBeforeUnmount(() => {
      data.currentGlobalData.$bus.all.delete("openSlideVerify");
    });

    //#endregion

    //#region 封装方法
    const GetData = throttle(async () => {
      try {
        // Ajax请求 获取图片
        thisData.url = (
          await mapActionsFun(["getVerifySlideImg"]).getVerifySlideImg()
        ).url;
        thisData.img.src = thisData.url;
        await DrawInitialize();
        await InitImg();
      } catch (e) {
        ElMessage({
          showClose: true,
          dangerouslyUseHTMLString: true,
          type: "error",
          message: e,
          duration: 1500,
        });
      }
    }, 1500);

    // 获取需要的元素
    const GetElement = async () => {
      thisData.captcha = currentInstance.proxy.$refs.captcha;
      thisData.canvas_bg = currentInstance.proxy.$refs.canvas_bg;
      thisData.blockBox = currentInstance.proxy.$refs.blockBox;
      thisData.img = currentInstance.proxy.$refs.img;
      thisData.slider_mask = currentInstance.proxy.$refs.slider_mask;
      thisData.slider = currentInstance.proxy.$refs.slider;
      thisData.iconRotate = currentInstance.proxy.$refs.iconRotate;
      // alpha(boolean):表示canvas是否包含一个alpha通道,设为false则浏览器知道背景永远不透明,能加速对于透明场景和图像的绘制。
      // willReadFrequently(Boolean):表示是否计划有大量的回读操作,频繁调用getImageData()方法时能节省内存,仅Gecko内核浏览器支持。
      // storage(String):声明使用的storage类型,默认为”persistent”。
      ctx.canvasCtx = thisData.canvas_bg.getContext("2d", {
        willReadFrequently: true,
      });
      ctx.blockBoxCtx = thisData.blockBox.getContext("2d", {
        willReadFrequently: true,
      });
    };
    // 绘画方法
    const DrawInitialize = () => {
      thisData.x = GetRandomNumberByRange(
        data.ll + 10,
        slidingData.w - (data.ll + 10)
      );
      thisData.y = GetRandomNumberByRange(
        slidingData.r * 2 + 10,
        slidingData.h - (data.ll + 10)
      );
      // fill 通过填充路径的内容区域生成实心的图形
      // clip 把已经创建的路径转换成裁剪路径。裁剪路径的作用是遮罩。只显示裁剪路径内的区域,裁剪路径外的区域会被隐藏。
      // 注意:clip()只能遮罩在这个方法调用之后绘制的图像,如果是clip()方法调用之前绘制的图像,则无法实现遮罩。
      Draw(ctx.canvasCtx, "fill", thisData.x, thisData.y);
      Draw(ctx.blockBoxCtx, "clip", thisData.x, thisData.y);
    };

    // 绘制  2D渲染,渲染方式,坐标(X,Y)
    function Draw(ctx, operation, x, y) {
      // ★前提要创建Canvas 否者无法绘画
      // 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径
      ctx.beginPath();
      // 把画笔移动到指定的坐标(x, y)。相当于设置路径的起始点坐标。
      ctx.moveTo(x, y);
      //(绘制一条从当前位置到指定坐标x,y)的直线. 假如 x 60~304 y 25~82
      ctx.lineTo(x + slidingData.l / 2, y);
      // 绘制圆弧 x, y, r, startAngle, endAngle, anticlockwise 六个参数
      // 以(x, y)为圆心,以r为半径,从 startAngle弧度开始到endAngle弧度结束。anticlosewise是布尔值,true表示逆时针,false表示顺时针。(默认是顺时针)
      // 注意1 这里的度数都是弧度  0弧度是指的x轴正方形
      // 注意2 arc绘制的坐标是从最开始的位置计算的
      ctx.arc(
        x + slidingData.l / 2,
        y - slidingData.r,
        slidingData.r,
        0,
        2 * slidingData.PI
      );
      // 回到原来起步画圆的位置
      ctx.lineTo(x + slidingData.l / 2, y);
      // 半径 直径是指 L
      // 向右再走 直径 位置
      ctx.lineTo(x + slidingData.l, y);
      // 向右再走 半径 位置
      ctx.lineTo(x + slidingData.l, y + slidingData.l / 2);
      // arc方法 须从最开始的位置计算 走到最右边 再向右走半径位置 向下走半径位置
      ctx.arc(
        x + slidingData.l + slidingData.r,
        y + slidingData.l / 2,
        slidingData.r,
        0,
        2 * slidingData.PI
      );
      ctx.lineTo(x + slidingData.l, y + slidingData.l / 2);
      ctx.lineTo(x + slidingData.l, y + slidingData.l);
      ctx.lineTo(x, y + slidingData.l);
      ctx.lineTo(x, y);
      ctx.fillStyle = "#fff";
      ctx[operation]();
      ctx.beginPath();
      ctx.arc(
        x,
        y + slidingData.l / 2,
        slidingData.r,
        1.5 * slidingData.PI,
        0.5 * slidingData.PI
      );
      // 合成 xor属性作用:重叠部分变透明
      ctx.globalCompositeOperation = "xor";
      ctx.fill();
    }

    // 初始化图像
    const InitImg = () => {
      ctx.canvasCtx.drawImage(thisData.img, 0, 0, slidingData.w, slidingData.h);
      ctx.blockBoxCtx.drawImage(
        thisData.img,
        0,
        0,
        slidingData.w,
        slidingData.h
      );
      const y = thisData.y - slidingData.r * 2; //减去突出圆的大小
      // 参数 获取那块区域坐标x,y 宽
      const imageData = ctx.blockBoxCtx.getImageData(
        thisData.x,
        y,
        data.ll,
        data.ll
      );
      thisData.blockBox.width = data.ll;
      // 后面两个参数从哪里放上去
      ctx.blockBoxCtx.putImageData(imageData, 0, y);
    };

    // 绑定事件 刷新
    let index = ref(0);
    methods.Refresh = throttle(() => {
      index.value++;
      thisData.iconRotate.style.rotate = -360 * index.value + "deg";
      Reset();
    }, 2000);
    // 关闭滑块验证
    methods.Close = () => {
      data.isOpen = false;
    };

    // ----------------按着---------------
    methods.SliderMousedownEvent = (e) => {
      eventData.originX = e.x;
      eventData.originY = e.y;
      eventData.isMouseDown = true;
    };
    // ----------------拖动---------------
    document.addEventListener("mousemove", (e) => {
      if (!eventData.isMouseDown) return false;
      const moveY = e.y - eventData.originY;
      const moveX = e.x - eventData.originX;
      // 判断时候超出或者小于0
      if (moveX < 0 || moveX + 40 >= slidingData.w) return false;
      // 拖动按钮位置
      thisData.slider.style.left = moveX + "px";
      // 拖动填充滑块位置
      const blockLeft = moveX;
      thisData.blockBox.style.left = blockLeft + "px";
      // 遮罩宽长度
      thisData.slider_mask.style.width = moveX + 40 + "px";
      // 添加位置
      eventData.trail.push(moveY);
    });
    // ----------------抬起---------------
    document.addEventListener("mouseup", () => {
      if (!eventData.isMouseDown) return false;
      else eventData.isMouseDown = false;
      // 验证位置;
      const spliced = Verify();
      if (spliced) {
        // 添加成功
        data.slideVerifyStatus = 2;
        SuccessCallback();
        methods.Close();
      } else {
        // 添加失败样式
        data.slideVerifyStatus = 4;
        FailCallback();
        setTimeout(() => {
          Reset();
        }, 1000);
      }
    });

    // 清除
    function CleanCtx() {
      ctx.canvasCtx.clearRect(0, 0, slidingData.w, slidingData.h);
      ctx.blockBoxCtx.clearRect(0, 0, slidingData.w, slidingData.h);
      thisData.blockBox.width = slidingData.w;
    }

    // 重置
    async function Reset() {
      data.slideVerifyStatus = 0;
      thisData.slider.style.left = 0;
      thisData.blockBox.style.left = 0;
      thisData.slider_mask.style.width = 0;
      await CleanCtx();
      await GetData();
    }

    // 验证
    function Verify() {
      const left = parseInt(thisData.blockBox.style.left);
      return Math.abs(left - thisData.x) < 1; //10表示容错率,值越小,需要拼得越精确
    }

    //#endregion

    //#region 子父传递事件
    async function EventGlobal() {
      data.currentGlobalData.$bus.on("openSlideVerify", (boolValue) => {
        if (data.isOpen) return;
        data.isOpen = boolValue;
        Reset();
      });
    }

    // 成功总事件
    function SuccessCallback() {
      context.emit("successCallback");
    }
    // 失败总事件
    function FailCallback() {
      context.emit("failCallback");
    }

    //#endregion

    return {
      ...toRefs(data),
      ...methods,
    };
  },
};
</script>

  完整代码

<template>
  <!--滑块验证的包裹-->
  <div class="slide-authCode-wrap" v-show="isOpen">
    <!--箭头-->
    <div class="arrow"></div>
    <!--关闭-->
    <div class="close" @click="Close">
      <span class="iconfont icon-chacha"></span>
    </div>

    <!--滑块主要容器-->
    <div class="validate-wrap">
      <!--header 头部-->
      <div class="refresh">
        <div class="refresh-text">完成拼图验证</div>
        <!--刷新区域-->
        <div class="refresh-icon" @click="Refresh">
          <!--刷新按钮Icon-->
          <span class="icon iconfont icon-gengxin" ref="iconRotate"></span>
          <span>换一张</span>
        </div>
      </div>
      <!--滑块区域-->
      <div class="slider-main-container">
        <!-- 画布容器Box -->
        <div id="captcha" ref="captcha" style="position: relative">
          <!-- 画布bg -->
          <canvas ref="canvas_bg" width="364" height="142"
            >浏览器版本过低,请升级浏览器
          </canvas>
          <!-- 滑块box -->
          <canvas ref="blockBox" width="364" height="142" class="block"
            >浏览器版本过低,请升级浏览器
          </canvas>
          <!--显示的图片-->
          <img ref="img" src="" style="display: none" width="0" height="0" />
          <!-- <img
            ref="img"
            src="./images/722-300x150.jpg"
            width="0"
            height="0"
            style="display: none"
          /> -->
          <!-- 拖动容器Box -->
          <div
            class="slider-container"
            :class="slideVerifyStatus === 4 ? 'slider-container-fail' : ''"
          >
            <div class="slide-bg">
              <div class="left"></div>
              <div class="center">拖动滑块完成拼图,进行账号验证</div>
              <div class="right"></div>
            </div>
            <!-- 拖动遮罩 -->
            <div ref="slider_mask" class="slider-mask">
              <!-- 拖动块 -->
              <div
                ref="slider"
                class="slider"
                @mousedown="SliderMousedownEvent"
              >
                <!-- 拖动Icon -->
                <span
                  class="slider-icon iconfont"
                  :class="[
                    slideVerifyStatus === 0 && 'icon-tubiao-xiaoshou',
                    slideVerifyStatus === 2 && 'icon-gouxuan',
                    slideVerifyStatus === 4 && 'icon-close',
                  ]"
                >
                </span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import {
  computed,
  getCurrentInstance,
  onBeforeUnmount,
  onMounted,
  reactive,
  ref,
  toRefs,
} from "vue";
//第三方模块
import throttle from "lodash/throttle"; // 引入节流会防抖插件
import { ElMessage } from "element-plus";
//自定义模块
import { mapActionsFun } from "@/hooks/VueX";
//获取两个值之间的随机数
import { GetRandomNumberByRange } from "@/utils/Random";
// //回去两个值之间的随机数
// function GetRandomNumberByRange(start, end) {
//     return Math.round(Math.random() * (end - start) + start);
// }
import GetGlobalData from "@/utils/Global/GetGlobalData";

export default {
  name: "SliderVerification",
  emits: ["successCallback", "failCallback"],
  setup(props, context) {
    const data = reactive({
      ll: 0,
      slideVerifyStatus: 0, //控制滑块的状态Icon
      isOpen: false,
    });

    const slidingData = {
      l: 35, //去掉突出的部分滑块总边长
      r: 7.5, //滑块突出的小圆圈半径
      w: 364, //canvas宽度
      h: 142, //canvas高度
      PI: Math.PI, //2PI = 360
      ll: 0, //滑块的实际边长(包括突出部分)
    };
    data.ll = computed(() => {
      return slidingData.l + slidingData.r * 2; //滑块的实际边长(包括突出部分)
    });
    const thisData = {
      x: 0,
      y: 0,
      captcha: null,
      canvas_bg: null,
      blockBox: null,
      img: null,
      slider_mask: null,
      slider: null,
      url: "",
      iconRotate: null,
      currentGlobalData: null,
    };
    const ctx = {
      canvasCtx: null,
      blockBoxCtx: null,
    };
    const methods = {
      Refresh: null,
      close: null,
      SliderMousedownEvent: null,
    };
    const eventData = {
      originX: 0,
      originY: 0,
      trail: [],
      isMouseDown: false,
    };

    //#region 生命周期
    const currentInstance = getCurrentInstance();
    data.currentGlobalData = GetGlobalData();
    onMounted(async () => {
      await GetElement();
      await EventGlobal();
    });

    onBeforeUnmount(() => {
      data.currentGlobalData.$bus.all.delete("openSlideVerify");
    });

    //#endregion

    //#region 封装方法
    const GetData = throttle(async () => {
      try {
        // Ajax请求 获取图片
        thisData.url = (
          await mapActionsFun(["getVerifySlideImg"]).getVerifySlideImg()
        ).url;
        thisData.img.src = thisData.url;
        await DrawInitialize();
        await InitImg();
      } catch (e) {
        ElMessage({
          showClose: true,
          dangerouslyUseHTMLString: true,
          type: "error",
          message: e,
          duration: 1500,
        });
      }
    }, 1500);

    // 获取需要的元素
    const GetElement = async () => {
      thisData.captcha = currentInstance.proxy.$refs.captcha;
      thisData.canvas_bg = currentInstance.proxy.$refs.canvas_bg;
      thisData.blockBox = currentInstance.proxy.$refs.blockBox;
      thisData.img = currentInstance.proxy.$refs.img;
      thisData.slider_mask = currentInstance.proxy.$refs.slider_mask;
      thisData.slider = currentInstance.proxy.$refs.slider;
      thisData.iconRotate = currentInstance.proxy.$refs.iconRotate;
      // alpha(boolean):表示canvas是否包含一个alpha通道,设为false则浏览器知道背景永远不透明,能加速对于透明场景和图像的绘制。
      // willReadFrequently(Boolean):表示是否计划有大量的回读操作,频繁调用getImageData()方法时能节省内存,仅Gecko内核浏览器支持。
      // storage(String):声明使用的storage类型,默认为”persistent”。
      ctx.canvasCtx = thisData.canvas_bg.getContext("2d", {
        willReadFrequently: true,
      });
      ctx.blockBoxCtx = thisData.blockBox.getContext("2d", {
        willReadFrequently: true,
      });
    };
    // 绘画方法
    const DrawInitialize = () => {
      thisData.x = GetRandomNumberByRange(
        data.ll + 10,
        slidingData.w - (data.ll + 10)
      );
      thisData.y = GetRandomNumberByRange(
        slidingData.r * 2 + 10,
        slidingData.h - (data.ll + 10)
      );
      // fill 通过填充路径的内容区域生成实心的图形
      // clip 把已经创建的路径转换成裁剪路径。裁剪路径的作用是遮罩。只显示裁剪路径内的区域,裁剪路径外的区域会被隐藏。
      // 注意:clip()只能遮罩在这个方法调用之后绘制的图像,如果是clip()方法调用之前绘制的图像,则无法实现遮罩。
      Draw(ctx.canvasCtx, "fill", thisData.x, thisData.y);
      Draw(ctx.blockBoxCtx, "clip", thisData.x, thisData.y);
    };

    // 绘制  2D渲染,渲染方式,坐标(X,Y)
    function Draw(ctx, operation, x, y) {
      // ★前提要创建Canvas 否者无法绘画
      // 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径
      ctx.beginPath();
      // 把画笔移动到指定的坐标(x, y)。相当于设置路径的起始点坐标。
      ctx.moveTo(x, y);
      //(绘制一条从当前位置到指定坐标x,y)的直线. 假如 x 60~304 y 25~82
      ctx.lineTo(x + slidingData.l / 2, y);
      // 绘制圆弧 x, y, r, startAngle, endAngle, anticlockwise 六个参数
      // 以(x, y)为圆心,以r为半径,从 startAngle弧度开始到endAngle弧度结束。anticlosewise是布尔值,true表示逆时针,false表示顺时针。(默认是顺时针)
      // 注意1 这里的度数都是弧度  0弧度是指的x轴正方形
      // 注意2 arc绘制的坐标是从最开始的位置计算的
      ctx.arc(
        x + slidingData.l / 2,
        y - slidingData.r,
        slidingData.r,
        0,
        2 * slidingData.PI
      );
      // 回到原来起步画圆的位置
      ctx.lineTo(x + slidingData.l / 2, y);
      // 半径 直径是指 L
      // 向右再走 直径 位置
      ctx.lineTo(x + slidingData.l, y);
      // 向右再走 半径 位置
      ctx.lineTo(x + slidingData.l, y + slidingData.l / 2);
      // arc方法 须从最开始的位置计算 走到最右边 再向右走半径位置 向下走半径位置
      ctx.arc(
        x + slidingData.l + slidingData.r,
        y + slidingData.l / 2,
        slidingData.r,
        0,
        2 * slidingData.PI
      );
      ctx.lineTo(x + slidingData.l, y + slidingData.l / 2);
      ctx.lineTo(x + slidingData.l, y + slidingData.l);
      ctx.lineTo(x, y + slidingData.l);
      ctx.lineTo(x, y);
      ctx.fillStyle = "#fff";
      ctx[operation]();
      ctx.beginPath();
      ctx.arc(
        x,
        y + slidingData.l / 2,
        slidingData.r,
        1.5 * slidingData.PI,
        0.5 * slidingData.PI
      );
      // 合成 xor属性作用:重叠部分变透明
      ctx.globalCompositeOperation = "xor";
      ctx.fill();
    }

    // 初始化图像
    const InitImg = () => {
      ctx.canvasCtx.drawImage(thisData.img, 0, 0, slidingData.w, slidingData.h);
      ctx.blockBoxCtx.drawImage(
        thisData.img,
        0,
        0,
        slidingData.w,
        slidingData.h
      );
      const y = thisData.y - slidingData.r * 2; //减去突出圆的大小
      // 参数 获取那块区域坐标x,y 宽
      const imageData = ctx.blockBoxCtx.getImageData(
        thisData.x,
        y,
        data.ll,
        data.ll
      );
      thisData.blockBox.width = data.ll;
      // 后面两个参数从哪里放上去
      ctx.blockBoxCtx.putImageData(imageData, 0, y);
    };

    // 绑定事件 刷新
    let index = ref(0);
    methods.Refresh = throttle(() => {
      index.value++;
      thisData.iconRotate.style.rotate = -360 * index.value + "deg";
      Reset();
    }, 2000);
    // 关闭滑块验证
    methods.Close = () => {
      data.isOpen = false;
    };

    // ----------------按着---------------
    methods.SliderMousedownEvent = (e) => {
      eventData.originX = e.x;
      eventData.originY = e.y;
      eventData.isMouseDown = true;
    };
    // ----------------拖动---------------
    document.addEventListener("mousemove", (e) => {
      if (!eventData.isMouseDown) return false;
      const moveY = e.y - eventData.originY;
      const moveX = e.x - eventData.originX;
      // 判断时候超出或者小于0
      if (moveX < 0 || moveX + 40 >= slidingData.w) return false;
      // 拖动按钮位置
      thisData.slider.style.left = moveX + "px";
      // 拖动填充滑块位置
      const blockLeft = moveX;
      thisData.blockBox.style.left = blockLeft + "px";
      // 遮罩宽长度
      thisData.slider_mask.style.width = moveX + 40 + "px";
      // 添加位置
      eventData.trail.push(moveY);
    });
    // ----------------抬起---------------
    document.addEventListener("mouseup", () => {
      if (!eventData.isMouseDown) return false;
      else eventData.isMouseDown = false;
      // 验证位置;
      const spliced = Verify();
      if (spliced) {
        // 添加成功
        data.slideVerifyStatus = 2;
        SuccessCallback();
        methods.Close();
      } else {
        // 添加失败样式
        data.slideVerifyStatus = 4;
        FailCallback();
        setTimeout(() => {
          Reset();
        }, 1000);
      }
    });

    // 清除
    function CleanCtx() {
      ctx.canvasCtx.clearRect(0, 0, slidingData.w, slidingData.h);
      ctx.blockBoxCtx.clearRect(0, 0, slidingData.w, slidingData.h);
      thisData.blockBox.width = slidingData.w;
    }

    // 重置
    async function Reset() {
      data.slideVerifyStatus = 0;
      thisData.slider.style.left = 0;
      thisData.blockBox.style.left = 0;
      thisData.slider_mask.style.width = 0;
      await CleanCtx();
      await GetData();
    }

    // 验证
    function Verify() {
      const left = parseInt(thisData.blockBox.style.left);
      return Math.abs(left - thisData.x) < 1; //10表示容错率,值越小,需要拼得越精确
    }

    //#endregion

    //#region 子父传递事件
    async function EventGlobal() {
      data.currentGlobalData.$bus.on("openSlideVerify", (boolValue) => {
        if (data.isOpen) return;
        data.isOpen = boolValue;
        Reset();
      });
    }

    // 成功总事件
    function SuccessCallback() {
      context.emit("successCallback");
    }
    // 失败总事件
    function FailCallback() {
      context.emit("failCallback");
    }

    //#endregion

    return {
      ...toRefs(data),
      ...methods,
    };
  },
};
</script>

<style scoped lang="less">
//滑块验证
.slide-authCode-wrap {
  position: absolute;
  left: 0;
  z-index: 110;
  bottom: 65px;
  width: 364px;
  height: 216.5px;
  padding: 12px 12px 12px 20px;
  border: 1px solid #eee;
  box-shadow: 0 0 2px 2px #eee;
  background-color: #fff;

  //关闭验证
  .close {
    cursor: pointer;
    z-index: 100;
    position: absolute;
    right: 10px;
    top: 10px;
    display: block;
    width: 20px;
    height: 20px;
    line-height: 20px;

    span {
      font-size: 20px;

      &:active {
        color: #a4a4a4;
      }
    }
  }

  //箭头
  .arrow {
    display: block;
    position: absolute;
    background-image: url("./images/tips.gif");
    background-repeat: no-repeat;
    width: 16px;
    height: 8px;
    background-position: 0 -8px;
    overflow: hidden;
    bottom: -8px;
    left: 190px;
  }

  //滑块主要容器
  .validate-wrap {
    //提醒区域
    .refresh {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-right: 30px;
      font-family: Helvetica, Tahoma, Arial, "Microsoft YaHei", "微软雅黑",
        sans-serif;

      .refresh-text {
        font-size: 15px;
        color: #666;
      }

      .refresh-icon {
        cursor: pointer;
        color: #06c;

        .icon {
          display: inline-block;
          margin-right: 4px;
          vertical-align: revert;
          transition: 0.6s linear;
        }

        span {
          font-size: 15px;
        }
      }
    }

    //滑块组要容器
    .slider-main-container {
      margin-top: 5px;
      //画布容器Box
      #captcha {
        display: flex;
        justify-content: center;
        flex-direction: column;
        /* 小拼图 */

        .block {
          position: absolute;
          left: 0;
          top: 0;
        }

        /* 滑动条 */

        .slider-container {
          position: relative;
          margin: 10px auto 0;
          opacity: 1;
          font-size: 14px;
          visibility: visible;
          width: 364px;
          height: 40px;
          line-height: 40px;
          text-align: center;
          color: #05a4ea;

          //滑块Bg
          .slide-bg {
            .left {
              float: left;
              width: 40px;
              height: 40px;
              background: url("./images/slide-left-icon2.png") no-repeat;
            }

            .center {
              background-image: url("./images/slide-center-bg.png");
              margin-left: 40px;
              margin-right: 40px;
              overflow: hidden;
              white-space: nowrap;
              user-select: none;
              -moz-user-select: none;
              -ms-user-select: none;
              -webkit-user-select: none;
            }

            .right {
              width: 40px;
              height: 40px;
              background: url("./images/slide-right-icon2.png") no-repeat;
              position: absolute;
              right: 0;
              top: 0;
            }
          }

          /* 拖动遮罩容器 */

          .slider-mask {
            position: absolute;
            top: 0;
            left: 0;
            width: 364px;
            height: 40px;
            border-radius: 36px;
            border: 0px solid #1991fa;
            background: linear-gradient(#33b5fb, #8fdfff);
          }

          /* 拖动块 */

          .slider {
            position: absolute;
            left: -3px;
            top: -3px;
            width: 45px;
            height: 45px;
            background: #fff;
            box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
            cursor: pointer;
            transition: background 0.2s linear;
            border-radius: 50%;
          }

          .slider:hover {
            background: #aed6ff;
            color: #06c;
          }

          /* 拖动Icon */

          .slider-icon {
            font-size: 25px;
            font-weight: 700;
            vertical-align: middle;
          }
        }

        //活动状态CSS

        /* 滑动条失败态 */

        .slider-container-fail {
          .slider-mask {
            background: linear-gradient(#ff5e5e, #ffb3b3);
          }

          .slider {
            padding-top: 2px;
            box-sizing: border-box;
          }

          .slider-icon {
            color: red;
          }
        }
      }
    }
  }
}
</style>

  实现效果

总结

以上是个人学习Vue的相关知识点,一点一滴的记录了下来,有问题请评论区指正,共同进步,这才是我写文章的原因之,如果这篇文章对您有帮助请三连支持一波

你可能感兴趣的:(VUE,vue.js,前端,javascript)