Vue 滑块验证组件实战:支持自定义图片与拖拽交互
在实际项目中,我们经常遇到需要防刷或人机验证的场景。原生实现滑块验证码不仅繁琐,还要处理移动端兼容和边界限制。这里分享一个基于 Vue 的滑块验证组件方案,支持自定义背景图和滑块样式,同时兼顾了鼠标和触摸事件。
核心逻辑拆解
这个组件的核心在于子组件对拖拽行为的控制。我们需要监听 mousedown、mousemove 和 mouseup(以及对应的 touch 事件),计算偏移量并限制在容器范围内。此外,为了防止页面滚动穿透,还需要动态锁定 body 的滚动位置。
子组件实现
子组件负责渲染界面和处理交互。通过 data 属性接收配置,如宽高、图片路径等,并通过 $emit 向父组件反馈拖动状态。
<template>
<div style="padding: 6px 7px">
<!-- 背景图区域 -->
<div class="box" :style="getStyle(1)">
<img :style="getStyle(1)" :src="\`data:image/jpeg;base64,${data.image}\`" alt="">
<!-- 缺口位置 -->
<div class="slids" :style="{'left': elePos.x + 'px', 'top': elePos.y + 'px', ...getStyle(3) }">
<img style="width: 100%; height: 100%" :src="\`data:image/jpeg;base64,${data.slide}\`" alt="">
</div>
</div>
<!-- 滑块轨道 -->
<div class="slidBox" :style="getStyle(2)">
<div class="slidBoxItem"></div>
<img class="slid"
:style="{width: '52px', height: '52px', margin: '1px 0 0 0','left': elePos.x + 'px', 'top': elePos.y + 'px' }"
src="@/assets/images/saleAndPurchase/handler_bg.png" alt=""
@mousedown.self="dragStartHandler"
@touchstart.stop="dragStartHandler"
@mousemove="draggingHandler"
@touchmove.stop="draggingHandler"
@mouseup="dragEndHandler"
@touchend.stop="dragEndHandler"
>
</div>
<!-- 操作按钮 -->
<div class="repossessColse">
<img @click.stop="close" style="width: 24px; height: 24px; margin: 0 16px" src="@/assets/images/saleAndPurchase/closes.png" alt="">
<img @click.stop="repossess" style="width: 24px; height: 24px" src="@/assets/images/saleAndPurchase/repossess.png" alt="">
</div>
</div>
</template>
<script>
/**
* 必传参数 data
* data={ width:number,height:number,randomStr:string,pos:{height:number},slideWidth:number,slideHeight:number,image:string}
* ******************************
* 回调方法 callBack(type,distance)
* type: 按下滑块|拖动滑块|松开滑块
* distance: 滑块已滑动的距离
*/
export default {
props:['data'],
components: {},
data() {
return {
fatherbox: null,
innerbox: null,
slidsbox: null,
// 元素位置,用于样式绑定,动态更新位置
elePos: {
x: null,
y: null
},
// 手指(鼠标)触摸点距离拖拽元素左上角的距离
diffOfTarget: {
x: 0,
y: 0
}
};
},
mounted() {
this.$nextTick(() => {
// 注意:实际项目中需确保这些选择器在当前 DOM 中可用
this.fatherbox = document.querySelector('.van-popup');
this.innerbox = document.querySelector('.slid');
this.slidsbox = document.querySelector('.slids');
});
},
methods: {
getStyle(e){
if(e == 1){
return {
width: this.data.width+'px',
height: this.data.height+'px',
}
}else if(e == 2){
return {
width: this.data.width+'px',
}
}else if(e == 3){
return {
width: this.data.slideWidth+'px',
height: this.data.slideHeight+'px',
top: this.data.pos.height + 6 + 'px',
}
}
},
dragStartHandler (e) {
let touch = e
if (e.touches) {
touch = e.touches[0]
}
// 记录初始偏移量
this.diffOfTarget.x = touch.clientX - e.target.offsetLeft
this.diffOfTarget.y = touch.clientY - e.target.offsetTop
// 解决滑动穿透问题,固定 body 滚动位置
let scrollTop = document.scrollingElement.scrollTop
document.body.classList.add('modal-open')
document.body.style.top = -scrollTop + 'px'
let dragEndHandlerLeft = this.innerbox.offsetLeft-7
this.$emit('callBack','dragStartHandler',dragEndHandlerLeft)
},
draggingHandler (e) {
let touch = e
if (e.touches) {
touch = e.touches[0]
}
// 设置拖拽元素的位置
this.elePos.x = touch.clientX - this.diffOfTarget.x
// 限制元素不能超过滑道范围
let limitWidth = this.fatherbox.offsetWidth - this.slidsbox.offsetWidth - 7
let limitHeight = window.screen.height
if (this.elePos.x < 7) {
this.elePos.x = 7
} else if (this.elePos.x > limitWidth) {
this.elePos.x = limitWidth
}
if (this.elePos.y < 0) {
this.elePos.y = 0
} else if (this.elePos.y > limitHeight) {
this.elePos.y = limitHeight - e.target.clientHeight
}
let dragEndHandlerLeft = this.innerbox.offsetLeft-7
this.$emit('callBack','draggingHandler',dragEndHandlerLeft)
},
dragEndHandler (e) {
let dragEndHandlerLeft = this.innerbox.offsetLeft-7
// 恢复 body 滚动
document.body.classList.remove('modal-open')
this.$emit('callBack','dragEndHandler',dragEndHandlerLeft)
},
repossess() { // 刷新重置
this.elePos.x = null
this.diffOfTarget.x = 0
let scrollTop = document.scrollingElement.scrollTop
document.body.classList.add('modal-open')
document.body.style.top = -scrollTop + 'px'
document.body.classList.remove('modal-open')
this.$emit('repossess')
},
close() {
this.$emit('close')
}
},
};
</script>
<style lang="scss" scoped>
.box {
width: 350px;
height: 100px;
overflow: hidden;
}
.repossessColse {
height: 40px;
display: flex;
justify-content: flex-end;
flex-direction: row;
align-items:flex-end;
}
.slidBox {
width: 100%;
height: 52px;
overflow: none;
margin: auto;
display: flex;
align-items: center;
justify-content: flex-end;
.slidBoxItem {
width: 98%;
height: 37px;
background-color: #E6E6E6;
border-radius: 50px;
}
&::-webkit-scrollbar {
width: 0px;
height: 0px;
}
.slid {
width: 52px;
height: 52px;
}
}
.slid {
position: fixed;
left: 7px;
bottom: 46px;
cursor: pointer;
}
.slids {
position: fixed;
left: 7px;
top: 0px;
width: 50px;
height: 50px;
cursor: pointer;
}
</style>


