hookehuyr

✨ feat(组件): 新增图片滑块验证组件

<template>
<!-- 滑块验证功能 -->
<div class="sliderModel" v-if="visible">
<div class="cont">
<div class="title">滑块验证</div>
<div class="slider-refresh">
<span @click="handleRefresh">刷新</span>
<span @click="handleClose">关闭</span>
</div>
<!-- canvas 图片 -->
<div class="imgWrap">
<canvas ref="sliderBlock" class="slider-block"></canvas>
<canvas ref="codeImg" class="code-img"></canvas>
</div>
<!-- 滑块 -->
<div class="sliderBox">
<div class="sliderF">
<div class="sliderS" @touchstart.prevent="handleTouch">
<div class="btn">&gt;&gt;</div>
</div>
</div>
<div class="bgC">
{{ tips }}
<div class="bgC_left"></div>
</div>
</div>
</div>
</div>
</template>
<script>
import img1 from './images/1.jpg'
import img2 from './images/2.jpg'
import img3 from './images/3.jpg'
import img4 from './images/4.jpg'
import img5 from './images/5.jpg'
import img6 from './images/6.jpg'
export default {
props: {
isShow: {
type: Boolean,
default: false
},
options: {
// 传入的参数不影响组件
type: Object,
default: () => ({})
},
imgList: { // 背景图片
type: Array,
default: () => {
return [img1, img2, img3, img4, img5, img6]
}
}
},
data() {
return {
// 滑块x轴数据
slider: {
mx: 0,
bx: 0
},
tips: '',
visible: false,
mainDom: '',
blockDom: ''
}
},
watch: {
isShow: {
handler(newVal) {
this.visible = newVal
if (newVal === true) {
this.tips = '拖动左边滑块完成上方拼图'
this.$nextTick(() => {
this.getDom()
this.canvasInit()
})
}
},
immediate: true
}
},
methods: {
// 获取 dom
getDom() {
this.mainDom = this.$refs.codeImg
this.blockDom = this.$refs.sliderBlock
},
handleClose() {
this.$emit('on-close', false)
},
// 刷新
handleRefresh() {
this.canvasInit()
},
// 移动端事件
handleTouch(e) {
const ev = e || window.event
const dom = ev.target // dom元素
const downCoordinate = {
x: ev.touches[0].pageX,
y: ev.touches[0].pageY
}
// 正确的滑块数据
const checkx = Number(this.slider.mx) - 0
// x轴数据
let x = 0
const move = (moveEV) => {
x = moveEV.touches[0].pageX - downCoordinate.x
// //y = moveEV.y - downCoordinate.y;
if (x >= 251 || x <= 0) return false
dom.style.left = x + 'px'
// dom.style.top = y + "px";
this.blockDom.style.left = x + 'px'
}
const up = () => {
document.removeEventListener('touchmove', move)
document.removeEventListener('touchend', up)
dom.style.left = ''
// console.log(x, checkx)
const max = checkx - 5
const min = checkx - 15
// 允许正负误差1
if ((max >= x && x >= min) || x === checkx) {
this.tips = '验证成功'
this.$emit('done', this.options.type)
} else {
this.tips = '验证失败,请重试'
this.blockDom.style.left = 0
this.canvasInit()
}
}
document.addEventListener('touchmove', move)
document.addEventListener('touchend', up)
},
// 拼图验证码初始化
canvasInit() {
// 生成指定区间的随机数
const random = (min, max) => {
return Math.floor(Math.random() * (max - min + 1) + min)
}
// x: 254, y: 109
const mx = random(127, 230)
const bx = random(10, 128)
const y = random(10, 99)
this.slider = { mx, bx }
this.draw(mx, bx, y)
},
draw(mx = 200, bx = 20, y = 50) {
const bg = this.mainDom.getContext('2d')
const block = this.blockDom.getContext('2d')
const width = this.mainDom.width
const height = this.mainDom.height
// 重新赋值,让canvas进行重新绘制
this.blockDom.height = height
this.mainDom.height = height
this.mainDom.width = width
// 随机背景图片
const randomImg = () => {
const num = Math.floor(Math.random() * this.imgList.length)
return this.imgList[num]
}
const imgsrc = randomImg()
const img = document.createElement('img')
img.style.objectFit = 'scale-down'
img.src = imgsrc
img.onload = () => {
bg.drawImage(img, 0, 0, width, height)
block.drawImage(img, 0, 0, width, height)
const ImageData = block.getImageData(mx, y, width, height)
block.putImageData(ImageData, 0, y)
}
const mainxy = { x: mx, y: y, r: 9 }
const blockxy = { x: mx, y: y, r: 9 }
this.drawBlock(bg, mainxy, 'fill')
this.drawBlock(block, blockxy, 'clip')
},
// 绘制拼图
drawBlock(ctx, xy = { x: 254, y: 109, r: 9 }, type) {
const x = xy.x
const y = xy.y
const r = xy.r
const w = 40
const PI = Math.PI
// 绘制
ctx.beginPath()
// left
// ctx.moveTo(x, y)
// top
ctx.arc(x + (w + 5) / 2, y, r, -PI, 0.15, true)
ctx.lineTo(x + w + 5, y)
// right
ctx.arc(x + w + 5, y + w / 2, r, 1.5 * PI, 0.5 * PI, false)
ctx.lineTo(x + w + 5, y + w)
// bottom
ctx.arc(x + (w + 5) / 2, y + w, r, 0, PI, false)
ctx.lineTo(x, y + w)
ctx.arc(x, y + w / 2, r, 0.5 * PI, 1.5 * PI, true)
ctx.lineTo(x, y)
// 修饰,没有会看不出效果
ctx.lineWidth = 1
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'
ctx.stroke()
ctx[type]()
ctx.globalCompositeOperation = 'xor'
}
}
}
</script>
<style scoped lang="less">
.sliderModel {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.title {
width: 100%;
height: 60px;
font-size: 18px;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.cont {
position: relative;
background: #fff;
width: 300px;
border-radius: 8px;
overflow: hidden;
padding-bottom: 10px;
}
.imgWrap {
position: relative;
width: 280px;
height: 150px;
margin: 0 auto;
overflow: hidden;
.code-img,
.slider-block {
border-radius: 8px;
height: inherit;
}
.code-img {
width: 280px;
margin: 0 auto;
}
.slider-block {
position: absolute;
z-index: 4000;
left: 0;
}
}
.slider-refresh {
font-size: 14px;
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
color: green;
span {
padding: 0 2px;
}
}
.img {
display: block;
width: 100%;
height: 100%;
}
.sliderOver {
position: absolute;
left: 0;
top: 0;
width: 50px;
height: 50px;
background: #ddd;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
}
.smartImg {
position: absolute;
z-index: 2;
left: 0;
top: 0;
width: 50px;
height: 50px;
overflow: hidden;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
.simg {
position: absolute;
display: block;
width: 280px;
height: 150px;
}
.sliderBox {
width: 280px;
margin: 15px auto 0;
height: 36px;
position: relative;
}
.sliderF {
width: 100%;
height: 100%;
z-index: 3;
}
.sliderS {
cursor: pointer;
position: absolute;
left: 0;
top: 0;
z-index: 2;
height: 36px;
width: 36px;
border-radius: 36px;
display: flex;
justify-content: center;
align-items: center;
}
.icon {
width: 20px;
height: 20px;
}
.bgC {
position: absolute;
z-index: 1;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 100%;
height: 30px;
border-radius: 30px;
line-height: 30px;
font-size: 14px;
color: #999999;
box-shadow: inset 0 0 4px #ccc;
text-align: center;
overflow: hidden;
}
.bgC_left {
position: absolute;
left: 0px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 28px;
border-top-left-radius: 28px;
border-bottom-left-radius: 28px;
line-height: 28px;
font-size: 14px;
background-color: #eee;
box-shadow: inset 0 0 4px #ccc;
text-align: center;
}
.showMessage {
text-align: center;
font-size: 14px;
height: 30px;
line-height: 30px;
}
#closeBtn {
position: fixed;
z-index: 10;
bottom: 10px;
left: 50%;
}
.btn {
width: 36px;
height: 36px;
position: absolute;
border: 1px solid #ccc;
cursor: move;
font-family: "宋体";
text-align: center;
line-height: 36px;
background-color: #fff;
user-select: none;
color: #666;
font-size: 16px;
}
</style>
// ts script setup 写法
<template>
<div class="img-verify">
<canvas ref="verify" :width="state.width" :height="state.height" @click="handleDraw" />
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted, ref } from 'vue'
const verify = ref({} as HTMLCanvasElement)
const state = reactive({
pool: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', // 字符串
width: 120,
height: 40,
imgCode: ''
})
onMounted(() => {
// 初始化绘制图片验证码
state.imgCode = draw()
})
// 点击图片重新绘制
const handleDraw = () => {
state.imgCode = draw()
}
// 随机数
const randomNum = (min: number, max: number) => {
return parseInt((Math.random() * (max - min) + min + '') as string)
}
// 随机颜色
const randomColor = (min: number, max: number) => {
const r = randomNum(min, max)
const g = randomNum(min, max)
const b = randomNum(min, max)
return `rgb(${r},${g},${b})`
}
// 绘制图片
const draw = () => {
// 3.填充背景颜色,背景颜色要浅一点
const ctx = verify.value.getContext('2d') as any
// 填充颜色
ctx.fillStyle = randomColor(180, 230)
// 填充的位置
ctx.fillRect(0, 0, state.width, state.height)
// 定义paramText
let imgCode = ''
// 4.随机产生字符串,并且随机旋转
for (let i = 0; i < 4; i++) {
// 随机的四个字
const text = state.pool[randomNum(0, state.pool.length)]
imgCode += text
// 随机的字体大小
const fontSize = randomNum(18, 40)
// 字体随机的旋转角度
const deg = randomNum(-30, 30)
/*
* 绘制文字并让四个文字在不同的位置显示的思路 :
* 1、定义字体
* 2、定义对齐方式
* 3、填充不同的颜色
* 4、保存当前的状态(以防止以上的状态受影响)
* 5、平移translate()
* 6、旋转 rotate()
* 7、填充文字
* 8、restore出栈
* */
ctx.font = fontSize + 'px Simhei'
ctx.textBaseline = 'top'
ctx.fillStyle = randomColor(80, 150)
/*
* save() 方法把当前状态的一份拷贝压入到一个保存图像状态的栈中。
* 这就允许您临时地改变图像状态,
* 然后,通过调用 restore() 来恢复以前的值。
* save是入栈,restore是出栈。
* 用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。 restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
*
* */
ctx.save()
ctx.translate(30 * i + 15, 15)
ctx.rotate((deg * Math.PI) / 180)
// fillText() 方法在画布上绘制填色的文本。文本的默认颜色是黑色。
// 请使用 font 属性来定义字体和字号,并使用 fillStyle 属性以另一种颜色/渐变来渲染文本。
// context.fillText(text,x,y,maxWidth);
ctx.fillText(text, -15 + 5, -15)
ctx.restore()
}
// 5.随机产生5条干扰线,干扰线的颜色要浅一点
for (let i = 0; i < 5; i++) {
ctx.beginPath()
ctx.moveTo(randomNum(0, state.width), randomNum(0, state.height))
ctx.lineTo(randomNum(0, state.width), randomNum(0, state.height))
ctx.strokeStyle = randomColor(180, 230)
ctx.closePath()
ctx.stroke()
}
// 6.随机产生40个干扰的小点
for (let i = 0; i < 40; i++) {
ctx.beginPath()
ctx.arc(randomNum(0, state.width), randomNum(0, state.height), 1, 0, 2 * Math.PI)
ctx.closePath()
ctx.fillStyle = randomColor(150, 200)
ctx.fill()
}
return imgCode
}
</script>
<style type="text/css">
.img-verify canvas {
cursor: pointer;
}
</style>
......@@ -39,6 +39,14 @@
</div>
<van-number-keyboard v-model="phone" :show="keyboard_show" :maxlength="11" @blur="onBlur" />
<!-- 图片滑块验证 -->
<image-slider-verify
:isShow="sliderShow"
@done="handleConfirm"
@on-close="handleClose"
>
</image-slider-verify>
</template>
<script setup>
......@@ -199,11 +207,14 @@ const themeVars = {
// FIXME: VUE2写法
import mixin from 'common/mixin';
import ImageSliderVerify from '@/components/ImageSliderVerify/index.vue'
export default {
components: { ImageSliderVerify },
mixins: [mixin.init],
data() {
return {
sliderShow: false
}
},
mounted() {
......@@ -223,6 +234,17 @@ export default {
icon: 'cross',
});
})
},
// 滑块验证成功后回调
handleConfirm (val) {
this.sliderShow = false
console.warn('验证成功');
},
handleClose () {
this.sliderShow = false
},
imageVerify () {
this.sliderShow = true;
}
},
}
......