183 lines
5.0 KiB
Vue
183 lines
5.0 KiB
Vue
<template>
|
||
<div class="login-auth">
|
||
<div class="slider-container"
|
||
@mousedown="startDrag" @mousemove="handleDrag"
|
||
@mouseup="endDrag" @mouseleave="endDrag"
|
||
@touchstart="startDrag" @touchmove="handleDrag" @touchend="endDrag">
|
||
<div ref="draggableRef" class="draggable"
|
||
:style="{ left: `${loginAuth.dragLeft}px` }">
|
||
<i v-if="authStore.isVerified" class="fas fa-check"></i>
|
||
<i v-else class="fas fa-arrow-right"></i>
|
||
</div>
|
||
<div :style="{ width: loginAuth.progressWidth + '%' }" class="slider-progress"></div>
|
||
<div class="slider-message" :style="{color: authStore.isVerified ? '#FFFFFF' : '#696969'}">
|
||
<span v-if="authStore.isVerified">验证成功!</span>
|
||
<span v-else-if="loginAuth.isDragging">拖动中...</span>
|
||
<span v-else>向右拖动滑块 完成验证</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { reactive, ref } from 'vue'
|
||
import { useAuthStore } from '@/store/auth'
|
||
|
||
const draggableRef = ref(null)
|
||
const authStore = useAuthStore()
|
||
|
||
const loginAuth = reactive({
|
||
isDragging: false,
|
||
initialX: 0,
|
||
offsetX: 0,
|
||
dragLeft: 0,
|
||
progressWidth: 0,
|
||
targetWidth: 100,
|
||
// 记录拖动的开始时间
|
||
startTime: 0,
|
||
// 记录拖动过程中的位置
|
||
positions: [],
|
||
// 记录拖动过程中的时间戳
|
||
timestamps: []
|
||
})
|
||
|
||
const startDrag = (event) => {
|
||
// 验证成功后禁止拖动
|
||
if (authStore.isVerified) return
|
||
loginAuth.isDragging = true
|
||
|
||
const touch = event.changedTouches ? event.changedTouches[0] : event
|
||
loginAuth.initialX = touch.clientX
|
||
loginAuth.offsetX = draggableRef.value?.offsetLeft
|
||
|
||
loginAuth.startTime = Date.now()
|
||
loginAuth.positions = []
|
||
loginAuth.timestamps = []
|
||
loginAuth.positions.push(loginAuth.initialX)
|
||
loginAuth.timestamps.push(loginAuth.startTime)
|
||
}
|
||
|
||
const handleDrag = (event) => {
|
||
if (loginAuth.isDragging && !authStore.isVerified) {
|
||
const touch = event.changedTouches ? event.changedTouches[0] : event
|
||
const dx = touch.clientX - loginAuth.initialX
|
||
const newLeft = loginAuth.offsetX + dx
|
||
const sliderWidth = draggableRef.value?.parentNode.offsetWidth
|
||
const handleWidth = draggableRef.value?.offsetWidth
|
||
|
||
if (newLeft < 0) {
|
||
loginAuth.dragLeft = 0
|
||
loginAuth.progressWidth = 0
|
||
} else if (newLeft > sliderWidth - handleWidth) {
|
||
loginAuth.dragLeft = sliderWidth - handleWidth
|
||
loginAuth.progressWidth = loginAuth.targetWidth
|
||
} else {
|
||
loginAuth.dragLeft = newLeft
|
||
loginAuth.progressWidth = (newLeft / (sliderWidth - handleWidth)) * loginAuth.targetWidth
|
||
}
|
||
|
||
loginAuth.positions.push(touch.clientX)
|
||
loginAuth.timestamps.push(Date.now())
|
||
}
|
||
}
|
||
|
||
const endDrag = () => {
|
||
loginAuth.isDragging = false
|
||
if (loginAuth.progressWidth === loginAuth.targetWidth && !checkIsHuman()) {
|
||
authStore.isVerified = true
|
||
} else {
|
||
loginAuth.dragLeft = 0
|
||
loginAuth.progressWidth = 0
|
||
}
|
||
}
|
||
|
||
const checkIsHuman = () => {
|
||
const endTime = Date.now()
|
||
const dragDuration = endTime - loginAuth.startTime
|
||
|
||
const speedChanges = []
|
||
for (let i = 1; i < loginAuth.positions.length; i++) {
|
||
const distance = loginAuth.positions[i] - loginAuth.positions[i - 1]
|
||
const time = loginAuth.timestamps[i] - loginAuth.timestamps[i - 1]
|
||
const speed = time > 0 ? distance / time : 0
|
||
speedChanges.push(speed)
|
||
}
|
||
|
||
// 计算速度变化的标准差
|
||
const meanSpeed = speedChanges.reduce((sum, speed) => sum + speed, 0) / speedChanges.length
|
||
const variance = speedChanges.reduce((sum, speed) => sum + Math.pow(speed - meanSpeed, 2), 0) / speedChanges.length
|
||
const stdDevSpeed = Math.sqrt(variance)
|
||
|
||
// 检查是否有停顿
|
||
let hasPause = false
|
||
for (let i = 1; i < loginAuth.timestamps.length; i++) {
|
||
if (loginAuth.timestamps[i] - loginAuth.timestamps[i - 1] > 200) {
|
||
hasPause = true
|
||
break
|
||
}
|
||
}
|
||
|
||
// 综合判断是否为机器操作
|
||
return dragDuration >= 100 && stdDevSpeed >= 0.1 && hasPause
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.login-auth {
|
||
width: 100%;
|
||
|
||
.slider-container {
|
||
position: relative;
|
||
height: 32px;
|
||
background-color: #E7E9E8;
|
||
border: 1px solid #E7E9E8;
|
||
border-radius: 5px;
|
||
overflow: hidden;
|
||
|
||
.draggable {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 35px;
|
||
height: 32px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background-color: var(--el-color-primary);
|
||
color: #FFFFFF;
|
||
cursor: pointer;
|
||
z-index: 3;
|
||
border-left: 1px solid #E7E9E8;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.slider-progress {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
height: 100%;
|
||
background-color: var(--el-color-primary);
|
||
z-index: 1;
|
||
}
|
||
|
||
.slider-message {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 2;
|
||
user-select: none;
|
||
|
||
span {
|
||
transition: opacity 0.3s ease;
|
||
opacity: 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|