在当今互联网时代,随着技术的不断进步,传统的验证码验证方式已经无法满足对安全性和用户体验的需求。为了应对日益狡猾的机器人和恶意攻击,许多网站和应用程序开始引入图形验证码,其中一种备受欢迎的形式就是图片旋转验证功能。这项技术通过利用用户交互、视觉识别和动态效果,为用户提供了一种全新、有趣且高效的验证方式。本文将深入探讨如何实现这一引人注目的图片旋转验证功能,让您轻松保护网站安全,同时提升用户体验

效果展示
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项目登录模块滑块拼图验证功能实现(纯前端)
E-->