从0-1开发一个Vue3前端系统页面-6.滑动验证码
不再会有谎言 2024-09-19 08:33:01 阅读 55
本节实现登录页面的滑动验证码功能
滑动验证码新增优化在6.1,链接如下:从0-1开发一个Vue3前端系统页面-6.1滑动验证码优化-CSDN博客
https://blog.csdn.net/XiaomeiGuiSnJs/article/details/141159971?spm=1001.2014.3001.5501
1.安装插件
<code>npm install --save vue-monoplasty-slide-verify
2.从Git上获取源码
源码地址:github
https://so.csdn.net/so/search?q=github&spm=1001.2101.3001.7020文档地址: GitHub - monoplasty/vue-monoplasty-slide-verify: vue slide verify online previewvue slide verify online preview. Contribute to monoplasty/vue-monoplasty-slide-verify development by creating an account on GitHub.
https://github.com/monoplasty/vue-monoplasty-slide-verify
3.创建工具类
在本地项目中src下新建一个工具类文件夹utils用作存储工具类并创建一个包用于存储滑动验证码组件,项目结构参考如图
4.打开Git源码
1.将lib下的文件复制到新建的滑动验证码包(slideVerify)中
2.将assets下的icon_light.png复制到本地项目中
此处在assets下新建了一个静态工具类utils用于存储项目工具类中需要的静态资源,与项目研发所需静态资源进行了区分。
5.修改源码
因为本地项目与源码项目使用的静态文件路径不同,所以在源码slide-Verify.vue中Ctrl+F寻找assets然后对图片icon_light.png的相对路径进行修改。参考如图:
源码:
修改后:
源码中共两处需要修改,分别是如图的P387和P438行。
6.配置main.js
7.本地项目中使用
<code><template>
<!-- 滑动验证插件 SLIDEVERIFY 的使用 -->
<div class="divout">code>
<slide-verify :l="42" :r="10" :w="310" :h="155" ref="slideblock" @again="onAgain" @fulfilled="onFulfilled"code>
@success="onSuccess" @fail="onFail" @refresh="onRefresh" :slider-text="text" :accuracy="accuracy"code>
:imgs="imgs"></slide-verify>code>
</div>
</template>
<script>
// prop参数说明
// 参数 类型 备注
// l Number 滑块边长
// r Number 滑块突出圆形半径
// w Number canvas width
// h Number canvas height
// sliderText String 滑块底部文字
// imgs Array picture array 背景图数组,默认值 []
// accuracy Number 滑动验证的误差范围,默认值 5
// show Boolean 是否显示刷新按钮,默认值 true
// 回调函数
// 回调函数 备注
// success success callback(返回时间参数,单位为毫秒)
// fail fail callback
// refresh 点击刷新按钮后的回调函数
// again 检测到非人为操作滑动时触发的回调函数
// fulfilled 刷新成功之后的回调函数
export default {
data() {
return {
text: "向右滑动->", // 设置滑块文字
// 精确度小,可允许的误差范围小;为1时,则表示滑块要与凹槽完全重叠,才能验证成功。默认值为5
accuracy: 2,
imgs: [
"https://img1.baidu.com/it/u=1890390320,3399874998&fm=253&fmt=auto&app=120&f=JPEG?w=1422&h=800",
"https://img1.baidu.com/it/u=1546227440,2897989905&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500",
"https://lmg.jj20.com/up/allimg/1113/052420110515/200524110515-1-1200.jpg",
"https://lmg.jj20.com/up/allimg/1114/0G020114924/200G0114924-11-1200.jpg",
],
};
},
methods: {
// 验证通过
onSuccess(times) {
console.log("验证通过,耗时 " + times + "毫秒");
},
// 验证失败
onFail() {
console.log("验证不通过");
},
// 滑块上的刷新
onRefresh() {
console.log("点击了刷新小图标");
},
// 刷新后执行的回调函数
onFulfilled() {
console.log("刷新成功啦!");
},
// 检测是否人为操作
onAgain() {
console.log("检测到非人为操作的哦!");
this.msg = "try again";
// 刷新
this.$refs.slideblock.reset();
},
// 重置刷新
handleClick() {
this.$refs.slideblock.reset();
},
},
};
</script>
<style scoped></style>
8.回到页面查看
滑动验证码组件配置成功
附件
考虑到有些同学可能进不去GitHub,所以这里把源码奉上。
注意:这里的源码若与我的路径不同的话会报路径错误,需要各位自行修改相对路径。
index.js
<code>import SlideVerify from "./slide-Verify.vue";
const plugins = {
install(Vue) {
Vue.component(SlideVerify.name, SlideVerify);
},
};
if (typeof window !== "undefined" && window.Vue) {
window.Vue.use(SlideVerify);
}
export default plugins;
slide-Verify.vue
<template>
<div class="slide-verify" :style="{ width: w + 'px' }" id="slideVerify" onselectstart="return false;">code>
<!-- 图片加载遮蔽罩 -->
<div :class="{ 'slider-verify-loading': loadBlock }"></div>code>
<canvas :width="w" :height="h" ref="canvas"></canvas>code>
<div v-if="show" @click="refresh" class="slide-verify-refresh-icon"></div>code>
<canvas :width="w" :height="h" ref="block" class="slide-verify-block"></canvas>code>
<!-- container -->
<div class="slide-verify-slider" :class="{
'container-active': containerActive,
'container-success': containerSuccess,
'container-fail': containerFail
}">code>
<div class="slide-verify-slider-mask" :style="{ width: sliderMaskWidth }">code>
<!-- slider -->
<div @mousedown="sliderDown" @touchstart="touchStartEvent" @touchmove="handleMoveEvent($event, 'touch')"code>
@touchend="handleMoveEndEvent($event, 'touch')" class="slide-verify-slider-mask-item"code>
:style="{ left: sliderLeft }">code>
<div class="slide-verify-slider-mask-item-icon"></div>code>
</div>
</div>
<span class="slide-verify-slider-text">{ { sliderText }}</span>code>
</div>
</div>
</template>
<script>
const PI = Math.PI;
function sum(x, y) {
return x + y;
}
function square(x) {
return x * x;
}
export default {
name: "SlideVerify",
props: {
// block length
l: {
type: Number,
default: 42
},
// block radius
r: {
type: Number,
default: 10
},
// canvas width
w: {
type: Number,
default: 310
},
// canvas height
h: {
type: Number,
default: 155
},
sliderText: {
type: String,
default: "Slide filled right"
},
accuracy: {
type: Number,
default: 5 // 若为 -1 则不进行机器判断
},
show: {
type: Boolean,
default: true
},
imgs: {
type: Array,
default: () => []
}
},
data() {
return {
containerActive: false, // container active class
containerSuccess: false, // container success class
containerFail: false, // container fail class
canvasCtx: null,
blockCtx: null,
block: null,
block_x: undefined, // container random position
block_y: undefined,
L: this.l + this.r * 2 + 3, // block real length
img: undefined,
originX: undefined,
originY: undefined,
isMouseDown: false,
trail: [],
sliderLeft: 0, // block right offset
sliderMaskWidth: 0, // mask width,
success: false, // Bug Fixes 修复了验证成功后还能滑动
loadBlock: true, // Features 图片加载提示,防止图片没加载完就开始验证
timestamp: null
};
},
mounted() {
this.init();
},
methods: {
init() {
this.initDom();
this.initImg();
this.bindEvents();
},
initDom() {
this.block = this.$refs.block;
this.canvasCtx = this.$refs.canvas.getContext("2d");
this.blockCtx = this.block.getContext("2d");
},
initImg() {
const img = this.createImg(() => {
// 图片加载完关闭遮蔽罩
this.loadBlock = false;
this.drawBlock();
this.canvasCtx.drawImage(img, 0, 0, this.w, this.h);
this.blockCtx.drawImage(img, 0, 0, this.w, this.h);
let { block_x: x, block_y: y, r, L } = this;
let _y = y - r * 2 - 1;
let ImageData = this.blockCtx.getImageData(x, _y, L, L);
this.block.width = L;
this.blockCtx.putImageData(ImageData, 0, _y);
});
this.img = img;
},
drawBlock() {
this.block_x = this.getRandomNumberByRange(
this.L + 10,
this.w - (this.L + 10)
);
this.block_y = this.getRandomNumberByRange(
10 + this.r * 2,
this.h - (this.L + 10)
);
this.draw(this.canvasCtx, this.block_x, this.block_y, "fill");
this.draw(this.blockCtx, this.block_x, this.block_y, "clip");
},
draw(ctx, x, y, operation) {
let { l, r } = this;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
ctx.lineTo(x + l, y);
ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
ctx.lineTo(x + l, y + l);
ctx.lineTo(x, y + l);
ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
ctx.lineTo(x, y);
ctx.lineWidth = 2;
ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
ctx.stroke();
ctx[operation]();
// Bug Fixes 修复了火狐和ie显示问题
ctx.globalCompositeOperation = "destination-over";
},
createImg(onload) {
const img = document.createElement("img");
img.crossOrigin = "Anonymous";
img.onload = onload;
img.onerror = () => {
img.src = this.getRandomImg();
};
img.src = this.getRandomImg();
return img;
},
// 随机生成img src
getRandomImg() {
// return require('../assets/img.jpg')
const len = this.imgs.length;
return len > 0
? this.imgs[this.getRandomNumberByRange(0, len - 1)]
: // "https://bing.ioliu.cn/v1/rand?w=300&h=150";
"https://source.unsplash.com/300x150/?book,library";
// "https://api.dujin.org/pic/fengjing";
},
getRandomNumberByRange(start, end) {
return Math.round(Math.random() * (end - start) + start);
},
refresh() {
this.reset();
this.$emit("refresh");
},
sliderDown(event) {
if (this.success) return;
this.originX = event.clientX;
this.originY = event.clientY;
this.isMouseDown = true;
this.timestamp = +new Date();
},
touchStartEvent(e) {
if (this.success) return;
this.originX = e.changedTouches[0].pageX;
this.originY = e.changedTouches[0].pageY;
this.isMouseDown = true;
this.timestamp = +new Date();
},
bindEvents() {
document.addEventListener("mousemove", this.handleMoveEvent);
document.addEventListener("mouseup", this.handleMoveEndEvent);
},
// 处理函数抽离
handleMoveEvent: throttle(function (e, type = "mouse") {
if (!this.isMouseDown) return false;
const moveX =
type === "mouse"
? e.clientX - this.originX
: e.changedTouches[0].pageX - this.originX;
const moveY =
type === "mouse"
? e.clientY - this.originY
: e.changedTouches[0].pageY - this.originY;
if (moveX < 0 || moveX + 38 >= this.w) return false;
this.sliderLeft = moveX + "px";
let blockLeft = ((this.w - 40 - 20) / (this.w - 40)) * moveX;
this.block.style.left = blockLeft + "px";
this.containerActive = true; // add active
this.sliderMaskWidth = moveX + "px";
this.trail.push(moveY);
}),
handleMoveEndEvent(e, type = "mouse") {
if (!this.isMouseDown) return false;
this.isMouseDown = false;
if (
(type === "mouse" && e.clientX === this.originX) ||
(type === "touch" && e.changedTouches[0].pageX === this.originX)
)
return false;
this.containerActive = false; // remove active
this.timestamp = +new Date() - this.timestamp;
const { spliced, TuringTest } = this.verify();
if (spliced) {
if (this.accuracy === -1) {
this.containerSuccess = true;
this.success = true;
this.$emit("success", this.timestamp);
return;
}
if (TuringTest) {
// succ
this.containerSuccess = true;
this.success = true;
this.$emit("success", this.timestamp);
} else {
this.containerFail = true;
this.$emit("again");
}
} else {
this.containerFail = true;
this.$emit("fail");
setTimeout(() => {
this.reset();
}, 1000);
}
},
verify() {
const arr = this.trail; // drag y move distance
const average = arr.reduce(sum) / arr.length; // average
const deviations = arr.map(x => x - average); // deviation array
const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length); // standard deviation
const left = parseInt(this.block.style.left);
const accuracy =
this.accuracy <= 1 ? 1 : this.accuracy > 10 ? 10 : this.accuracy;
return {
spliced: Math.abs(left - this.block_x) <= accuracy,
TuringTest: average !== stddev // equal => not person operate
};
},
reset() {
this.success = false;
this.containerActive = false;
this.containerSuccess = false;
this.containerFail = false;
this.sliderLeft = 0;
this.block.style.left = 0;
this.sliderMaskWidth = 0;
// canvas
let { w, h } = this;
this.canvasCtx.clearRect(0, 0, w, h);
this.blockCtx.clearRect(0, 0, w, h);
this.block.width = w;
// generate img
this.img.src = this.getRandomImg();
this.$emit("fulfilled");
}
},
destroyed() {
document.removeEventListener("mousemove", this.handleMoveEvent);
document.removeEventListener("mouseup", this.handleMoveEndEvent);
}
};
function throttle(
fn,
interval = 50,
options = { leading: true, trailing: true }
) {
const { leading, trailing, resultCallback } = options;
let lastTime = 0;
let timer = null;
const _throttle = function (...args) {
return new Promise((resolve, reject) => {
const nowTime = new Date().getTime();
if (!lastTime && !leading) lastTime = nowTime;
const remainTime = interval - (nowTime - lastTime);
if (remainTime <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
const result = fn.apply(this, args);
if (resultCallback) resultCallback(result);
resolve(result);
lastTime = nowTime;
return;
}
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null;
lastTime = !leading ? 0 : new Date().getTime();
const result = fn.apply(this, args);
if (resultCallback) resultCallback(result);
resolve(result);
}, remainTime);
}
});
};
_throttle.cancel = function () {
if (timer) clearTimeout(timer);
timer = null;
lastTime = 0;
};
return _throttle;
}
</script>
<style scoped>
.slide-verify {
position: relative;
}
/* 图片加载样式 */
.slider-verify-loading {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
z-index: 999;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
opacity: 0.7;
}
100% {
opacity: 9;
}
}
.slide-verify-block {
position: absolute;
left: 0;
top: 0;
}
.slide-verify-refresh-icon {
position: absolute;
right: 0;
top: 0;
width: 34px;
height: 34px;
cursor: pointer;
background: url("../../../assets/utils/icon_light.png") 0 -437px;
background-size: 34px 471px;
}
.slide-verify-slider {
position: relative;
text-align: center;
width: 100%;
height: 40px;
line-height: 40px;
margin-top: 15px;
background: #f7f9fa;
color: #45494c;
border: 1px solid #e4e7eb;
}
.slide-verify-slider-mask {
position: absolute;
left: 0;
top: 0;
height: 40px;
border: 0 solid #1991fa;
background: #d1e9fe;
}
.slide-verify-slider-mask-item {
position: absolute;
top: 0;
left: 0;
width: 40px;
height: 40px;
background: #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: background 0.2s linear;
}
.slide-verify-slider-mask-item:hover {
background: #1991fa;
}
.slide-verify-slider-mask-item:hover .slide-verify-slider-mask-item-icon {
background-position: 0 -13px;
}
.slide-verify-slider-mask-item-icon {
position: absolute;
top: 15px;
left: 13px;
width: 14px;
height: 12px;
background: url("../../../assets/utils/icon_light.png") 0 -26px;
background-size: 34px 471px;
}
.container-active .slide-verify-slider-mask-item {
height: 38px;
top: -1px;
border: 1px solid #1991fa;
}
.container-active .slide-verify-slider-mask {
height: 38px;
border-width: 1px;
}
.container-success .slide-verify-slider-mask-item {
height: 38px;
top: -1px;
border: 1px solid #52ccba;
background-color: #52ccba !important;
}
.container-success .slide-verify-slider-mask {
height: 38px;
border: 1px solid #52ccba;
background-color: #d2f4ef;
}
.container-success .slide-verify-slider-mask-item-icon {
background-position: 0 0 !important;
}
.container-fail .slide-verify-slider-mask-item {
height: 38px;
top: -1px;
border: 1px solid #f57a7a;
background-color: #f57a7a !important;
}
.container-fail .slide-verify-slider-mask {
height: 38px;
border: 1px solid #f57a7a;
background-color: #fce1e1;
}
.container-fail .slide-verify-slider-mask-item-icon {
top: 14px;
background-position: 0 -82px !important;
}
.container-active .slide-verify-slider-text,
.container-success .slide-verify-slider-text,
.container-fail .slide-verify-slider-text {
display: none;
}
</style>
icon_light.png
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。