1.效果图
2.整体思路是:点登录时触发验证码索取接口取到图片,滑动验证后返回数据给后端验证是否成功(本来做了个纯前端的滑动验证,但是不复合安全性要求被打回去重做了)
<template>
<div class="slider">
<div class="content">
<div class="slider-img-div" id="slider-img-div">
<img id="slider-img" src="" alt />
</div>
<div class="bg-img-div"></div>
</div>
<div class="slider-move">
<div class="slider-move-track">拖动滑块完成拼图</div>
<div class="slider-move-btn" id="slider-move-btn"></div>
</div>
<div class="bottom">
<div class="close-btn" @click="closeClick" id="slider-close-btn"></div>
<div class="refresh-btn" @click="refreshCaptcha" id="slider-refresh-btn"></div>
</div>
</div>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue'
import dayjs from 'dayjs'
import { message } from 'ant-design-vue'
export default defineComponent({
props: {
isShow: {
type: Boolean
}
},
setup(props, context) {
let currentCaptchaConfig = null
const currentCaptchaId = ref(null)
const noticeText = ref(null)
const clearPreventDefault = (event) => {
if (event.preventDefault) {
event.preventDefault()
}
}
// 清除事件的默认行为。
const clearAllPreventDefault = ($div) => {
$div.each(function (index, el) {
el.addEventListener('touchmove', clearPreventDefault, false)
})
}
// 使用滑动条的时间格式
const formaTime = (time) => {
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
}
// 初始化加载配置
const initConfig = (bgImageWidth, bgImageHeight, sliderImageWidth, sliderImageHeight, end) => {
currentCaptchaConfig = {
startTime: new Date(),
trackArr: [],
movePercent: 0,
bgImageWidth, // 背景大图宽度
bgImageHeight, // 背景大图高度
sliderImageWidth, // 滑动的小图的宽度
sliderImageHeight, // 滑动的小图的高度
end
}
return currentCaptchaConfig
}
// 重置操作
const refreshCaptcha = () => {
// jQuery请求接口方式
$.get('/api/captcha/gen', function (data) {
reset()
currentCaptchaId.value = data.data.id // 获取验证码id
const bgImg = $('.bg-img-div')
const sliderImg = $('.slider-img-div')
// 通过接口取值给背景图和滑动图块进行初始化
bgImg.css('background-image', 'url(' + data.data.captcha.backgroundImage + ')')
bgImg.css('width', data.data.captcha.backgroundImageWidth / 2.27)
sliderImg.css('background-image', 'url(' + data.data.captcha.templateImage + ')')
sliderImg.css('width', data.data.captcha.templateImageWidth / 2.27)
initConfig(bgImg.width(), bgImg.height(), sliderImg.width(), sliderImg.height(), 206)
})
}
const reset = () => {
// 重置时默认将滑动条恢复到初始位置
$('#slider-move-btn').css('background-position', '-5px 11.79625%')
$('#slider-move-btn').css('transform', 'translate(0px, 0px)')
$('#slider-img-div').css('transform', 'translate(0px, 0px)')
currentCaptchaId.value = null
}
onMounted(() => {
refreshCaptcha()
clearAllPreventDefault($('.slider'))
$('#slider-move-btn').mousedown(down)
})
// 鼠标点击触发事件
const down = (event) => {
let targetTouches = event.originalEvent ? event.originalEvent.targetTouches : event.targetTouches
let startX = event.pageX
let startY = event.pageY
if (startX === undefined) {
startX = Math.round(targetTouches[0].pageX)
startY = Math.round(targetTouches[0].pageY)
}
currentCaptchaConfig.startX = startX
currentCaptchaConfig.startY = startY
const pageX = currentCaptchaConfig.startX
const pageY = currentCaptchaConfig.startY
const startTime = currentCaptchaConfig.startTime
const trackArr = currentCaptchaConfig.trackArr
trackArr.push({
x: pageX - startX,
y: pageY - startY,
type: 'down',
t: new Date().getTime() - startTime.getTime()
})
// printLog('start', startX, startY)
// pc
window.addEventListener('mousemove', move)
window.addEventListener('mouseup', up)
// 手机端
window.addEventListener('touchmove', move, false)
window.addEventListener('touchend', up, false)
doDown(currentCaptchaConfig)
}
const doDown = () => {
$('#slider-move-btn').css('background-position', '-5px 31.0092%')
}
// 点击滑块移动触发事件
const move = (event) => {
if (event instanceof TouchEvent) {
event = event.touches[0]
}
let pageX = Math.round(event.pageX)
let pageY = Math.round(event.pageY)
const startX = currentCaptchaConfig.startX
const startY = currentCaptchaConfig.startY
const startTime = currentCaptchaConfig.startTime
const end = currentCaptchaConfig.end
const bgImageWidth = currentCaptchaConfig.bgImageWidth
const trackArr = currentCaptchaConfig.trackArr
let moveX = pageX - startX
const track = {
x: pageX - startX,
y: pageY - startY,
type: 'move',
t: new Date().getTime() - startTime.getTime()
}
trackArr.push(track)
if (moveX < 0) {
moveX = 0
} else if (moveX > end) {
moveX = end
}
currentCaptchaConfig.moveX = moveX
currentCaptchaConfig.movePercent = moveX / bgImageWidth
doMove(currentCaptchaConfig)
// printLog('move', track)
}
const doMove = (currentCaptchaConfig) => {
const moveX = currentCaptchaConfig.moveX
$('#slider-move-btn').css('transform', 'translate(' + moveX + 'px, 0px)')
$('#slider-img-div').css('transform', 'translate(' + moveX + 'px, 0px)')
}
// 滑动图块松开后触发事件
const up = (event) => {
window.removeEventListener('mousemove', move)
window.removeEventListener('mouseup', up)
window.removeEventListener('touchmove', move)
window.removeEventListener('touchend', up)
if (event instanceof TouchEvent) {
event = event.changedTouches[0]
}
currentCaptchaConfig.stopTime = new Date()
let pageX = Math.round(event.pageX)
let pageY = Math.round(event.pageY)
const startX = currentCaptchaConfig.startX
const startY = currentCaptchaConfig.startY
const startTime = currentCaptchaConfig.startTime
const trackArr = currentCaptchaConfig.trackArr
const track = {
x: pageX - startX,
y: pageY - startY,
type: 'up',
t: new Date().getTime() - startTime.getTime()
}
trackArr.push(track)
// printLog('up', track)
valid(currentCaptchaConfig)
}
$('#slider-move-btn').mousedown(down)
$('#slider-move-btn').on('touchstart', down)
const valid = (captchaConfig) => {
let data = {
bgImageWidth: captchaConfig.bgImageWidth,
bgImageHeight: captchaConfig.bgImageHeight,
templaterImageWidth: captchaConfig.templateImageWidth,
templateImageHeight: captchaConfig.templateImageHeight,
//startSlidingTime: `${captchaConfig.startTime}-${month}-${day} ${hours}:${minutes}:${seconds}`
startSlidingTime: formaTime(captchaConfig.startTime),
entSlidingTime: formaTime(captchaConfig.stopTime),
trackList: captchaConfig.trackArr
}
// console.log(currentCaptchaId.value)
$.ajax({
type: 'POST',
url: '/api/captcha/check?id=' + currentCaptchaId.value,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(data),
success: function (res) {
// console.log(res)
if (res.data) {
// message.success('验证成功')
// noticeText.value = '验证成功'
context.emit('verify', false)
context.emit('continue', true)
} else {
// message.warning('请重新验证')
// noticeText.value = '请重新验证'
}
refreshCaptcha()
}
})
}
const closeClick = () => {
context.emit('verify', false)
}
return {
currentCaptchaId,
refreshCaptcha,
reset,
down,
doDown,
move,
doMove,
up,
valid,
closeClick,
noticeText
}
}
})
</script>
<style scoped>
.slider {
background-color: #fff;
/* width: 278px; */
height: 285px;
z-index: 999;
box-sizing: border-box;
padding: 9px;
border-radius: 6px;
box-shadow: 0 0 11px 0 #999999;
}
.slider .content {
width: 100%;
height: 159px;
position: relative;
}
.bg-img-div {
width: 100%;
height: 100%;
position: absolute;
transform: translate(0px, 0px);
background-size: 100% 159px;
background-image: none;
background-position: 0 0;
z-index: 0;
}
.slider-img-div {
height: 100%;
width: 100%;
background-size: 100% 159px;
position: absolute;
transform: translate(0px, 0px);
/*border-bottom: 1px solid blue;*/
z-index: 1;
}
.bg-img-div img {
width: 100%;
}
.slider-img-div img {
height: 100%;
}
.slider .slider-move {
height: 60px;
width: 100%;
margin: 11px 0;
position: relative;
}
.slider .bottom {
height: 19px;
width: 100%;
}
.refresh-btn,
.close-btn,
.slider-move-track,
.slider-move-btn {
background: url(https://static.geetest.com/static/ant/sprite.1.2.4.png) no-repeat;
}
.refresh-btn,
.close-btn {
display: inline-block;
}
.slider-move .slider-move-track {
/* background-size: 100% auto; */
line-height: 38px;
font-size: 14px;
text-align: center;
white-space: nowrap;
color: #88949d;
-moz-user-select: none;
-webkit-user-select: none;
user-select: none;
}
.slider {
user-select: none;
}
.slider-move .slider-move-btn {
transform: translate(0px, 0px);
background-position: -5px 11.79625%;
position: absolute;
top: -12px;
left: 0;
width: 66px;
height: 66px;
}
.slider-move .slider-text {
margin: 9px 0;
color: red;
}
.slider-move-btn:hover,
.close-btn:hover,
.refresh-btn:hover {
cursor: pointer;
}
.bottom .close-btn {
width: 20px;
height: 20px;
margin-right: 20px;
background-position: 0 44.86874%;
}
.bottom .refresh-btn {
width: 20px;
height: 20px;
background-position: 0 81.38425%;
}
</style>
3.后端接口设计参照
tianai-captcha-demo: 滑块验证码demo - Gitee.com