Appearance
图片滑动验证的实现
本章主要讲的是登录的时候为了防止机器操作而使用图片拼图进行验证的实现方法。
实现方式
第一步:先将基本样式完善好以及使用到参数准备好
template部分代码
vue
<template>
<div class="sv-box" :style="{ width: w + 'px' }" onselectstart="return false">
<!-- 加载背景图片遮罩层 -->
<div :class="{ 'bg-mask': state.loadFlag }"></div>
<!-- 刷新 -->
<div v-show="show" class="sv-refresh" @click="refresh"></div>
<canvas :width="props.w" :height="props.h" ref="canvas"> </canvas>
<canvas :width="props.w" :height="props.h" ref="block" class="canvas-block"> </canvas>
<!-- 滑动条容器 -->
<div
class="slider"
:class="{
// 滑动过程中的样式
active: state.sliderActive,
// 验证通过时的样式
success: state.sliderSuccess,
// 验证失败时的样式
fail: state.sliderFail
}"
>
<div class="slider--mask" :style="{ width: state.sliderMaskWidth }">
<!-- 滑动块 -->
<div class="slider--inner" :style="{ left: state.sliderLeft }" @mousedown="sliderDown">
<div class="slider--icon"></div>
</div>
</div>
<span class="slider--text">{{ text }}</span>
</div>
</div>
</template>
ts参数部分
ts
const props = defineProps({
l: {
type: Number,
default: 42
},
w: {
type: Number,
default: 310
},
h: {
type: Number,
default: 155
},
text: {
type: String,
default: '向右侧滑动'
},
// 精准度
accuracy: {
type: Number,
default: 5
},
// 是否展示刷新
show: {
type: Boolean,
default: true
},
bgList: {
type: Array,
default: () => [
'https://storage.beta.custouch.com/res/8663/bg1.png',
'https://storage.beta.custouch.com/res/8664/bg2.png',
'https://storage.beta.custouch.com/res/8662/banner2.png',
'https://storage.beta.custouch.com/res/8665/bg3.png',
'https://storage.beta.custouch.com/res/8666/bg4.png',
'https://storage.beta.custouch.com/res/8667/bg5.png'
]
}
})
const state = reactive<Record<string, any>>({
sliderActive: false,
sliderSuccess: false,
sliderFail: false,
canvasCtx: null,
blockCtx: null,
block: null,
blockX: undefined,
blockY: undefined,
L: props.l + 4,
img: undefined,
originX: undefined,
isMouseDown: false,
sliderLeft: '0px',
sliderMaskWidth: 0,
success: false,
loadFlag: false
})
const block = ref<HTMLCanvasElement>()
const canvas = ref<HTMLCanvasElement>()
第二步:添加一个校验区域以及被校验区域
使用canvas画出对应的校验区域以及被校验区域,被校验区域通过canvas的getImageData()复制画布上指定的矩形的像素元素,再通过putImageData()将图像放回画布上。代码如下:
ts
const initDom = () => {
// 获取canvas画布的绘图环境
state.block = block.value
state.canvasCtx = canvas.value?.getContext('2d')
state.blockCtx = state.block.getContext('2d')
}
const initImg = () => {
const img = createImg(() => {
state.loadFlag = false
drawBlock()
state.canvasCtx.drawImage(img, 0, 0, props.w, props.h)
state.blockCtx.drawImage(img, 0, 0, props.w, props.h)
const _y = state.blockY - 1
const imageDate = state.blockCtx.getImageData(state.blockX, _y, state.L, state.L)
state.block.width = state.L
state.blockCtx.putImageData(imageDate, 0, _y)
})
state.img = img
}
const createImg = (onload: any) => {
state.loadFlag = true
const img = new Image()
img.crossOrigin = 'Anonymous'
// 图片预加载方法
img.onload = onload
img.onerror = () => {
console.error('Background image failed to load')
}
img.src = getRandomBg() as string
return img
}
const getRandomBg = () => {
const len = props.bgList.length
return props.bgList[getRandomNumberByRange(0, len)]
}
const getRandomNumberByRange = (start: number, end: number) => {
return Math.round(Math.random() * (end - start) + start)
}
const drawBlock = () => {
state.blockX = getRandomNumberByRange(state.L + 10, props.w - (state.L + 10))
state.blockY = getRandomNumberByRange(10, props.h - (state.L + 10))
draw(state.canvasCtx, state.blockX, state.blockY, 'fill')
draw(state.blockCtx, state.blockX, state.blockY, 'clip')
}
const draw = (ctx: any, x: number, y: number, type: string) => {
const { l } = props
ctx.beginPath()
ctx.rect(x, y, l, l)
ctx.lineWidth = 2
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'
ctx.stroke()
ctx[type]()
// Canvas的合成操作,destination-over目标绘制在源之上
ctx.globalCompositeOperation = 'destination-over'
}
第三步:绑定事件
在滑块上绑定mousedown以及在document上绑定对应的mousemove和mouseup事件,通过计算滑块的移动距离与校验区域所在位置差的绝对值是否小于等于精确度,是的话则说明校验成功,否的话则说明校验失败。代码如下:
ts
const sliderDown = (e: any) => {
if (state.success) return
state.originX = e.clientX
state.isMouseDown = true
}
const bindEvt = () => {
const moveEvt = (e: any) => {
if (!state.isMouseDown) return
const moveX = e.clientX - state.originX
if (moveX < 0 || moveX + props.l >= props.w) return
state.sliderLeft = moveX + 'px'
state.block.style.left = moveX + 'px'
state.sliderActive = true // add active
state.sliderMaskWidth = moveX + 'px'
}
const upEvt = (e: any) => {
if (!state.isMouseDown) return
state.isMouseDown = false
if (e.clientX === state.originX) return
state.sliderActive = false // remove active
const blockLeft = state.block.style.left.replace(/px/gi, '')
if (Math.abs(blockLeft - state.blockX) <= props.accuracy) {
state.sliderSuccess = true
state.success = true
} else {
state.sliderFail = true
setTimeout(() => {
reset()
}, 1000)
}
}
document.addEventListener('mousemove', moveEvt)
document.addEventListener('mouseup', upEvt)
}
校验失败的时候进行reset操作或者用户可以进行手动刷新
ts
const reset = () => {
state.success = false
state.sliderFail = false
state.sliderSuccess = false
state.sliderLeft = 0
state.block.style.left = 0
state.sliderMaskWidth = 0
// canvas
const { w, h } = props
// 清空指定矩形区域内的绘图
state.canvasCtx.clearRect(0, 0, w, h)
state.blockCtx.clearRect(0, 0, w, h)
state.block.width = w
// 生成图片
state.img.src = getRandomBg()
}
const refresh = () => {
reset()
}
展示效果
查看源码
vue
<template>
<div class="sv-box" :style="{ width: w + 'px' }" onselectstart="return false">
<!-- 加载背景图片遮罩层 -->
<div :class="{ 'bg-mask': state.loadFlag }"></div>
<!-- 刷新 -->
<div v-show="show" class="sv-refresh" @click="refresh"></div>
<canvas :width="props.w" :height="props.h" ref="canvas"> </canvas>
<canvas :width="props.w" :height="props.h" ref="block" class="canvas-block"> </canvas>
<!-- 滑动条容器 -->
<div
class="slider"
:class="{
// 滑动过程中的样式
active: state.sliderActive,
// 验证通过时的样式
success: state.sliderSuccess,
// 验证失败时的样式
fail: state.sliderFail
}"
>
<div class="slider--mask" :style="{ width: state.sliderMaskWidth }">
<!-- 滑动块 -->
<div class="slider--inner" :style="{ left: state.sliderLeft }" @mousedown="sliderDown">
<div class="slider--icon"></div>
</div>
</div>
<span class="slider--text">{{ text }}</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, reactive } from 'vue'
const props = defineProps({
l: {
type: Number,
default: 42
},
w: {
type: Number,
default: 310
},
h: {
type: Number,
default: 155
},
text: {
type: String,
default: '向右侧滑动'
},
// 精准度
accuracy: {
type: Number,
default: 5
},
// 是否展示刷新
show: {
type: Boolean,
default: true
},
bgList: {
type: Array,
default: () => [
'https://storage.beta.custouch.com/res/8663/bg1.png',
'https://storage.beta.custouch.com/res/8664/bg2.png',
'https://storage.beta.custouch.com/res/8662/banner2.png',
'https://storage.beta.custouch.com/res/8665/bg3.png',
'https://storage.beta.custouch.com/res/8666/bg4.png',
'https://storage.beta.custouch.com/res/8667/bg5.png'
]
}
})
const state = reactive<Record<string, any>>({
sliderActive: false,
sliderSuccess: false,
sliderFail: false,
canvasCtx: null,
blockCtx: null,
block: null,
blockX: undefined,
blockY: undefined,
L: props.l + 4,
img: undefined,
originX: undefined,
isMouseDown: false,
sliderLeft: '0px',
sliderMaskWidth: 0,
success: false,
loadFlag: false
})
const block = ref<HTMLCanvasElement>()
const canvas = ref<HTMLCanvasElement>()
const init = () => {
initDom()
initImg()
bindEvt()
}
const initDom = () => {
// 获取canvas画布的绘图环境
state.block = block.value
state.canvasCtx = canvas.value?.getContext('2d')
state.blockCtx = state.block.getContext('2d')
}
const initImg = () => {
const img = createImg(() => {
state.loadFlag = false
drawBlock()
state.canvasCtx.drawImage(img, 0, 0, props.w, props.h)
state.blockCtx.drawImage(img, 0, 0, props.w, props.h)
const _y = state.blockY - 1
const imageDate = state.blockCtx.getImageData(state.blockX, _y, state.L, state.L)
state.block.width = state.L
state.blockCtx.putImageData(imageDate, 0, _y)
})
state.img = img
}
const bindEvt = () => {
const moveEvt = (e: any) => {
if (!state.isMouseDown) return
const moveX = e.clientX - state.originX
if (moveX < 0 || moveX + props.l >= props.w) return
state.sliderLeft = moveX + 'px'
state.block.style.left = moveX + 'px'
state.sliderActive = true // add active
state.sliderMaskWidth = moveX + 'px'
}
const upEvt = (e: any) => {
if (!state.isMouseDown) return
state.isMouseDown = false
if (e.clientX === state.originX) return
state.sliderActive = false // remove active
const blockLeft = state.block.style.left.replace(/px/gi, '')
if (Math.abs(blockLeft - state.blockX) <= props.accuracy) {
state.sliderSuccess = true
state.success = true
} else {
state.sliderFail = true
setTimeout(() => {
reset()
}, 1000)
}
}
document.addEventListener('mousemove', moveEvt)
document.addEventListener('mouseup', upEvt)
}
const sliderDown = (e: any) => {
if (state.success) return
state.originX = e.clientX
state.isMouseDown = true
}
const reset = () => {
state.success = false
state.sliderFail = false
state.sliderSuccess = false
state.sliderLeft = 0
state.block.style.left = 0
state.sliderMaskWidth = 0
// canvas
const { w, h } = props
// 清空指定矩形区域内的绘图
state.canvasCtx.clearRect(0, 0, w, h)
state.blockCtx.clearRect(0, 0, w, h)
state.block.width = w
// 生成图片
state.img.src = getRandomBg()
}
const refresh = () => {
reset()
}
const drawBlock = () => {
state.blockX = getRandomNumberByRange(state.L + 10, props.w - (state.L + 10))
state.blockY = getRandomNumberByRange(10, props.h - (state.L + 10))
draw(state.canvasCtx, state.blockX, state.blockY, 'fill')
draw(state.blockCtx, state.blockX, state.blockY, 'clip')
}
const draw = (ctx: any, x: number, y: number, type: string) => {
const { l } = props
ctx.beginPath()
ctx.rect(x, y, l, l)
ctx.lineWidth = 2
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'
ctx.stroke()
ctx[type]()
// Canvas的合成操作
ctx.globalCompositeOperation = 'destination-over'
}
const createImg = (onload: any) => {
state.loadFlag = true
const img = new Image()
img.crossOrigin = 'Anonymous'
// 图片预加载方法
img.onload = onload
img.onerror = () => {
console.error('Background image failed to load')
}
img.src = getRandomBg() as string
return img
}
const getRandomBg = () => {
const len = props.bgList.length
return props.bgList[getRandomNumberByRange(0, len)]
}
const getRandomNumberByRange = (start: number, end: number) => {
return Math.round(Math.random() * (end - start) + start)
}
onMounted(() => {
init()
})
</script>
<style scoped>
.sv-box{
position: relative;
margin: 20px;
}
.sv-box .bg-mask{
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.9);
z-index: 999;
animation: loading 1.5s infinite;
}
@keyframes loading{
0% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.sv-box .canvas-block{
position: absolute;
left: 0;
top: 0;
}
.sv-box .sv-refresh{
position: absolute;
right: 0;
top: 0;
width: 34px;
height: 34px;
cursor: pointer;
background: url('./images/icons-sprite.png');
background-position: 34px 35px;
z-index: 999;
}
.sv-box .slider{
position: absolute;
text-align: center;
width: 100%;
height: 40px;
line-height: 40px;
margin-top: 15px;
color: #45494c;
border: 1px solid #e4e7eb;
background: #f7f9fa;
}
.sv-box .slider .slider--mask{
position: absolute;
left: -1px;
top: -1px;
height: 40px;
border: 1px solid transparent;
}
.sv-box .slider .slider--mask .slider--inner{
position: absolute;
top: 0;
left: 0;
width: 40px;
height: 40px;
background: #fff;
cursor: pointer;
transition: background 0.2 linear;
}
.sv-box .slider .slider--mask .slider--inner .slider--icon{
position: absolute;
top: 15px;
left: 13px;
width: 14px;
height: 12px;
background: url('./images/icons-sprite.png');
background-position: 34px 445px;
}
.sv-box .slider .slider--mask .slider--inner:hover{
background: #1991fa;
}
.sv-box .slider .slider--mask .slider--inner:hover .slider--icon{
background-position: 34px 458px;
}
.sv-box .active .slider--mask{
border: 1px solid #1991fa;
}
.sv-box .active .slider--inner{
top: -1px !important;
border: 1px solid #1991fa;
background-color: #1991fa !important;
}
.sv-box .success .slider--mask{
border: 1px solid #52ccba;
}
.sv-box .success .slider--inner{
top: -1px !important;
border: 1px solid #52ccba;
background-color: #52ccba !important;
}
.sv-box .success .slider--icon{
background-position: 0 0 !important;
}
.sv-box .fail .slider--mask{
border: 1px solid #f57a7a;
background-color: #fce1e1;
}
.sv-box .fail .slider--inner{
top: -1px !important;
border: 1px solid #f57a7a;
background-color: #f57a7a !important;
}
.sv-box .active .slider--text,.sv-box .success .slider--text,.sv-box .fail .slider--text{
display: none;
}
</style>