終於實現滑動拼圖驗證碼,vue

原因:以前看嗶哩嗶哩官網登陸的時候有一個拼圖驗證碼,很好奇怎麼去實現。而後就想着本身弄一個。先給你們看個人最終效果。後面再一點點拆解代碼。css


爲何想着寫這個功能呢,主要在於拼圖驗證碼在前端這裏會比較複雜而且深刻。相比文字拼寫,12306的圖片驗證碼都沒有拼圖驗證碼對前端的要求來的複雜,和難。html

我總結下知識點:前端

一、彈窗功能git

二、彈窗基於元素定位github

三、元素拖動canvas

四、canvas繪圖vim

五、基礎邏輯bash

1、彈窗和彈窗組件

抱歉,這裏我偷懶了直接用了elementUI的el-popover組件,因此小夥伴不懂的直接看elementUI官網的說明。dom

我我的也研究和編寫了這塊的組件功能(基於popper.js)ide

2、編寫基礎結構

這塊屬於html的基礎內容,也就標題黨了

3、canvas繪製圖片

一、canvas繪製外部img圖片

代碼:

let mainDom = document.querySelector("#codeImg");
let bg = mainDom.getContext("2d");
let width = mainDom.width;
let height = mainDom.height;

let blockDom = document.querySelector("#sliderBlock");
let block = blockDom.getContext("2d");
//從新賦值,讓canvas進行從新繪製
blockDom.height = height;
mainDom.height = height;

let imgsrc = require("../assets/images/back.jpg");
let img = document.createElement("img");
img.style.objectFit = "scale-down";
img.src = imgsrc;
img.onload = function() {
  bg.drawImage(img, 0, 0, width, height);
  block.drawImage(img, 0, 0, width, height);
};複製代碼

這裏我繪製了兩個canvas,由於一個是背景一個是滑塊

核心在於

let mainDom = document.querySelector("#codeImg");
let imgsrc = require("../assets/images/back.jpg");
let bg = mainDom.getContext("2d");
let img = document.createElement("img");
img.onload = function() {
    bg.drawImage(img, 0, 0, width, height);
};

複製代碼

二、canvas繪製滑塊部分

就是這個圖,這個有一些知識點,不難,可是很複雜。

具體請看:https://www.w3school.com.cn/tags/html_ref_canvas.asp


代碼部分:

drawBlock(ctx, xy = { x: 254, y: 109, r: 9 }, type) {
  let x = xy.x,
    y = xy.y,
    r = xy.r,
    w = 40;
  let PI = Math.PI;
  //繪製
  ctx.beginPath();
  //left
  ctx.moveTo(x, y);
  //top
  ctx.arc(x + (w + 5) / 2, y, r, -PI, 0, true);
  ctx.lineTo(x + w + 5, y);
  //right
  ctx.arc(x + w + 5, y + w / 2, r, 1.5 * PI, 0.5 * PI, false);
  ctx.lineTo(x + w + 5, y + w);
  //bottom
  ctx.arc(x + (w + 5) / 2, y + w, r, 0, PI, false);
  ctx.lineTo(x, y + w);
  ctx.arc(x, y + w / 2, r, 0.5 * PI, 1.5 * PI, true);
  ctx.lineTo(x, y);
  //修飾,沒有會看不出效果
  ctx.lineWidth = 1;
  ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
  ctx.strokeStyle = "rgba(255, 255, 255, 0.5)";
  ctx.stroke();
  ctx[type]();
  ctx.globalCompositeOperation = "xor";
}複製代碼

解釋下:

參數是傳入canvas對象

x,y軸數據

剪切仍是填充的canvas函數(fill,clip)

繪製難點:(很重要,否則你無法理解它怎麼繪製的)

繪製主要是須要理解這裏的繪製是根據你設置一個起始點座標,而後你繪製第二次的時候線就會鏈接到第二個點,依次鏈接最後回到原點就造成一個完整的圖形。

圓形的繪製:來自:https://www.w3school.com.cn/tags/canvas_arc.asp

用的是arc參數,主要是看這個圖


fill是用於填充繪製的部分,clip是裁剪出繪製的部分,利用這個就能夠出現一個扣掉的圖片和一個裁剪出來的圖片。

完成以後就是個人那個函數了。你們能夠直接拿去用。

三、讓元素跟隨鼠標點擊以後滑動

這裏其實原理很是簡單,就是有一個注意點。

原理:

鼠標點擊以後記錄當前座標,而後隨着(mousemove)滾動的時候修改元素的left和top值就好了。

還有一點就是鼠標快速滑動會致使丟失滑動效果,這裏須要用document,不能是元素級別的監聽。

元素上面我只須要鑑定按下mousedown

代碼:

//鼠標按下
drag(e) {
  console.log("鼠標按下", e);
  let dom = e.target; //dom元素
  let slider = document.querySelector("#sliderBlock"); //滑塊dom
  const downCoordinate = { x: e.x, y: e.y };

  //正確的滑塊數據
  let checkx = Number(this.slider.mx) - Number(this.slider.bx);
  //x軸數據
  let x = 0;
  const move = moveEV => {
    x = moveEV.x - downCoordinate.x;
    //y = moveEV.y - downCoordinate.y;
    if (x >= 251 || x <= 0) return false;
    dom.style.left = x + "px";
    //dom.style.top = y + "px";
    slider.style.left = x + "px";
  };

  const up = () => {
    document.removeEventListener("mousemove", move);
    document.removeEventListener("mouseup", up);
    dom.style.left = "";

    console.log(x, checkx);
    let max = checkx - 5;
    let min = checkx - 10;
    //容許正負偏差1
    if ((max >= x && x >= min) || x === checkx) {
      console.log("滑動解鎖成功");
      this.puzzle = true;
      this.tips = "驗證成功";
      setTimeout(() => {
        this.visible = false;
      }, 500);
    } else {
      console.log("拼圖位置不正確");
      this.tips = "驗證失敗,請重試";
      this.puzzle = false;
      this.canvasInit();
    }
  };

  document.addEventListener("mousemove", move);
  document.addEventListener("mouseup", up);
}複製代碼

四、總結

核心點比較多,寫過以後發現不難,關鍵在於去寫

我的該頁面git地址:https://github.com/ht-sauce/dream

該頁面處於項目的


路由訪問爲:http://localhost:8080/consumer

五、完整的頁面代碼

<template>
  <div id="login">
    <el-form class="loginFrom" :model="logindata" :rules="rules" ref="ruleForm">
      <el-form-item class="login-item">
        <h1 class="login-title">海天醬油登陸中心</h1>
      </el-form-item>
      <el-form-item prop="userName">
        <el-input
          class="login-inputorbuttom"
          prefix-icon="el-icon-user"
          placeholder="登陸名"
          v-model="logindata.userName"
        ></el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          class="login-inputorbuttom"
          prefix-icon="el-icon-lock"
          placeholder="密碼"
          v-model="logindata.password"
        ></el-input>
      </el-form-item>
      <!--<el-form-item prop="verificationCode">
        <el-input
          class="login-inputorbuttom"
          v-model="logindata.verificationCode"
        ></el-input>
      </el-form-item>-->
      <el-form-item class="login-item">
        <el-button
          class="login-inputorbuttom login-bottom"
          type="primary"
          v-popover:popover
          @click="visible = !visible"
          >登 錄</el-button
        >
      </el-form-item>
    </el-form>
    <!--驗證碼彈窗-->
    <el-popover
      popper-class="slidingPictures"
      ref="popover"
      trigger="manual"
      v-model="visible"
    >
      <div class="sliding-pictures">
        <div class="vimg">
          <canvas id="sliderBlock"></canvas>
          <canvas id="codeImg"></canvas>
        </div>
        <div class="slider">
          <div class="track" :class="{ pintuTrue: puzzle }">
            {{ tips }}
          </div>
          <div class="button el-icon-s-grid" @mousedown.prevent="drag"></div>
        </div>
        <div class="operation">
          <span
            title="關閉驗證碼"
            @click="visible = false"
            class="el-icon-circle-close"
          ></span>
          <span
            title="刷新驗證碼"
            @click="canvasInit"
            class="el-icon-refresh-left"
          ></span>
        </div>
      </div>
    </el-popover>
  </div>
</template>

<script>
export default {
  name: "login",
  data() {
    return {
      tips: "拖動左邊滑塊完成上方拼圖",
      logindata: {
        userName: "",
        password: "",
        verificationCode: ""
      },
      rules: {},
      visible: false,
      //滑塊x軸數據
      slider: {
        mx: 0,
        bx: 0
      },
      //拼圖是否正確
      puzzle: false
    };
  },
  watch: {
    visible(e) {
      if (e === true) {
        this.canvasInit();
        this.puzzle = false;
      }
    }
  },
  beforeCreate() {},
  created() {},
  beforeMount() {},
  mounted() {},
  methods: {
    //拼圖驗證碼初始化
    canvasInit() {
      //生成指定區間的隨機數
      const random = (min, max) => {
        return Math.floor(Math.random() * (max - min + 1) + min);
      };
      //x: 254, y: 109
      let mx = random(127, 244),
        bx = random(10, 128),
        y = random(10, 99);

      this.slider = { mx, bx };

      this.draw(mx, bx, y);
    },
    //鼠標按下
    drag(e) {
      console.log("鼠標按下", e);
      let dom = e.target; //dom元素
      let slider = document.querySelector("#sliderBlock"); //滑塊dom
      const downCoordinate = { x: e.x, y: e.y };

      //正確的滑塊數據
      let checkx = Number(this.slider.mx) - Number(this.slider.bx);
      //x軸數據
      let x = 0;
      const move = moveEV => {
        x = moveEV.x - downCoordinate.x;
        //y = moveEV.y - downCoordinate.y;
        if (x >= 251 || x <= 0) return false;
        dom.style.left = x + "px";
        //dom.style.top = y + "px";
        slider.style.left = x + "px";
      };

      const up = () => {
        document.removeEventListener("mousemove", move);
        document.removeEventListener("mouseup", up);
        dom.style.left = "";

        console.log(x, checkx);
        let max = checkx - 5;
        let min = checkx - 10;
        //容許正負偏差1
        if ((max >= x && x >= min) || x === checkx) {
          console.log("滑動解鎖成功");
          this.puzzle = true;
          this.tips = "驗證成功";
          setTimeout(() => {
            this.visible = false;
          }, 500);
        } else {
          console.log("拼圖位置不正確");
          this.tips = "驗證失敗,請重試";
          this.puzzle = false;
          this.canvasInit();
        }
      };

      document.addEventListener("mousemove", move);
      document.addEventListener("mouseup", up);
    },
    draw(mx = 200, bx = 20, y = 50) {
      let mainDom = document.querySelector("#codeImg");
      let bg = mainDom.getContext("2d");
      let width = mainDom.width;
      let height = mainDom.height;

      let blockDom = document.querySelector("#sliderBlock");
      let block = blockDom.getContext("2d");
      //從新賦值,讓canvas進行從新繪製
      blockDom.height = height;
      mainDom.height = height;

      let imgsrc = require("../assets/images/back.jpg");
      let img = document.createElement("img");
      img.style.objectFit = "scale-down";
      img.src = imgsrc;
      img.onload = function() {
        bg.drawImage(img, 0, 0, width, height);
        block.drawImage(img, 0, 0, width, height);
      };

      let mainxy = { x: mx, y: y, r: 9 };
      let blockxy = { x: bx, y: y, r: 9 };
      this.drawBlock(bg, mainxy, "fill");
      this.drawBlock(block, blockxy, "clip");
    },
    //繪製拼圖
    drawBlock(ctx, xy = { x: 254, y: 109, r: 9 }, type) {
      let x = xy.x,
        y = xy.y,
        r = xy.r,
        w = 40;
      let PI = Math.PI;
      //繪製
      ctx.beginPath();
      //left
      ctx.moveTo(x, y);
      //top
      ctx.arc(x + (w + 5) / 2, y, r, -PI, 0, true);
      ctx.lineTo(x + w + 5, y);
      //right
      ctx.arc(x + w + 5, y + w / 2, r, 1.5 * PI, 0.5 * PI, false);
      ctx.lineTo(x + w + 5, y + w);
      //bottom
      ctx.arc(x + (w + 5) / 2, y + w, r, 0, PI, false);
      ctx.lineTo(x, y + w);
      ctx.arc(x, y + w / 2, r, 0.5 * PI, 1.5 * PI, true);
      ctx.lineTo(x, y);
      //修飾,沒有會看不出效果
      ctx.lineWidth = 1;
      ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
      ctx.strokeStyle = "rgba(255, 255, 255, 0.5)";
      ctx.stroke();
      ctx[type]();
      ctx.globalCompositeOperation = "xor";
    }
  }
};
</script>
<style>
.slidingPictures {
  padding: 0;
  width: 300px;
  border-radius: 2px;
}
</style>
<style scoped lang="scss">
#login {
  display: flex;
  flex-flow: row;
  justify-content: flex-end;
  align-items: center;
  width: 100%;
  height: 100%;
  background-image: url("../assets/images/back.jpg");
  background-size: 100% 100%;
  .loginFrom {
    width: 300px;
    margin-top: -10vw;
    margin-right: 10vw;
    .login-item {
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .login-title {
      color: #ffffff;
      font-size: 16px;
      margin-bottom: 10px;
    }
    .login-bottom {
      margin-top: 15px;
    }
    .login-bottom:hover {
      background: rgba(28, 136, 188, 0.5);
    }
    .login-bottom:active {
      background: rgba(228, 199, 200, 0.5);
    }
    /deep/.login-inputorbuttom {
      height: 40px;
      width: 300px;
      background: rgba(57, 108, 158, 0.5);
      border-radius: 20px;
      border: #396c9e 1px solid;
      font-size: 14px;
      color: #ffffff;
      .el-input--small,
      .el-input__inner {
        line-height: 43px;
        border: none;
        color: #ffffff;
        font-size: 14px;
        height: 40px;
        border-radius: 20px;
        background: transparent;
        text-align: center;
      }
      .el-input__icon {
        line-height: 40px;
        font-size: 16px;
      }
    }
  }
}
/*該樣式最終是以彈窗插入*/
.sliding-pictures {
  width: 100%;
  .vimg {
    width: 100%;
    height: 170px;
    #codeImg,
    #sliderBlock {
      padding: 7px 7px 0 7px;
      width: inherit;
      height: inherit;
    }
    #codeImg {
      //display: none;
    }
    #sliderBlock {
      position: absolute;
      z-index: 4000;
    }
  }
  .slider {
    width: 100%;
    height: 65px;
    border-bottom: #c7c9d0 1px solid;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    .track {
      margin-left: 7px;
      width: 286px;
      height: 38px;
      background: rgba(28, 136, 188, 0.5);
      border-radius: 25px;
      font-size: 14px;
      line-height: 38px;
      padding-right: 15px;
      padding-left: 70px;
    }
    .pintuTrue {
      background: #67c23a;
      color: #ffffff;
    }
    .button {
      position: absolute;
      width: 50px;
      height: 50px;
      line-height: 48px;
      background: #ffffff;
      box-shadow: #b9bdc8 0 0 3px;
      border-radius: 50%;
      left: 7px;
      text-align: center;
      font-size: 28px;
      color: #3e5d8b;
      &:hover {
        color: #2181bd;
      }
    }
  }
  .operation {
    width: 100%;
    height: 40px;
    > span {
      color: #9fa3ac;
      display: inline-block;
      width: 40px;
      font-size: 25px;
      line-height: 40px;
      text-align: center;
      &:hover {
        background: #e2e8f5;
      }
    }
  }
}
</style>
複製代碼

六、致謝

個人代碼參考來自:

https://www.jb51.net/article/137129.htm

做者的git地址:https://github.com/yeild/jigsaw

相關文章
相關標籤/搜索