前端滑块旋转验证登录
效果图如下
实现: 封装VerifyImg组件
<template>
<el-dialog v-model="dialogShow" width="380px" top="24vh" class="verifyDialog">
<div class="verify-v">
<div
class="check"
@mousedown="onMouseDown"
@mouseup="onMouseUp"
@mousemove="onMouseMove"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<p>拖动滑块使图片角度为正</p>
<div class="img-con">
<img :src="imgUrl" :style="{ transform: imgAngle }" />
<div v-if="showError" class="check-state">验证失败</div>
<div v-else-if="showSuccess" class="check-state">验证成功</div>
<div v-else-if="checking" class="check-state">验证中</div>
</div>
<div
ref="sliderCon"
class="slider-con"
:class="{ 'err-anim': showError }"
:style="{ '--percent': percent, '--bgColor': showError ? bgError : bgColor }"
>
<div ref="slider" class="slider" id="slider" :class="{ sliding }" :style="{ '--move': `${slidMove}px` }">
<el-icon size="22"><Right id="slider" /></el-icon>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
imgUrl: '',
dialogShow: false,
showError: false,
showSuccess: false,
checking: false,
sliding: false,
slidMove: 0,
percent: 0,
sliderConWidth: 0,
bgColor: 'rgba(25, 145, 250, 0.2)',
bgError: 'rgba(255,78,78,0.2)',
imgList: [
new URL(`../../assets/images/verify/fn1.png`, import.meta.url).href,
new URL(`../../assets/images/verify/fn2.png`, import.meta.url).href,
new URL(`../../assets/images/verify/fn3.png`, import.meta.url).href
]
}
},
computed: {
angle() {
let sliderConWidth = this.sliderConWidth ?? 0
let sliderWidth = this.sliderWidth ?? 0
let ratio = this.slidMove / (sliderConWidth - sliderWidth)
return 360 * ratio
},
imgAngle() {
return `rotate(${this.angle}deg)`
}
},
mounted() {
this.imgUrl = this.imgList[this.rand(0, 2)]
},
methods: {
onTouchMove(event) {
console.log('onTouchMove')
if (this.sliding && this.checking === false) {
// 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
let m = event.touches[0].clientX - this.sliderLeft
if (m < 0) {
// 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
// 所以直接等于 0,以防止越界
m = 0
} else if (m > this.sliderConWidth - this.sliderWidth) {
// 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
// 因为css的 translateX 函数是以元素的左上角坐标计算的
// 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
m = this.sliderConWidth - this.sliderWidth
}
this.slidMove = m
console.log(this.slidMove)
this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
}
},
onTouchEnd() {
console.log('onTouchEnd')
if (this.sliding && this.checking === false) {
this.checking = true
this.validApi(this.angle)
.then(isok => {
if (isok) {
this.showSuccess = true
} else {
this.showError = true
}
return new Promise((resolve, reject) => {
// setTimeout(() => {
if (isok) {
resolve(Math.round(this.angle))
} else {
reject()
}
setTimeout(() => {
this.resetSlider()
}, 1000)
// }, 1500)
})
})
.then(angle => {
// 处理业务,或者通知调用者验证成功
console.log(angle)
this.$emit('toLogin')
})
.catch(e => {
console.log(e)
// alert('旋转错误')
})
}
},
onTouchStart(event) {
console.log('onTouchStart', event)
// 设置状态为滑动中
this.sliding = true
// 下面三个变量不需要监听变化,因此不放到 data 中
this.sliderLeft = event.touches[0].clientX // 记录鼠标按下时的x位置
this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
console.log(this.sliderLeft, this.sliderConWidth, this.sliderWidth)
},
rand(m, n) {
return Math.ceil(Math.random() * (n - m + 1) + m - 1)
},
showVerify() {
this.imgUrl = this.imgList[this.rand(0, 2)]
this.dialogShow = true
},
closeVerify() {
//1.5s后关闭弹框
setTimeout(() => {
this.dialogShow = false
}, 1500)
},
// 重置滑块
resetSlider() {
this.sliding = false
this.slidMove = 0
this.checking = false
this.showSuccess = false
this.showError = false
this.percent = 0
},
//拖拽开始
onMouseDown(event) {
console.log(event.target.id, this.checking)
if (event.target.id !== 'slider') {
return
}
if (this.checking) return
// 设置状态为滑动中
this.sliding = true
// 下面三个变量不需要监听变化,因此不放到 data 中
this.sliderLeft = event.clientX // 记录鼠标按下时的x位置
this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
},
//拖拽停止
onMouseUp(event) {
if (this.sliding && this.checking === false) {
this.checking = true
this.validApi(this.angle)
.then(isok => {
if (isok) {
this.showSuccess = true
} else {
this.showError = true
}
return new Promise((resolve, reject) => {
// setTimeout(() => {
if (isok) {
resolve(Math.round(this.angle))
} else {
reject()
}
setTimeout(() => {
this.resetSlider()
}, 1000)
// }, 1500)
})
})
.then(angle => {
// 处理业务,或者通知调用者验证成功
console.log(angle)
this.$emit('toLogin')
})
.catch(e => {
console.log(e)
// alert('旋转错误')
})
}
},
//拖拽进行中
onMouseMove(event) {
if (this.sliding && this.checking === false) {
// 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
let m = event.clientX - this.sliderLeft
if (m < 0) {
// 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
// 所以直接等于 0,以防止越界
m = 0
} else if (m > this.sliderConWidth - this.sliderWidth) {
// 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
// 因为css的 translateX 函数是以元素的左上角坐标计算的
// 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
m = this.sliderConWidth - this.sliderWidth
}
this.slidMove = m
this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
}
},
// 验证角度是否正确
validApi(angle) {
return new Promise((resolve, reject) => {
// 模拟网络请求
setTimeout(() => {
// 图片已旋转的角度
const imgAngle = 90
// 图片已旋转角度和用户旋转角度之和
let sum = imgAngle + angle
// 误差范围
const errorRang = 20
// 当用户旋转角度和已旋转角度之和为360度时,表示旋转了一整圈,也就是转正了
// 但是不能指望用户刚好转到那么多,所以需要留有一定的误差
let isOk = Math.abs(360 - sum) <= errorRang
resolve(isOk)
}, 1000)
})
}
}
}
</script>
<style lang="scss">
.verifyDialog {
.el-dialog__body {
padding: 15px !important;
}
}
</style>
<style lang="scss" scoped>
.verify-v {
display: flex;
justify-content: center;
align-items: center;
}
.check {
--slider-size: 40px;
width: 300px;
background: white;
box-shadow: 0px 0px 12px rgb(0 0 0 / 8%);
border-radius: 5px;
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: center;
.img-con {
position: relative;
overflow: hidden;
width: 120px;
height: 120px;
border-radius: 50%;
margin-top: 20px;
img {
width: 100%;
height: 100%;
user-select: none;
}
.check-state {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: white;
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.check .slider-con {
width: 80%;
height: var(--slider-size);
border-radius: 3px;
margin-top: 1rem;
position: relative;
background: #f5f5f5;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1) inset;
background: linear-gradient(to right, var(--bgColor) 0%, var(--bgColor) var(--percent), #fff var(--percent), #fff 100%);
.slider {
&:hover {
background: #1991fa;
color: #fff;
}
background: #fff;
width: var(--slider-size);
height: var(--slider-size);
border-radius: 3px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
cursor: move;
display: flex;
justify-content: center;
align-items: center;
--move: 0px;
transform: translateX(var(--move));
.sliding {
background: #4ed3ff;
}
}
}
.slider-con.err-anim {
animation: jitter 0.5s;
.slider {
background: #ff4e4e;
}
}
body {
padding: 0;
margin: 0;
background: #fef5e0;
}
@keyframes jitter {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(10px);
}
60% {
transform: translateX(-5px);
}
80% {
transform: translateX(10px);
}
100% {
transform: translateX(0);
}
}
</style>
使用
<VerifyImg ref="verifyRef" @to-login="handleLogin"></VerifyImg>
handleLogin(){
...
}
作者:点赞侠01
来源:juejin.cn/post/7358004857889275958
来源:juejin.cn/post/7358004857889275958