验证码组件开发之路(2)
自上次完成验证码组件的两种类型之后,这段时间又添加了两种类型上去,因此写个文章记录一下这个过程。很多功能完成可能有更好的方法,欢迎各位大佬指点
附上一次文章链接
一、按规定顺序点击相应字符的验证码
首先来看一下效果
这个的思路就是将传入的字段作为验证码的答案,将每个字符打乱渲染到容器上,每次点击字符再按点击顺序拼接起来和原字符作比较,判断验证是否成功
需要传入的props
字段:
interface PropType{
width?:number, //验证码容器的宽度
bgImgList?:Array<string>, //背景图,每次刷新会从数组中随机挑选一张作为背景
verifyText?:string, //验证码字符串
checkOkColor?:string, //验证成功的提示颜色
checkFailColor?:string, //验证失败的提示颜色
autoRefresh?:boolean, //是否开启自动刷新,开启后在验证失败后会自动刷新
resSize?:number //验证结果的字符大小
}
1、HTML
<div ref="container" class="click-verify_box"
:data-result="result"
:style="{
width:width + 'px',
height:width / 2 + 'px',
backgroundImage:`url(${bgImg})`,
'--result-opacity':resOpacity,
'--res-zindex':res_z_index,
'--res-size':resSize + 'px'
}"
>
</div>
html
部分只需要一个容器就行,验证结果的提示我是用伪元素来显示,所以data-result
就作为伪元素的content
值,resOpacity
和res_z_index
控制伪元素的出现和隐藏
less
部分没什么难度就直接附代码了
<style lang="less" scoped>
.click-verify_box{
background-repeat: no-repeat;
background-size: 100% 100%;
position: relative;
user-select: none;
overflow: hidden;
&::before{
content: attr(data-result);
display: flex;
justify-content: center;
align-items: center;
color: var(--res-color);
position: relative;
z-index: var(--res-zindex);
background-color: rgba(255,255,255,.3);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
font-size: var(--res-size);
font-family:'Times New Roman', Times, serif;
font-weight: 500;
width: 100%;
height: 100%;
opacity: var(--result-opacity);
transition: opacity .3s linear;
}
}
</style>
<style>
.zyhui-verify-code-order-num{
position: absolute;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #fff;
border-radius: 50%;
width: 20px;
height: 20px;
color: #fff;
font-size: 16px;
font-weight: 600;
transform: translate(-50%,-50%);
}
</style>
2、JS
首先,从props
里的bgImgList
中随机选择一张作为背景
const bgImg = ref<any>()
const getBgImg = ()=> {
const img = bgImgList[Math.floor(Math.random()*bgImgList.length)]
bgImg.value = new URL(img,import.meta.url).href
}
接着就是初始化的函数。一开始我想用canvas
作为容器,将验证码字符画到画布上,但是这样在点击的时候很难判断每个字符的点击位置。之后还是选择了为每个字符创建一个span
放到容器中。所以初始化和刷新的时候先判断容易是否有子元素并移除
const container = ref<HTMLDivElement>()
const init = ()=> {
removeChild()
function removeChild() {
const children = (container.value as HTMLDivElement).childNodes
while(children.length){
container.value?.removeChild(children[0])
}
}
}
接着就是将props
中的verifyText
转成数组,循环这个数组,为每个字符生成不同的样式和指定范围的随机位置渲染到容器上,完成后从数组中移除这个元素
const verifyTextArr = verifyText.split('')
const letterWidth = width / verifyTextArr.length
const fragment = document.createDocumentFragment()
while(verifyTextArr.length){
const index = Math.floor(Math.random() * verifyTextArr.length)
const text = verifyTextArr[index]
verifyTextArr.splice(index,1)
const fontSize = Math.floor(Math.random() * width / 10 + width / 7.5)
const span = document.createElement('span')
span.textContent = text
span.style.position = 'absolute'
span.style.transformOrigin = 'center center'
span.style.lineHeight = '1em'
span.style.color = getColor()
span.style.fontSize = fontSize + 'px'
span.style.top = Math.random() * (width / 2 - fontSize) + 'px'
span.style.left = Math.random() * (letterWidth - fontSize) + (verifyText.length - verifyTextArr.length - 1) * letterWidth + 'px'
span.style.transform = `rotate(${Math.floor(Math.random() * 61 - 30)}deg)`
fragment.appendChild(span)
}
(container.value as HTMLDivElement).appendChild(fragment)
function getColor():string {
return `rgb(${Math.floor(Math.random()*256)},${Math.floor(Math.random()*256)},${Math.floor(Math.random()*256)})`
}
还得为每个span
添加点击事件,点击之后将span
的字符添加到结果中,并在字符上层渲染出一个点击顺序的符号。先写出创建这个符号的方法
const orderNum = ref<number>(1)
function createClickOrder(x:number,y:number) {
const clickOrderEl = document.createElement('div');
(container.value as HTMLDivElement).appendChild(clickOrderEl)
clickOrderEl.classList.add('zyhui-verify-code-order-num')
clickOrderEl.textContent = orderNum.value.toString()
clickOrderEl.style.top = y + 'px'
clickOrderEl.style.left = x + 'px'
orderNum.value += 1
}
x、y就是点击时候计算出来的所需渲染符号的坐标
在为span
添加点击事件之前,还要考虑到的是,当点击最后一个字符,会自动进行验证,所以可以使用vue
的watch
函数监听结果字符串和原始字符串的长度相等时进行验证,也可以直接将span
的点击事件中添加的创建字符替换为验证函数,在验证函数中创建点击字符,并判断结果字符串和原始字符串相等时进行验证。这里我选择了后者
const emits = defineEmits(['ok','fail','reFresh'])
const result = ref<string>('')
const resOpacity = ref<number>(0)
const res_z_index = ref<number>(-10)
function checkVerifyCode(x:number,y:number,text:string) {
if(result.value.length !== verifyText.length - 1){
result.value += text
createClickOrder(x,y)
}else{
result.value += text
createClickOrder(x,y)
if(result.value === verifyText){
res_z_index.value = 999
result.value = '🗹'
container.value?.style.setProperty('--res-color',checkOkColor)
resOpacity.value = 1
emits('ok')
}else{
res_z_index.value = 999
result.value = '⛌'
container.value?.style.setProperty('--res-color',checkFailColor)
resOpacity.value = 1
emits('fail')
if(autoRefresh){
setTimeout(() => {
reFresh()
}, 1000)
}
}
}
}
每次验证都会传递相应的事件出去
然后将checkVerifyCode
事件添加到span
的点击事件中就可以了
while(verifyTextArr.length){
//......
span.addEventListener('click',(e)=>{
const x = span.offsetLeft + e.offsetX
const y = span.offsetTop + e.offsetY
checkVerifyCode(x,y,text)
})
}
再补充一个reFresh
的刷新函数
const reFresh = ()=> {
result.value = ''
orderNum.value = 1
res_z_index.value = -10
resOpacity.value = 0
init()
}
最后,将init
和getBgImg
函数在组件挂载的时候执行一下就可以了
onMounted(() => {
getBgImg()
init()
})
二、按文字提示选择相应图片的验证码
老规矩,先看效果:
介绍一下思路。首先这个验证码得从外部接收图片进来,接收的图片分为rightImgs
(代表需要用户选择的图片)和wrongImgs
(代表不需要用户选择的图片),其中rightImgs
中可以传多组图片,每组图片都会有一个type
用来告诉用户需要选择是哪些图片。当初始化时会从rightImgs
中随机挑选一组作为resultImgList
(最终渲染的图片数组),当图片数量大于9张时,则随机挑选9张加入resultImgList
,小于9张时则从wrongImgs
中随机挑选补齐9张图片,以下为示例:
const imgList = ref({
rightImgs:[
{
type:'月亮',
imgs:[
'/hah.png',
'/hah.png',
'/hah.png'
]
}
],
wrongImgs:[
'https://t7.baidu.com/it/u=1956604245,3662848045&fm=193&f=GIF',
'https://t7.baidu.com/it/u=4198287529,2774471735&fm=193&f=GIF',
'https://t7.baidu.com/it/u=1063451194,1129125124&fm=193&f=GIF',
'https://t7.baidu.com/it/u=2374506090,1216769752&fm=193&f=GIF',
'https://t7.baidu.com/it/u=4158958181,280757487&fm=193&f=GIF',
'https://t7.baidu.com/it/u=1423490396,3473826719&fm=193&f=GIF',
'https://t7.baidu.com/it/u=3796392429,3515260353&fm=193&f=GIF',
'https://t7.baidu.com/it/u=3980489931,4090080080&fm=193&f=GIF',
'https://t7.baidu.com/it/u=1463594131,3731527799&fm=193&f=GIF',
'https://t7.baidu.com/it/u=3265501662,2644840891&fm=193&f=GIF',
'https://t7.baidu.com/it/u=3638940699,1955702687&fm=193&f=GIF',
'https://t7.baidu.com/it/u=30263305,3485941528&fm=193&f=GIF'
]
})
需要传入的props
字段:
interface RightImgsType{
type:string,
imgs:Array<string>
}
interface PropType {
ftp_text_style?:{
text:string, //按钮文字
success_text:string, //验证成功按钮文字
color:string, //按钮颜色
success_color:string, // 验证成功按钮颜色
fontSize:string, //按钮字体大小
clickImgColor:string //点击图片时显示的符号颜色
}
imgList:{
rightImgs:Array<RightImgsType>,
wrongImgs:Array<string>,
}
}
const {
ftp_text_style:{ text,color,fontSize,clickImgColor,success_text,success_color },
imgList:{rightImgs,wrongImgs}
} = withDefaults(defineProps<PropType>(),{
ftp_text_style:()=>({
text:'点击验证',
success_text:'验证成功',
color:'rgb(35, 95, 225)',
success_color:'rgb(30, 167, 55)',
fontSize:'16px',
clickImgColor:'#fff'
})
})
1、创建容器和按钮
首先创建一个容器,绑定上一些动态属性,为了防止点击按钮时下方弹出的图片选择器影响页面布局,所以容器使用定位并在之后给图片选择器加上绝对定位和zindex
使选择器固定在按钮下方并在所有图层之上
<script lang="ts" setup>
const CHECK_SUCCESS = 1
const CHECK_FAIL = 2
const CHECK_ORIGINAL = 0
const checkCodeStatus = ref<number>(CHECK_ORIGINAL) // 验证码的三种状态
const isCodeOpen = ref<boolean>(false) //判断是否打开了图片选择器
const showTip = ref<boolean>(false) //图片选择器打开时显示提示文字
const codeType = ref<string>('') //提示用户所需选择的图片
const showCheckSuccess = computed(() => checkCodeStatus.value === CHECK_SUCCESS)//验证成功后控制显示按钮的成功样式
const openCode = ()=> {
//....
}
</script>
<template>
<div style="position: relative;display: inline-block;"
:style="{'--color':color,'--fontSize':fontSize,'--success-color':success_color}"
>
<span
class="verify_btn"
:class="showCheckSuccess?'check-success':'no-check'"
:style="isCodeOpen?`border-color:${color};color:${color}`:''"
@click="openCode"
>
{{ showCheckSuccess?success_text:text }}
</span>
<span class="tip" v-show="showTip">请选择下方所有的{{ codeType }}</span>
</div>
</template>
相应的less
代码:
*{
box-sizing: border-box;
padding: 0;
margin: 0;
user-select: none;
}
.verify_btn{
display: inline-block;
padding: calc(var(--fontSize) * 2 / 3);
border: 1px solid;
border-radius: 5px;
font-size: var(--fontSize);
margin-bottom: 5px;
}
.no-check{
border-color: #ccc;
color: rgb(143, 143, 143);
&:hover{
cursor: pointer;
border-color: var(--color);
color: var(--color);
opacity: .8;
}
&:active{
opacity: 1;
}
}
.check-success{
border-color: var(--success-color);
color: var(--success-color);
&::before{
content: '√';
display: inline;
margin-right: 3px;
animation: check-success .4s forwards;
}
}
.tip{
top: 10px;
margin-left: 5px;
font-size: 14px;
color: rgb(112, 111, 111);
white-space: nowrap;
}
@keyframes check-success{
0%{
opacity: 0;
}
100%{
opacity: 1;
}
}
2、创建图片选择器
加入图片选择器,并给它设置一个出场动画:
<script lang="ts" setup>
interface ResultImg{
isRight:boolean, //当前图片是否为需要用户点击的图片
imgUrl:string, //图片链接
key:number, //图片的唯一标识
content:string, //点击图片时显示的提示文案
display:string //控制图片点击文案的显示与隐藏
}
const showCode = ref<boolean>(false) //控制图片选择器的显示与隐藏
const resultImgList = ref<Array<ResultImg>>([])
const imgRefs = ref()
const clickImg = ()=> {
//...
}
const showImgs = ()=> {
//...
}
const checkCode = ()=> {
//...
}
</script>
//图片选择器和按钮处于同一级下
<transition name="code">
<div class="verify-show" v-if="showCode">
<div class="img_box"
:style="{'--display':item.display,'--clickImgColor':clickImgColor}"
v-for="(item) in resultImgList" :key="item.key"
:data-order="item.content"
@click="clickImg(item.key)"
>
<img :src="item.imgUrl" alt="" ref="imgRefs" >
</div>
<div class="btn_box">
<span class="refresh_btn" @click="showImgs">找不到?刷新</span>
<span style="float: right;">
<span class="btn" @click="closeCode">取消</span>
<span class="btn" style="margin-left: 5px;" @click="checkCode">确认</span>
</span>
</div>
</div>
</transition>
这里使用
img
显示图片而不是直接给div
设置背景图的原因后面会介绍。此外,点击图片显示的选中样式是通过img_box
这个div
的before
伪元素来渲染的,所以resultImgList
中的图片属性需要content
和display
来控制伪元素的显示隐藏与显示的内容
相应的less
代码;
.verify-show{
position: absolute;
z-index: 999;
width: 234px;
border: 1px solid #ccc;
background-color: #fff;
border-radius: 2px;
padding: 5px;
.img_box{
display: inline-block;
position: relative;
margin: 0 2px;
&:hover{
cursor: pointer;
}
&::before{
content: attr(data-order);
position: absolute;
display: var(--display);
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid var(--clickImgColor);
text-align: center;
line-height: 18px;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
color: var(--clickImgColor);
padding: 5px;
font-size: 16px;
font-weight: 600;
z-index: 999;
}
img{
width: 70px;
height: 70px;
position: relative;
}
}
.btn_box{
margin-top: 5px;
.refresh_btn{
font-size: 14px;
color: #999;
&:hover{
cursor: pointer;
color: var(--color);
}
}
.btn{
padding: 2px 3px;
font-size: 14px;
border-radius: 2px;
color: #fff;
&:first-child{
background-color: rgba(248, 10, 30, 0.7);
}
&:last-child{
background-color: rgba(16, 89, 245, 0.7);
}
&:hover{
cursor: pointer;
}
&:active{
box-shadow: inset 0 0 3px #fff;
}
}
}
}
.code-enter-active,
.code-leave-active{
transition: all .5s;
}
.code-enter-from,
.code-leave-to{
opacity: 0;
transform: scale(0);
transform-origin: left top;
}
3、为图片选择器创建loading
和error
按目前的思路,点击按钮调用按钮的showImgs
事件,在事件中将图片选择器显示出来并构建图片属性对象放入resultImgList
,就能正常渲染了。不过当网络较慢的时候,图片加载也会变慢,如下面的示例一样:
为了解决这个问题,我选择在图片的上面盖一层loading
,当所有图片加载完成后再去掉loading
,所以要用到img
的onload
方法,如果直接设置背景图片的话,我目前还不知道有什么方法可以监听背景图的加载,要是有大佬知道可以指点一下

既然有了loading
,那么图片要是加载失败就在上面盖一层error
提示图片加载失败,完成后的效果如下:
loading
的图片使用svg
制作,相关代码如下:
<script lang="ts" setup>
const showError = ref<boolean>(false)
const showLoading = computed(
//...
)
</script>
// 图片的div在同一层级
<div class="masking" v-if="showLoading">
<svg class="loading" width="35" height="35">
<line y2="13.42387" x2="17.5" y1="0.17387" x1="17.5" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" stroke="#999999" fill="none"/>
<line y2="34.96296" x2="17.54115" y1="22.23122" x1="17.45885" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" stroke="#999999" fill="none"/>
<line transform="rotate(45 25.185184478759773,10.008744239807127)" stroke="#999999" y2="16.63374" x2="25.18518" y1="3.38374" x1="25.18518" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="none"/>
<line transform="rotate(90 28.148147583007812,17.500000000000004)" stroke="#999999" y2="24.125" x2="28.14815" y1="10.875" x1="28.14815" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="none"/>
<line transform="rotate(135 25.34979248046875,25.564292907714844)" stroke="#999999" y2="32.18929" x2="25.34979" y1="18.93929" x1="25.34979" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="none"/>
<line transform="rotate(90 6.748980522155759,17.500000000000004)" stroke="#999999" y2="24.125" x2="6.74898" y1="10.875" x1="6.74898" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="none"/>
<line transform="rotate(-45 9.711940765380861,9.926440238952637)" y2="16.55144" x2="9.71194" y1="3.30144" x1="9.71194" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" stroke="#999999" fill="none"/>
<line transform="rotate(-135 9.87654972076416,25.399682998657227)" stroke="#999999" y2="32.02468" x2="9.87655" y1="18.77468" x1="9.87655" fill-opacity="null" stroke-opacity="null" stroke-width="1.5" fill="none"/>
</svg>
<div class="loading-text">loading...</div>
</div>
<div v-if="showError" class="loading-error">加载图片出错...</div>
showLoading
的实现后面再进行介绍
4、按钮点击和图片加载
按钮的点击方法还是很简单的,如下所示:
const openCode = ()=> {
if(showCheckSuccess.value){
return
}
if(isCodeOpen.value) {
return closeCode()
}
isCodeOpen.value = true
showImgs()
}
const closeCode = ()=> {
showCode.value = false
showTip.value = false
isCodeOpen.value = false
}
在实现showImgs
之前,先完成图片的初始化事件,
第一步先清空原来的最终需要渲染的图片数组resultImgList
,再从传入的rightImgs
随机挑选一组图片作为需要选择的图片:
const initCodeImgs = ()=> {
resultImgList.value.length = 0
isloadCount.value = 0 // 已经加载的图片数量,后面会用到
const randomImgs = rightImgs[Math.floor(Math.random() * rightImgs.length)]
codeType.value = randomImgs.type
//...
}
然后根据randomImgs
和wrongImgs
生成resultImgList
,为了后面的代码清晰,这里使用promise
进行控制:
let imgKey:any = 0 //每个图片的唯一标识,因为数组需要打乱,所以使用v-for时不能使用index
//跟在上文的省略号后
return new Promise((resolve,reject)=>{
if(randomImgs.imgs.length >= 9){
randomSort(randomImgs.imgs)
for (let i = 0; i < 9; i++) {
resultImgList.value.push({
isRight:true,
imgUrl:new URL(randomImgs.imgs[i],import.meta.url).href,
key:imgKey++,
content:'',
display:'none'
})
}
resolve([randomImgs.type,resultImgList.value.length])
}else if(randomImgs.imgs.length > 0 && randomImgs.imgs.length < 9){
const arr = []
for (const imgUrl of randomImgs.imgs) {
arr.push({
isRight:true,
imgUrl:new URL(imgUrl,import.meta.url).href,
key:imgKey++,
content:'',
display:'none'
})
}
randomSort(wrongImgs)
for (let i = 0; i < 9-randomImgs.imgs.length; i++) {
if(wrongImgs[i]){
arr.push({
isRight:false,
imgUrl:new URL(wrongImgs[i],import.meta.url).href,
key:imgKey++,
content:'',
display:'none'
})
}
}
randomSort(arr)
resultImgList.value = [...arr]
resolve([randomImgs.type,arr.length])
}else{
reject('error')
}
})
//打乱一个数组的辅助函数
function randomSort<T extends any>(arr:Array<T>){
let n = arr.length
while(n--){
const index = Math.floor(Math.random() * n);
[arr[n],arr[index]] = [arr[index],arr[n]]
}
}
因为最后渲染的图片最多为9张,所以优先考虑
rightImgs
中的图片,不够9张,就从wrongImgs
中随机选取补齐9张,如果最后加起来都不够9张,就全部都放到resultImgList
中。这里应该还有更简单的写法,不过我当时没去多想
之后就是showImgs
的实现,用三个常量表示当前验证码的三种状态,通过绑定在img
标签上的imgRef
,调用img
的onload
和onerror
事件,每次加载完成则记录一次已加载的图片数量,当有一张图片加载失败,就显示加载失败,代码如下:
const isloadCount = ref<number>(0) //表示已经加载的图片数量
const needLoadCount = ref<number>(0)//表示需要加载的图片数量
const showLoading = computed(() => !(isloadCount.value === needLoadCount.value && isloadCount.value != 0))
const showImgs = ()=> {
showCode.value = true
showError.value = false
checkCodeStatus.value = CHECK_ORIGINAL
initCodeImgs()
.then((array:any)=>{
const [type,imgCount] = array
codeType.value = type
showTip.value = true
needLoadCount.value = imgCount
nextTick(() => {
for (const img of imgRefs.value) {
(img as HTMLImageElement).onload = ()=> {
isloadCount.value++
}
(img as HTMLImageElement).onerror = ()=> {
needLoadCount.value = isloadCount.value = -1 //关闭loading
showError.value = true
showTip.value = false
}
}
})
})
.catch((err)=>{
console.log(err);
})
}
这里用计算属性控制
showLoading
,当需要加载的图片数量不等于已经加载的图片数量时,显示loading
5、点击图片和验证
这部分就很简单了,点击图片的时候只要设置图片对应的resultImgList
中对象的content
和display
就行,代码如下:
const clickImg = (key:number)=> {
const imgObj = resultImgList.value.find(item => item.key === key)
if(imgObj){
imgObj.content = imgObj.content?'':'√'
imgObj.display = imgObj.display=='block'?'none':'block'
}
}
验证函数代码如下:
const checkCode = ()=> {
const result = resultImgList.value.findIndex(item => (item.isRight && !item.content) || (!item.isRight && item.content))
if(result === -1){
checkCodeStatus.value = CHECK_SUCCESS
emits('ok')
closeCode()
}else{
checkCodeStatus.value = CHECK_FAIL
emits('fail')
let timer = setTimeout(() => {
showImgs()
clearTimeout(timer)
}, 600);
}
}
最后在模版中把验证失败部分加在与图片盒子同级下就行了:
//js
const showCheckFail = computed(() => checkCodeStatus.value === CHECK_FAIL)
//html
<transition name="check-defeat">
<div v-if="showCheckFail" class="masking-defeat">验证失败</div>
</transition>
//less
.masking-fail{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 99999;
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
display: flex;
justify-content: center;
align-items: center;
color: rgb(235, 54, 54);
font-size: 20px;
font-weight: 600;
letter-spacing: .2em;
}
.check-fail-enter-active,
.check-fail-leave-active{
transition: .5s;
}
.check-fail-enter-from,
.check-fail-leave-to{
opacity: 0;
}
三、总结
完成这两验证码到我写这篇文章的时候隔了也有两礼拜了,写的过程中又发现有些地方可以简化一下,不过就不花费时间去改了。如果有什么错误或者可以改进的地方,欢迎大佬们指点。
组件已经发布了,可通过
npm i zyhzyh-ui
下载,在main.js
中使用
import { VerifyCode} from 'zyhzyh-ui'
import 'zyhzyh-ui/es/style.css'
const app = createApp(App)
app.use(VerifyCode)
转载自:https://juejin.cn/post/7187267436214190117