缘由:之前看哔哩哔哩官网登录的时候有一个拼图验证码,很好奇怎么去实现。然后就想着自己弄一个。先给大家看我的最终效果。后面再一点点拆解代码。
为什么想着写这个功能呢,主要在于拼图验证码在前端这里会比较复杂并且深入。相比文字拼写,12306的图片验证码都没有拼图验证码对前端的要求来的复杂,和难。
我总结下知识点:
1、弹窗功能
2、弹窗基于元素定位
3、元素拖动
4、canvas绘图
5、基础逻辑
一、弹窗和弹窗组件
抱歉,这里我偷懒了直接用了elementUI的el-popover组件,所以小伙伴不懂的直接看elementUI官网的说明。
我个人也研究和编写了这块的组件功能(基于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绘制滑块部分
就是这个图,这个有一些知识点,不难,但是很复杂。
代码部分:
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)
绘制难点:(很重要,不然你没法理解它怎么绘制的)
绘制主要是需要理解这里的绘制是根据你设置一个起始点坐标,然后你绘制第二次的时候线就会连接到第二个点,依次连接最后回到原点就形成一个完整的图形。
用的是arc参数,主要是看这个图
fill是用于填充绘制的部分,clip是裁剪出绘制的部分,利用这个就可以出现一个扣掉的图片和一个裁剪出来的图片。
完成之后就是我的那个函数了。大家可以直接拿去用。
3、让元素跟随鼠标点击之后滑动
这里其实原理非常简单,就是有一个注意点。
原理:
鼠标点击之后记录当前坐标,然后随着(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);
}复制代码
4、总结
核心点比较多,写过之后发现不难,关键在于去写
个人该页面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">
<h2 class="login-title">海天酱油登录中心</h2>
</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"></style>
最后
喜欢的可以点个关注吗,小可乐也不容易呢