Vue3.0 PC端滑块拼图验证,配合后端验证
简介
最近因为产品需要实现一个滑块拼图验证,而且需要配合后端进行验证,不想接入第三方SDK,所以自己手写了一个,主要是配合element plus 实现UI大致框架,背景图片和拼图都是通过后端接口获取,通过CryptoJS对滑块滑动距离以及当前拼图唯一标识和秘钥进行加密处理,配合后端进行验证。 使用requestAnimationFrame 优化性能 为了防止滑块卡顿,有问题欢迎评论交流,另外还有小程序版本的 下次发布
安装CryptoJS库
yarn add crypto-js 或 npm i crypto-js
效果图
后端接口返回数据
获取图片的接口
背景图和拼图都是base64格式的
验证接口看后端怎么写
实现代码
<template>
<a-modal
:visible="store.state.isVerify"
:footer="null"
:maskClosable="false"
:closable="false"
width="350px"
:body-style="{ padding: '20px' }"
@cancel="onClose"
centered
>
<div class="image-body">
<div class="verify-title">
请先完成以下安全验证:
</div>
<a-spin :spinning="loading" tip="加载中...">
<div v-if="loading" class="image-div"></div>
</a-spin>
<div v-if="!loading&&bkImage&&slideImage" class="image-div">
<ElImage class="image-bk" :src="bkImage"></ElImage>
<ElImage
:style="{ marginLeft: marginLeft + 'px' }"
class="image-slide"
:src="slideImage"
></ElImage>
</div>
<div class="image-slide-div">
<div class="image-slide-text">
<span class="image-slide-tips">
{
{
showTips ? "向右拖动滑块完成拼图" : " " }}
</span>
</div>
<div :style="slideStyle" class="slide-div">
<ElButton
:style="slideButtonStyle"
@mousedown="handleDrag"
class="slide-button"
:round="false"
type="primary"
plain
>
<div class="iconbox">
<i
class="iconfont icon-jiantou"
v-if="result === 'default'"
style="color:#52ccba;font-size: 16px"
>
</i>
<i class="iconfont icon-duigou" v-if="result === 'success'" style="color: #fff;font-size: 28px"></i>
<i class="iconfont icon-a-shanchu_huaban1" v-if="result === 'error'" style="color: rgb(246, 185, 186);"></i>
</div>
</ElButton>
</div>
</div>
</div>
</a-modal>
</template>
<script>
import {
onMounted, ref, computed, reactive, nextTick } from 'vue'
import {
Modal, Spin, message } from 'ant-design-vue'
import $http from '@/utils/request/api'
import {
ElImage, ElButton } from 'element-plus'
import {
useStore } from 'vuex'
import {
useRoute, useRouter } from 'vue-router'
import CryptoJS from 'crypto-js'
export default {
components: {
ElImage,
ElButton,
'a-modal': Modal,
'a-spin': Spin
},
setup () {
const route = useRoute()
const router = useRouter()
const store = useStore()
const loading = ref(false)
// 验证码背景图片
const bkImage = ref('')
// 验证码滑块图片
const slideImage = ref('')
const secretKey = ref('')
const currentToken = ref('')
// 是否显示提示文字
const showTips = ref(true)
// 验证码滑块图片移动量
const marginLeft = ref(0)
// 验证码状态
const result = ref('default')
// 滑动背景样式
const slideStyleJson = reactive({
})
// 滑块按钮样式
const slideButtonStyleJson = reactive({
})
const slideStyle = computed(() => {
return slideStyleJson
})
const slideButtonStyle = computed(() => {
return slideButtonStyleJson
})
function loadImage () {
loading.value = true
$http.getSlider().then((res) => {
loading.value = !loading.value
bkImage.value = res.data.original_img_base_64
slideImage.value = res.data.slider_img_base_64
secretKey.value = res.data.secret_key
currentToken.value = res.data.token
})
}
function onClose () {
store.commit('SET_IS_VERIFY', false)
}
/**
* 改变拖动时改变
*/
function dragChangeSildeStyle () {
slideStyleJson.background = 'rgba(25,145,250,0.5)'
slideStyleJson.transition = null
slideButtonStyleJson.transition = null
}
/**
* 验证成功
*/
const reload = () => {
store.commit('SET_IS_RELOAD', true)
nextTick(() => {
store.commit('SET_IS_RELOAD', false)
})
}
function handleSuccess () {
result.value = 'success'
slideStyleJson.background = '#d2f4ef'
slideButtonStyleJson.background = '#52ccba'
slideButtonStyleJson.color = 'white'
// slideButtonStyleJson.transform = 'translateX(0px)'
slideButtonStyleJson.border = '1px solid #52ccba'
message.success('验证成功')
//这里写自己的代码,以下代码只是参考(可自己封装)
setTimeout(() => {
if (route.path.split('/').includes('detail')) {
router.go(0)
} else {
reload()
}
onClose()
}, 400)
}
/**
* 验证失败
*/
function handleError () {
result.value = 'error'
slideStyleJson.background = 'rgba(245,122,122,0.5)'
slideButtonStyleJson.background = '#f57a7a'
slideButtonStyleJson.transition = 'transform 0.5s'
slideButtonStyleJson.color = 'white'
slideButtonStyleJson.border = '1px solid #f57a7a'
message.error('验证失败请重试')
//这里写自己的代码,以下代码只是参考(可自己封装)
setTimeout(() => {
handleReset()
}, 300)
}
/**
* 重置验证码
*/
function handleReset () {
result.value = 'default'
marginLeft.value = 0
slideStyleJson.width = '0px'
slideButtonStyleJson.marginLeft = '0px'
slideButtonStyleJson.transform = 'translateX(0px)'
slideButtonStyleJson.color = null
slideButtonStyleJson.border = null
slideButtonStyleJson.background = null
// slideStyleJson.transition = 'all ease 0.5s'
slideButtonStyleJson.transition = 'margin-left 0.5s'
showTips.value = true
loadImage()
}
onMounted(() => {
loadImage()
})
// 添加移动事件
function handleDrag (c) {
let moveX = 0; let offset = 0; const y = 5
const clickX = c.clientX
dragChangeSildeStyle()
showTips.value = false
const handleMove = (e) => {
moveX = e.clientX
offset = Math.min(Math.max(moveX - clickX, 0), 260)
slideStyleJson.width = offset + 'px'
slideButtonStyleJson.transform = `translateX(${
offset}px)`
marginLeft.value = offset
}
const handleUp = async () => {
document.removeEventListener('mousemove', handleMove)
document.removeEventListener('mouseup', handleUp)
// 校验验证码
// aes加密
const key = CryptoJS.enc.Utf8.parse(secretKey.value)
const srcs = CryptoJS.enc.Utf8.parse(JSON.stringify({
x: offset, y: y }))
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
// 将ciphertext字符串转二进制
const params = {
point_json: CryptoJS.enc.Base64.stringify(encrypted.ciphertext),
token: currentToken.value
}
const res = await $http.verifyCodeCheck(params)
if (res.code === 0) {
// 成功
handleSuccess()
} else {
// 失败
handleError()
}
}
document.addEventListener('mousemove', handleMove)
document.addEventListener('mouseup', handleUp)
requestAnimationFrame(() => {
handleMove(c)
})
}
return {
onClose,
loadImage,
bkImage,
loading,
slideImage,
marginLeft,
handleDrag,
slideStyle,
slideButtonStyle,
result,
showTips,
store
}
}
}
</script>
<style lang="less" scoped>
.iconbox{
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.iconfont{
font-size: 20px;
color: #333;
}
.image-body {
margin: 0 auto;
width: 310px;
margin-top: 10px;
.verify-title{
font-size: 14px;
color: #333;
margin-bottom: 10px;
}
.image-div {
width: 310px;
height: 155px;
background: rgb(153, 216, 197);
.image-bk {
width: 310px;
height: 155px;
z-index: 1;
position: absolute;
}
.image-slide {
width: 50px;
height: 155px;
position: absolute;
z-index: 2;
}
}
.image-slide-div {
width: 310px;
height: 39px;
margin-top: 15px;
position: relative;
.image-slide-text {
text-align: center;
background: #eef1f8;
border: 1px solid #ebebeb;
.image-slide-tips {
display: inline-block;
font-size: 14px;
color: #b7bcd1;
line-height: 36px;
height: 36px;
text-align: center;
}
}
.slide-div {
width: 0px;
height: 38px;
margin-top: -39px;
.slide-button {
width: 50px;
height: 38px;
border: none;
border-left: 1px solid;
border-right: 1px solid;
border-bottom: 1px solid;
border-color: #ebebeb;
box-shadow: 0 0 4px #ccc;
background: white;
cursor: pointer;
&:hover {
background: #52ccba;
border-color: #52ccba;
color: white;
.icon-jiantou{
color: #fff!important;
}
}
}
}
}
}
</style>