Vue實現(xiàn)滑動拼圖驗證碼功能
緣由:之前看嗶哩嗶哩官網(wǎng)登錄的時候有一個拼圖驗證碼,很好奇怎么去實現(xiàn)。然后就想著自己弄一個。先給大家看我的最終效果。后面再一點點拆解代碼。
為什么想著寫這個功能呢,主要在于拼圖驗證碼在前端這里會比較復雜并且深入。相比文字拼寫,12306的圖片驗證碼都沒有拼圖驗證碼對前端的要求來的復雜,和難。
我總結下知識點:
1、彈窗功能
2、彈窗基于元素定位
3、元素拖動
4、canvas繪圖
5、基礎邏輯
一、彈窗和彈窗組件
抱歉,這里我偷懶了直接用了elementUI的el-popover組件,所以小伙伴不懂的直接看elementUI官網(wǎng)的說明。
我個人也研究和編寫了這塊的組件功能(基于popper.js)
二、編寫基礎結構
這塊屬于html的基礎內容,也就標題黨了
三、canvas繪制圖片
1、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);
};
2、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";
}
解釋下:
參數(shù)是傳入canvas對象
x,y軸數(shù)據(jù)
剪切還是填充的canvas函數(shù)(fill,clip)
繪制難點:(很重要,不然你沒法理解它怎么繪制的)
繪制主要是需要理解這里的繪制是根據(jù)你設置一個起始點坐標,然后你繪制第二次的時候線就會連接到第二個點,依次連接最后回到原點就形成一個完整的圖形。
圓形的繪制:來自:https://www.w3school.com.cn/tags/canvas_arc.asp
用的是arc參數(shù),主要是看這個圖
fill是用于填充繪制的部分,clip是裁剪出繪制的部分,利用這個就可以出現(xiàn)一個扣掉的圖片和一個裁剪出來的圖片。
完成之后就是我的那個函數(shù)了。大家可以直接拿去用。
3、讓元素跟隨鼠標點擊之后滑動
這里其實原理非常簡單,就是有一個注意點。
原理:
鼠標點擊之后記錄當前坐標,然后隨著( mousemove )滾動的時候修改元素的left和top值就行了。
還有一點就是鼠標快速滑動會導致丟失滑動效果,這里需要用document,不能是元素級別的監(jiān)聽。
元素上面我只需要鑒定按下 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 };
//正確的滑塊數(shù)據(jù)
let checkx = Number(this.slider.mx) - Number(this.slider.bx);
//x軸數(shù)據(jù)
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);
}
4、總結
核心點比較多,寫過之后發(fā)現(xiàn)不難,關鍵在于去寫
個人該頁面git地址:https://github.com/ht-sauce/dream
該頁面處于項目的
路由訪問為:http://localhost:8080/consumer
5、完整的頁面代碼
<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軸數(shù)據(jù)
slider: {
mx: 0,
bx: 0
},
//拼圖是否正確
puzzle: false
};
},
watch: {
visible(e) {
if (e === true) {
this.canvasInit();
this.puzzle = false;
}
}
},
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
methods: {
//拼圖驗證碼初始化
canvasInit() {
//生成指定區(qū)間的隨機數(shù)
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 };
//正確的滑塊數(shù)據(jù)
let checkx = Number(this.slider.mx) - Number(this.slider.bx);
//x軸數(shù)據(jù)
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>
總結
以上所述是小編給大家介紹的Vue實現(xiàn)滑動拼圖驗證碼功能,希望對大家有所幫助,如果大家有任何疑問歡迎給我留言,小會及時回復大家的!
相關文章
利用vue + koa2 + mockjs模擬數(shù)據(jù)的方法教程
這篇文章主要給大家介紹了關于利用vue + koa2 + mockjs模擬數(shù)據(jù)的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2017-11-11
vue?eslint報錯error?"Component?name?"*****"
這篇文章主要給大家介紹了關于vue?eslint報錯error?“Component?name?“*****“?should?always?be?multi-word”的解決方法,文中通過圖文以及實例代碼介紹的非常詳細,需要的朋友可以參考下2022-09-09
electron踩坑之remote of undefined的解決
這篇文章主要介紹了electron踩坑之remote of undefined的解決,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-10-10
利用vue3+threejs仿iView官網(wǎng)大波浪特效實例
最近好幾個vue項目都是用ivew作為UI框架,所以下面這篇文章主要給大家介紹了關于如何利用vue3?+?threejs仿iView官網(wǎng)大波浪特效的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2021-12-12
vue使用$attrs和$listeners多級組件嵌套傳遞數(shù)據(jù)
這篇文章主要為大家介紹了vue使用$attrs和$listeners多級組件嵌套傳遞數(shù)據(jù)示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08

