vue项目登录模块滑块拼图验证功能实现代码(纯前端)
作者:Doraemon*
滑块验证作为一种反机器人的工具,也会不断发展和演进,以适应不断变化的威胁,这篇文章主要给大家介绍了vue项目登录模块滑块拼图验证功能实现的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
前言
在当今互联网时代,随着技术的不断进步,传统的验证码验证方式已经无法满足对安全性和用户体验的需求。为了应对日益狡猾的机器人和恶意攻击,许多网站和应用程序开始引入图形验证码,其中一种备受欢迎的形式就是图片旋转验证功能。这项技术通过利用用户交互、视觉识别和动态效果,为用户提供了一种全新、有趣且高效的验证方式。本文将深入探讨如何实现这一引人注目的图片旋转验证功能,让您轻松保护网站安全,同时提升用户体验
效果展示
功能介绍:
在vue项目中将此验证弹框封装成一个单独的组件,完整代码如下;
此功能中的图是利用canvas技术随机画10个图形拼接而成,然后就是画缺口和缺口的内阴影。
拖动滑轨调整小图移动位置,完成验证功能,验证失败会自动刷新再次验证,点击“刷新”也可以收到刷新图案,这是一个由纯前端实现的验证功能;
完整代码—组件封装
<!-- 滑块拼图验证模块 --> <template> <div> <!-- <div @click="changeBtn" class="btn">开始验证</div> --> <div></div> <!-- 本体部分 --> <div v-show="shoWData" :class="['vue-puzzle-vcode', { show_: show }]" @mousedown="onCloseMouseDown" @mouseup="onCloseMouseUp" @touchstart="onCloseMouseDown" @touchend="onCloseMouseUp"> <div class="vue-auth-box_" @mousedown.stop @touchstart.stop> <div class="auth-body_" :style="`height: ${canvasHeight}px`"> <!-- 主图,有缺口 --> <canvas style="border-radius: 10px" ref="canvas1" :width="canvasWidth" :height="canvasHeight" :style="`width:${canvasWidth}px;height:${canvasHeight}px`" /> <!-- 成功后显示的完整图 --> <canvas ref="canvas3" :class="['auth-canvas3_', { show: isSuccess }]" :width="canvasWidth" :height="canvasHeight" :style="`width:${canvasWidth}px;height:${canvasHeight}px`" /> <!-- 小图 --> <canvas :width="puzzleBaseSize" class="auth-canvas2_" :height="canvasHeight" ref="canvas2" :style="`width:${puzzleBaseSize}px;height:${canvasHeight}px;transform:translateX(${styleWidth - sliderBaseSize - (puzzleBaseSize - sliderBaseSize) * ((styleWidth - sliderBaseSize) / (canvasWidth - sliderBaseSize))}px)` " /> <div :class="['info-box_', { show: infoBoxShow }, { fail: infoBoxFail }]"> {{ infoText }} </div> <div :class="['flash_', { show: !isSuccess }]" :style="`transform: translateX(${isSuccess ? `${canvasWidth + canvasHeight * 0.578}px` : `-${canvasHeight * 0.578}px` }) skew(-30deg, 0);` "></div> <img class="reset_" @click="reset" :src="resetSvg" /> </div> <div class="auth-control_"> <div class="range-box" :style="`height:${sliderBaseSize}px`"> <div class="range-text">{{ sliderText }}</div> <div class="range-slider" ref="range-slider" :style="`width:${styleWidth}px`"> <div :class="['range-btn', { isDown: mouseDown }]" :style="`width:${sliderBaseSize}px`" @mousedown="onRangeMouseDown($event)" @touchstart="onRangeMouseDown($event)"> <!-- 按钮内部样式 --> <div></div> <div></div> <div></div> </div> </div> </div> </div> </div> </div> </div> </template> <script> import resetSvg from "@/assets/images/pc/login/Vector.png"; export default { props: { canvasWidth: { type: Number, default: 350 }, // 主canvas的宽 canvasHeight: { type: Number, default: 200 }, // 主canvas的高 // 是否出现,由父级控制 show: { type: Boolean, default: true }, puzzleScale: { type: Number, default: 1 }, // 拼图块的大小缩放比例 sliderSize: { type: Number, default: 50 }, // 滑块的大小 range: { type: Number, default: 10 }, // 允许的偏差值 // 所有的背景图片 imgs: { type: Array }, successText: { type: String, default: "验证通过!" }, failText: { type: String, default: "验证失败,请重试" }, sliderText: { type: String, default: "拖动滑块完成拼图验证" }, shoWData: { type: Boolean, default: false } }, data() { return { verSuccess: false, isShow: false, mouseDown: false, // 鼠标是否在按钮上按下 startWidth: 50, // 鼠标点下去时父级的width startX: 0, // 鼠标按下时的X newX: 0, // 鼠标当前的偏移X pinX: 0, // 拼图的起始X pinY: 0, // 拼图的起始Y loading: false, // 是否正在加在中,主要是等图片onload isCanSlide: false, // 是否可以拉动滑动条 error: false, // 图片加在失败会出现这个,提示用户手动刷新 infoBoxShow: false, // 提示信息是否出现 infoText: "", // 提示等信息 infoBoxFail: false, // 是否验证失败 timer1: null, // setTimout1 closeDown: false, // 为了解决Mac上的click BUG isSuccess: false, // 验证成功 imgIndex: -1, // 用于自定义图片时不会随机到重复的图片 isSubmting: false, // 是否正在判定,主要用于判定中不能点击重置按钮 resetSvg, }; }, /** 生命周期 **/ mounted() { // document.body.appendChild(this.$el); document.addEventListener("mousemove", this.onRangeMouseMove, { passive: false }); document.addEventListener("mouseup", this.onRangeMouseUp, { passive: false }); document.addEventListener("touchmove", this.onRangeMouseMove, { passive: false }); document.addEventListener("touchend", this.onRangeMouseUp, { passive: false }); if (this.show) { document.body.classList.add("vue-puzzle-overflow"); this.reset(); } // if (this.shoWData) { // this.isShow = this.shoWData; // console.log('我收到了验证!'); // } }, beforeDestroy() { clearTimeout(this.timer1); document.removeEventListener("mousemove", this.onRangeMouseMove, { passive: false }); document.removeEventListener("mouseup", this.onRangeMouseUp, { passive: false }); document.removeEventListener("touchmove", this.onRangeMouseMove, { passive: false }); document.removeEventListener("touchend", this.onRangeMouseUp, { passive: false }); }, /** 监听 **/ watch: { show(newV) { // 每次出现都应该重新初始化 if (newV) { document.body.classList.add("vue-puzzle-overflow"); this.reset(); } else { this.isSubmting = false; this.isSuccess = false; this.infoBoxShow = false; document.body.classList.remove("vue-puzzle-overflow"); } }, }, /** 计算属性 **/ computed: { // styleWidth是底部用户操作的滑块的父级,就是轨道在鼠标的作用下应该具有的宽度 styleWidth() { const w = this.startWidth + this.newX - this.startX; return w < this.sliderBaseSize ? this.sliderBaseSize : w > this.canvasWidth ? this.canvasWidth : w; }, // 图中拼图块的60 * 用户设定的缩放比例计算之后的值 0.2~2 puzzleBaseSize() { return Math.round( Math.max(Math.min(this.puzzleScale, 2), 0.2) * 52.5 + 6 ); }, // 处理一下sliderSize,弄成整数,以免计算有偏差 sliderBaseSize() { return Math.max( Math.min( Math.round(this.sliderSize), Math.round(this.canvasWidth * 0.5) ), 10 ); } }, /** 方法 **/ methods: { changeBtn() { this.isShow = true; }, // 关闭 onClose() { if (!this.mouseDown && !this.isSubmting) { clearTimeout(this.timer1); } }, onCloseMouseDown() { this.closeDown = true; this.isShow = false; this.init(true); //给父组件传一个状态 this.$emit('submit', 'F') }, onCloseMouseUp() { if (this.closeDown) { this.onClose(); } this.closeDown = false; }, // 鼠标按下准备拖动 onRangeMouseDown(e) { if (this.isCanSlide) { this.mouseDown = true; this.startWidth = this.$refs["range-slider"].clientWidth; this.newX = e.clientX || e.changedTouches[0].clientX; this.startX = e.clientX || e.changedTouches[0].clientX; } }, // 鼠标移动 onRangeMouseMove(e) { if (this.mouseDown) { // e.preventDefault(); this.newX = e.clientX || e.changedTouches[0].clientX; } }, // 鼠标抬起 onRangeMouseUp() { if (this.mouseDown) { this.mouseDown = false; this.submit(); } }, /** * 开始进行 * @param withCanvas 是否强制使用canvas随机作图 */ init(withCanvas) { // 防止重复加载导致的渲染错误 if (this.loading && !withCanvas) { return; } this.loading = true; this.isCanSlide = false; const c = this.$refs.canvas1; const c2 = this.$refs.canvas2; const c3 = this.$refs.canvas3; const ctx = c.getContext("2d", { willReadFrequently: true }); const ctx2 = c2.getContext("2d", { willReadFrequently: true }); const ctx3 = c3.getContext("2d", { willReadFrequently: true }); const isFirefox = navigator.userAgent.indexOf("Firefox") >= 0 && navigator.userAgent.indexOf("Windows") >= 0; // 是windows版火狐 const img = document.createElement("img"); ctx.fillStyle = "rgba(255,255,255,1)"; ctx3.fillStyle = "rgba(255,255,255,1)"; ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); ctx2.clearRect(0, 0, this.canvasWidth, this.canvasHeight); // 取一个随机坐标,作为拼图块的位置 this.pinX = this.getRandom(this.puzzleBaseSize, this.canvasWidth - this.puzzleBaseSize - 20); // 留20的边距 this.pinY = this.getRandom(20, this.canvasHeight - this.puzzleBaseSize - 20); // 主图高度 - 拼图块自身高度 - 20边距 img.crossOrigin = "anonymous"; // 匿名,想要获取跨域的图片 img.onload = () => { const [x, y, w, h] = this.makeImgSize(img); ctx.save(); // 先画小图 this.paintBrick(ctx); ctx.closePath(); if (!isFirefox) { ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.shadowColor = "#000"; ctx.shadowBlur = 0; //ctx.globalAlpha = 0.4; ctx.fill(); ctx.clip(); } else { ctx.clip(); ctx.save(); ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.shadowColor = "#000"; ctx.shadowBlur = 0; //ctx.globalAlpha = 0.3; ctx.fill(); ctx.restore(); } ctx.drawImage(img, x, y, w, h); ctx3.fillRect(0, 0, this.canvasWidth, this.canvasHeight); ctx3.drawImage(img, x, y, w, h); // 设置小图的内阴影 ctx.globalCompositeOperation = "source-atop"; this.paintBrick(ctx); ctx.arc( this.pinX + Math.ceil(this.puzzleBaseSize / 2), this.pinY + Math.ceil(this.puzzleBaseSize / 2), this.puzzleBaseSize * 1.2, 0, Math.PI * 2, true ); ctx.closePath(); ctx.shadowColor = "rgba(255, 255, 255, .8)"; ctx.shadowOffsetX = -1; ctx.shadowOffsetY = -1; ctx.shadowBlur = Math.min(Math.ceil(8 * this.puzzleScale), 12); ctx.fillStyle = "#ffffaa"; ctx.fill(); // 将小图赋值给ctx2 const imgData = ctx.getImageData( this.pinX - 3, // 为了阴影 是从-3px开始截取,判定的时候要+3px this.pinY - 20, this.pinX + this.puzzleBaseSize + 5, this.pinY + this.puzzleBaseSize + 5 ); ctx2.putImageData(imgData, 0, this.pinY - 20); // ctx2.drawImage(c, this.pinX - 3,this.pinY - 20,this.pinX + this.puzzleBaseSize + 5,this.pinY + this.puzzleBaseSize + 5, // 0, this.pinY - 20, this.pinX + this.puzzleBaseSize + 5, this.pinY + this.puzzleBaseSize + 5); // 清理 ctx.restore(); ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); // 画缺口 ctx.save(); this.paintBrick(ctx); ctx.globalAlpha = 1; ctx.fillStyle = "#ffffff"; ctx.fill(); ctx.restore(); // 画缺口的内阴影 ctx.save(); ctx.globalCompositeOperation = "source-atop"; this.paintBrick(ctx); ctx.arc( this.pinX + Math.ceil(this.puzzleBaseSize / 2), this.pinY + Math.ceil(this.puzzleBaseSize / 2), this.puzzleBaseSize * 1.2, 0, Math.PI * 2, true ); ctx.shadowColor = "#ffffff"; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; ctx.shadowBlur = 16; ctx.fill(); ctx.restore(); // 画整体背景图 ctx.save(); ctx.globalCompositeOperation = "destination-over"; ctx.drawImage(img, x, y, w, h); ctx.restore(); this.loading = false; this.isCanSlide = true; }; img.onerror = () => { this.init(true); // 如果图片加载错误就重新来,并强制用canvas随机作图 }; if (!withCanvas && this.imgs && this.imgs.length) { let randomNum = this.getRandom(0, this.imgs.length - 1); if (randomNum === this.imgIndex) { if (randomNum === this.imgs.length - 1) { randomNum = 0; } else { randomNum++; } } this.imgIndex = randomNum; img.src = this.imgs[randomNum]; } else { img.src = this.makeImgWithCanvas(); } }, // 工具 - 范围随机数 getRandom(min, max) { return Math.ceil(Math.random() * (max - min) + min); }, // 工具 - 设置图片尺寸cover方式贴合canvas尺寸 w/h makeImgSize(img) { const imgScale = img.width / img.height; const canvasScale = this.canvasWidth / this.canvasHeight; let x = 0, y = 0, w = 0, h = 0; if (imgScale > canvasScale) { h = this.canvasHeight; w = imgScale * h; y = 0; x = (this.canvasWidth - w) / 2; } else { w = this.canvasWidth; h = w / imgScale; x = 0; y = (this.canvasHeight - h) / 2; } return [x, y, w, h]; }, // 绘制拼图块的路径 paintBrick(ctx) { const moveL = Math.ceil(15 * this.puzzleScale); // 直线移动的基础距离 ctx.beginPath(); ctx.moveTo(this.pinX, this.pinY); ctx.lineTo(this.pinX + moveL, this.pinY); ctx.arcTo( this.pinX + moveL, this.pinY - moveL / 2, this.pinX + moveL + moveL / 2, this.pinY - moveL / 2, moveL / 2 ); ctx.arcTo( this.pinX + moveL + moveL, this.pinY - moveL / 2, this.pinX + moveL + moveL, this.pinY, moveL / 2 ); ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY); ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY + moveL); ctx.arcTo( this.pinX + moveL + moveL + moveL + moveL / 2, this.pinY + moveL, this.pinX + moveL + moveL + moveL + moveL / 2, this.pinY + moveL + moveL / 2, moveL / 2 ); ctx.arcTo( this.pinX + moveL + moveL + moveL + moveL / 2, this.pinY + moveL + moveL, this.pinX + moveL + moveL + moveL, this.pinY + moveL + moveL, moveL / 2 ); ctx.lineTo( this.pinX + moveL + moveL + moveL, this.pinY + moveL + moveL + moveL ); ctx.lineTo(this.pinX, this.pinY + moveL + moveL + moveL); ctx.lineTo(this.pinX, this.pinY + moveL + moveL); ctx.arcTo( this.pinX + moveL / 2, this.pinY + moveL + moveL, this.pinX + moveL / 2, this.pinY + moveL + moveL / 2, moveL / 2 ); ctx.arcTo( this.pinX + moveL / 2, this.pinY + moveL, this.pinX, this.pinY + moveL, moveL / 2 ); ctx.lineTo(this.pinX, this.pinY); }, // 用canvas随机生成图片 makeImgWithCanvas() { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d", { willReadFrequently: true }); canvas.width = this.canvasWidth; canvas.height = this.canvasHeight; ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom( 100, 255 )},${this.getRandom(100, 255)})`; ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); // 随机画10个图形 for (let i = 0; i < 12; i++) { ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom( 100, 255 )},${this.getRandom(100, 255)})`; ctx.strokeStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom( 100, 255 )},${this.getRandom(100, 255)})`; if (this.getRandom(0, 2) > 1) { // 矩形 ctx.save(); ctx.rotate((this.getRandom(-90, 90) * Math.PI) / 180); ctx.fillRect( this.getRandom(-20, canvas.width - 20), this.getRandom(-20, canvas.height - 20), this.getRandom(10, canvas.width / 2 + 10), this.getRandom(10, canvas.height / 2 + 10) ); ctx.restore(); } else { // 圆 ctx.beginPath(); const ran = this.getRandom(-Math.PI, Math.PI); ctx.arc( this.getRandom(0, canvas.width), this.getRandom(0, canvas.height), this.getRandom(10, canvas.height / 2 + 10), ran, ran + Math.PI * 1.5 ); ctx.closePath(); ctx.fill(); } } return canvas.toDataURL("image/png"); }, // 开始判定 submit() { this.isSubmting = true; // 偏差 x = puzzle的起始X - (用户真滑动的距离) + (puzzle的宽度 - 滑块的宽度) * (用户真滑动的距离/canvas总宽度) // 最后+ 的是补上slider和滑块宽度不一致造成的缝隙 const x = Math.abs( this.pinX - (this.styleWidth - this.sliderBaseSize) + (this.puzzleBaseSize - this.sliderBaseSize) * ((this.styleWidth - this.sliderBaseSize) / (this.canvasWidth - this.sliderBaseSize)) - 3 ); if (x < this.range) { // 成功 this.infoText = this.successText; this.infoBoxFail = false; this.infoBoxShow = true; this.isCanSlide = false; this.isSuccess = false; // 成功后准备关闭 clearTimeout(this.timer1); this.timer1 = setTimeout(() => { // 成功的回调 this.isSubmting = false; this.isShow = false; this.verSuccess = true; this.$emit('submit', 'F', this.verSuccess); this.reset(); }, 800); } else { // 失败 this.infoText = this.failText; this.infoBoxFail = true; this.infoBoxShow = true; this.isCanSlide = false; // 失败的回调 // this.$emit("fail", x); // 800ms后重置 clearTimeout(this.timer1); this.timer1 = setTimeout(() => { this.isSubmting = false; this.reset(); }, 800); } }, // 重置 - 重新设置初始状态 resetState() { this.infoBoxFail = false; this.infoBoxShow = false; this.isCanSlide = false; this.isSuccess = false; this.startWidth = this.sliderBaseSize; // 鼠标点下去时父级的width this.startX = 0; // 鼠标按下时的X this.newX = 0; // 鼠标当前的偏移X }, // 重置 reset() { if (this.isSubmting) { debugger return; } this.resetState(); this.init(); } } }; </script> <style lang="scss" scoped> .btn { cursor: pointer; background-color: #6aa0ff; width: 80px; height: 30px; text-align: center; line-height: 30px; color: #fff; } .vue-puzzle-vcode { position: fixed; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(0, 0, 0, 0.3); z-index: 999; opacity: 1; pointer-events: none; transition: opacity 200ms; &.show_ { opacity: 1; pointer-events: auto; } } .vue-auth-box_ { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 20px; background: #fff; user-select: none; border-radius: 20px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); .auth-body_ { position: relative; overflow: hidden; border-radius: 3px; .loading-box_ { position: absolute; top: 0; left: 0; bottom: 0; right: 0; background-color: rgba(0, 0, 0, 0.8); z-index: 20; opacity: 1; transition: opacity 200ms; display: flex; align-items: center; justify-content: center; &.hide_ { opacity: 0; pointer-events: none; .loading-gif_ { span { animation-play-state: paused; } } } .loading-gif_ { flex: none; height: 5px; line-height: 0; @keyframes load { 0% { opacity: 1; transform: scale(1.3); } 100% { opacity: 0.2; transform: scale(0.3); } } span { display: inline-block; width: 5px; height: 100%; margin-left: 2px; border-radius: 50%; background-color: #888; animation: load 1.04s ease infinite; &:nth-child(1) { margin-left: 0; } &:nth-child(2) { animation-delay: 0.13s; } &:nth-child(3) { animation-delay: 0.26s; } &:nth-child(4) { animation-delay: 0.39s; } &:nth-child(5) { animation-delay: 0.52s; } } } } .info-box_ { position: absolute; bottom: 0; left: 0; width: 100%; height: 24px; line-height: 24px; text-align: center; overflow: hidden; font-size: 13px; background-color: #83ce3f; opacity: 0; transform: translateY(24px); transition: all 200ms; color: #fff; z-index: 10; &.show { opacity: 0.95; transform: translateY(0); } &.fail { background-color: #ce594b; } } .auth-canvas2_ { position: absolute; top: 0; left: 0; width: 60px; height: 100%; z-index: 2; } .auth-canvas3_ { position: absolute; top: 0; left: 0; opacity: 0; transition: opacity 600ms; z-index: 3; &.show { opacity: 1; } } .flash_ { position: absolute; top: 0; left: 0; width: 30px; height: 100%; background-color: rgba(255, 255, 255, 0.1); z-index: 3; &.show { transition: transform 600ms; } } .reset_ { position: absolute; top: 2px; right: 2px; width: 35px; height: auto; z-index: 12; cursor: pointer; transition: transform 200ms; transform: rotate(0deg); &:hover { transform: rotate(-90deg); } } } .auth-control_ { .range-box { position: relative; width: 100%; background-color: #eef1f8; margin-top: 20px; border-radius: 3px; // box-shadow: 0 0 8px rgba(240, 240, 240, 0.6) inset; box-shadow: inset -2px -2px 4px rgba(50, 130, 251, 0.1), inset 2px 2px 4px rgba(34, 73, 132, 0.2); border-radius: 43px; .range-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 14px; color: #b7bcd1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; width: 100%; /* 背景颜色线性渐变 */ /* linear为线性渐变,也可以用下面的那种写法。left top,right top指的是渐变方向,左上到右上 */ /* color-stop函数,第一个表示渐变的位置,0为起点,0.5为中点,1为结束点;第二个表示该点的颜色。所以本次渐变为两边灰色,中间渐白色 */ background: -webkit-gradient(linear, left top, right top, color-stop(0, #4d4d4d), color-stop(.4, #4d4d4d), color-stop(.5, white), color-stop(.6, #4d4d4d), color-stop(1, #4d4d4d)); /* 设置为text,意思是把文本内容之外的背景给裁剪掉 */ -webkit-background-clip: text; /* 设置对象中的文字填充颜色 这里设置为透明 */ -webkit-text-fill-color: transparent; /* 每隔2秒调用下面的CSS3动画 infinite属性为循环执行animate */ -webkit-animation: animate 1.5s infinite; } /* 兼容写法,要放在@keyframes前面 */ @-webkit-keyframes animate { /* 背景从-100px的水平位置,移动到+100px的水平位置。如果要移动Y轴的,设置第二个数值 */ from { background-position: -100px; } to { background-position: 100px; } } @keyframes animate { from { background-position: -100px; } to { background-position: 100px; } } .range-slider { position: absolute; height: 100%; width: 50px; /**background-color: rgba(106, 160, 255, 0.8);*/ border-radius: 3px; .range-btn { position: absolute; display: flex; align-items: center; justify-content: center; right: 0; width: 50px; height: 100%; background-color: #fff; border-radius: 3px; /** box-shadow: 0 0 4px #ccc;*/ cursor: pointer; box-shadow: inset 0px -2px 4px rgba(0, 36, 90, 0.2), inset 0px 2px 4px rgba(194, 219, 255, 0.8); border-radius: 50%; &>div { width: 0; height: 40%; transition: all 200ms; &:nth-child(2) { margin: 0 4px; } border: solid 1px #6aa0ff; } &:hover, &.isDown { &>div:first-child { border: solid 4px transparent; height: 0; border-right-color: #6aa0ff; } &>div:nth-child(2) { border-width: 3px; height: 0; border-radius: 3px; margin: 0 6px; border-right-color: #6aa0ff; } &>div:nth-child(3) { border: solid 4px transparent; height: 0; border-left-color: #6aa0ff; } } } } } } } .vue-puzzle-overflow { overflow: hidden !important; } </style>
总结
到此这篇关于vue项目登录模块滑块拼图验证功能(纯前端)的文章就介绍到这了,更多相关vue登录模块滑块拼图验证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!