vue項(xiàng)目登錄模塊滑塊拼圖驗(yàn)證功能實(shí)現(xiàn)代碼(純前端)
前言
在當(dāng)今互聯(lián)網(wǎng)時(shí)代,隨著技術(shù)的不斷進(jìn)步,傳統(tǒng)的驗(yàn)證碼驗(yàn)證方式已經(jīng)無法滿足對安全性和用戶體驗(yàn)的需求。為了應(yīng)對日益狡猾的機(jī)器人和惡意攻擊,許多網(wǎng)站和應(yīng)用程序開始引入圖形驗(yàn)證碼,其中一種備受歡迎的形式就是圖片旋轉(zhuǎn)驗(yàn)證功能。這項(xiàng)技術(shù)通過利用用戶交互、視覺識別和動(dòng)態(tài)效果,為用戶提供了一種全新、有趣且高效的驗(yàn)證方式。本文將深入探討如何實(shí)現(xiàn)這一引人注目的圖片旋轉(zhuǎn)驗(yàn)證功能,讓您輕松保護(hù)網(wǎng)站安全,同時(shí)提升用戶體驗(yàn)
效果展示
功能介紹:
在vue項(xiàng)目中將此驗(yàn)證彈框封裝成一個(gè)單獨(dú)的組件,完整代碼如下;
此功能中的圖是利用canvas技術(shù)隨機(jī)畫10個(gè)圖形拼接而成,然后就是畫缺口和缺口的內(nèi)陰影。
拖動(dòng)滑軌調(diào)整小圖移動(dòng)位置,完成驗(yàn)證功能,驗(yàn)證失敗會自動(dòng)刷新再次驗(yàn)證,點(diǎn)擊“刷新”也可以收到刷新圖案,這是一個(gè)由純前端實(shí)現(xiàn)的驗(yàn)證功能;
完整代碼—組件封裝
<!-- 滑塊拼圖驗(yàn)證模塊 --> <template> <div> <!-- <div @click="changeBtn" class="btn">開始驗(yàn)證</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)"> <!-- 按鈕內(nèi)部樣式 --> <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的高 // 是否出現(xiàn),由父級控制 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: "驗(yàn)證通過!" }, failText: { type: String, default: "驗(yàn)證失敗,請重試" }, sliderText: { type: String, default: "拖動(dòng)滑塊完成拼圖驗(yàn)證" }, shoWData: { type: Boolean, default: false } }, data() { return { verSuccess: false, isShow: false, mouseDown: false, // 鼠標(biāo)是否在按鈕上按下 startWidth: 50, // 鼠標(biāo)點(diǎn)下去時(shí)父級的width startX: 0, // 鼠標(biāo)按下時(shí)的X newX: 0, // 鼠標(biāo)當(dāng)前的偏移X pinX: 0, // 拼圖的起始X pinY: 0, // 拼圖的起始Y loading: false, // 是否正在加在中,主要是等圖片onload isCanSlide: false, // 是否可以拉動(dòng)滑動(dòng)條 error: false, // 圖片加在失敗會出現(xiàn)這個(gè),提示用戶手動(dòng)刷新 infoBoxShow: false, // 提示信息是否出現(xiàn) infoText: "", // 提示等信息 infoBoxFail: false, // 是否驗(yàn)證失敗 timer1: null, // setTimout1 closeDown: false, // 為了解決Mac上的click BUG isSuccess: false, // 驗(yàn)證成功 imgIndex: -1, // 用于自定義圖片時(shí)不會隨機(jī)到重復(fù)的圖片 isSubmting: false, // 是否正在判定,主要用于判定中不能點(diǎn)擊重置按鈕 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('我收到了驗(yàn)證!'); // } }, 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 }); }, /** 監(jiān)聽 **/ watch: { show(newV) { // 每次出現(xiàn)都應(yīng)該重新初始化 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"); } }, }, /** 計(jì)算屬性 **/ computed: { // styleWidth是底部用戶操作的滑塊的父級,就是軌道在鼠標(biāo)的作用下應(yīng)該具有的寬度 styleWidth() { const w = this.startWidth + this.newX - this.startX; return w < this.sliderBaseSize ? this.sliderBaseSize : w > this.canvasWidth ? this.canvasWidth : w; }, // 圖中拼圖塊的60 * 用戶設(shè)定的縮放比例計(jì)算之后的值 0.2~2 puzzleBaseSize() { return Math.round( Math.max(Math.min(this.puzzleScale, 2), 0.2) * 52.5 + 6 ); }, // 處理一下sliderSize,弄成整數(shù),以免計(jì)算有偏差 sliderBaseSize() { return Math.max( Math.min( Math.round(this.sliderSize), Math.round(this.canvasWidth * 0.5) ), 10 ); } }, /** 方法 **/ methods: { changeBtn() { this.isShow = true; }, // 關(guān)閉 onClose() { if (!this.mouseDown && !this.isSubmting) { clearTimeout(this.timer1); } }, onCloseMouseDown() { this.closeDown = true; this.isShow = false; this.init(true); //給父組件傳一個(gè)狀態(tài) this.$emit('submit', 'F') }, onCloseMouseUp() { if (this.closeDown) { this.onClose(); } this.closeDown = false; }, // 鼠標(biāo)按下準(zhǔn)備拖動(dòng) 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; } }, // 鼠標(biāo)移動(dòng) onRangeMouseMove(e) { if (this.mouseDown) { // e.preventDefault(); this.newX = e.clientX || e.changedTouches[0].clientX; } }, // 鼠標(biāo)抬起 onRangeMouseUp() { if (this.mouseDown) { this.mouseDown = false; this.submit(); } }, /** * 開始進(jìn)行 * @param withCanvas 是否強(qiáng)制使用canvas隨機(jī)作圖 */ init(withCanvas) { // 防止重復(fù)加載導(dǎo)致的渲染錯(cuò)誤 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); // 取一個(gè)隨機(jī)坐標(biāo),作為拼圖塊的位置 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); // 設(shè)置小圖的內(nèi)陰影 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開始截取,判定的時(shí)候要+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(); // 畫缺口的內(nèi)陰影 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); // 如果圖片加載錯(cuò)誤就重新來,并強(qiáng)制用canvas隨機(jī)作圖 }; 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(); } }, // 工具 - 范圍隨機(jī)數(shù) getRandom(min, max) { return Math.ceil(Math.random() * (max - min) + min); }, // 工具 - 設(shè)置圖片尺寸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); // 直線移動(dòng)的基礎(chǔ)距離 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隨機(jī)生成圖片 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); // 隨機(jī)畫10個(gè)圖形 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 - (用戶真滑動(dòng)的距離) + (puzzle的寬度 - 滑塊的寬度) * (用戶真滑動(dòng)的距離/canvas總寬度) // 最后+ 的是補(bǔ)上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; // 成功后準(zhǔn)備關(guān)閉 clearTimeout(this.timer1); this.timer1 = setTimeout(() => { // 成功的回調(diào) 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; // 失敗的回調(diào) // this.$emit("fail", x); // 800ms后重置 clearTimeout(this.timer1); this.timer1 = setTimeout(() => { this.isSubmting = false; this.reset(); }, 800); } }, // 重置 - 重新設(shè)置初始狀態(tài) resetState() { this.infoBoxFail = false; this.infoBoxShow = false; this.isCanSlide = false; this.isSuccess = false; this.startWidth = this.sliderBaseSize; // 鼠標(biāo)點(diǎn)下去時(shí)父級的width this.startX = 0; // 鼠標(biāo)按下時(shí)的X this.newX = 0; // 鼠標(biāo)當(dāng)前的偏移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函數(shù),第一個(gè)表示漸變的位置,0為起點(diǎn),0.5為中點(diǎn),1為結(jié)束點(diǎn);第二個(gè)表示該點(diǎn)的顏色。所以本次漸變?yōu)閮蛇吇疑虚g漸白色 */ 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)); /* 設(shè)置為text,意思是把文本內(nèi)容之外的背景給裁剪掉 */ -webkit-background-clip: text; /* 設(shè)置對象中的文字填充顏色 這里設(shè)置為透明 */ -webkit-text-fill-color: transparent; /* 每隔2秒調(diào)用下面的CSS3動(dòng)畫 infinite屬性為循環(huán)執(zhí)行animate */ -webkit-animation: animate 1.5s infinite; } /* 兼容寫法,要放在@keyframes前面 */ @-webkit-keyframes animate { /* 背景從-100px的水平位置,移動(dòng)到+100px的水平位置。如果要移動(dòng)Y軸的,設(shè)置第二個(gè)數(shù)值 */ 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>
總結(jié)
到此這篇關(guān)于vue項(xiàng)目登錄模塊滑塊拼圖驗(yàn)證功能(純前端)的文章就介紹到這了,更多相關(guān)vue登錄模塊滑塊拼圖驗(yàn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue v-for中:key中item.id與Index使用的區(qū)別解析
這篇文章主要介紹了Vue v-for中:key中item.id與Index使用的區(qū)別解析,推薦使用【:key="item.id"】而不是將數(shù)組下標(biāo)當(dāng)做唯一標(biāo)識,前者能做到全部復(fù)用,本文給大家詳細(xì)講解,感興趣的朋友跟隨小編一起看看吧2024-02-02vue.js模仿京東省市區(qū)三級聯(lián)動(dòng)的選擇組件實(shí)例代碼
選擇省市區(qū)是我們大家在填寫地址的時(shí)候經(jīng)常會遇到的一個(gè)功能,下面這篇文章主要給大家介紹了關(guān)于利用vue.js模仿實(shí)現(xiàn)京東省市區(qū)三級聯(lián)動(dòng)選擇組件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2017-11-11解決vue里a標(biāo)簽值解析變量,跳轉(zhuǎn)頁面,前面加默認(rèn)域名端口的問題
這篇文章主要介紹了解決vue里a標(biāo)簽值解析變量,跳轉(zhuǎn)頁面,前面加默認(rèn)域名端口的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07vue?+?element-plus自定義表單驗(yàn)證(修改密碼業(yè)務(wù))的示例
這篇文章主要介紹了vue?+?element-plus自定義表單驗(yàn)證(修改密碼業(yè)務(wù)),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-04-04Vue Object 的變化偵測實(shí)現(xiàn)代碼
這篇文章主要介紹了Vue Object的變化偵測實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04el-tree的實(shí)現(xiàn)葉子節(jié)點(diǎn)單選的示例代碼
本文主要介紹了el-tree的實(shí)現(xiàn)葉子節(jié)點(diǎn)單選的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08關(guān)于Vue.nextTick()的正確使用方法淺析
最近在項(xiàng)目中遇到了一個(gè)需求,我們通過Vue.nextTick()來解決這一需求,但發(fā)現(xiàn)網(wǎng)上這方面的資料較少,所以自己來總結(jié)下,下面這篇文章主要給大家介紹了關(guān)于Vue.nextTick()正確使用方法的相關(guān)資料,需要的朋友可以參考下。2017-08-08前端Vue項(xiàng)目部署到服務(wù)器的全過程以及踩坑記錄
使用Vue做前后端分離項(xiàng)目時(shí),通常前端是單獨(dú)部署,用戶訪問的也是前端項(xiàng)目地址,因此前端開發(fā)人員很有必要熟悉一下項(xiàng)目部署的流程,下面這篇文章主要給大家介紹了關(guān)于前端Vue項(xiàng)目部署到服務(wù)器的全過程以及踩坑記錄的相關(guān)資料,需要的朋友可以參考下2023-05-05