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