vue项目登录模块滑块拼图验证功能实现(纯前端)

Doraemon* 2024-06-28 10:03:03 阅读 51

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

效果展示

在这里插入图片描述

功能介绍:

在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>



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。