hookehuyr

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

1 +<template>
2 + <!-- 滑块验证功能 -->
3 + <div class="sliderModel" v-if="visible">
4 + <div class="cont">
5 + <div class="title">滑块验证</div>
6 + <div class="slider-refresh">
7 + <span @click="handleRefresh">刷新</span>
8 + <span @click="handleClose">关闭</span>
9 + </div>
10 + <!-- canvas 图片 -->
11 + <div class="imgWrap">
12 + <canvas ref="sliderBlock" class="slider-block"></canvas>
13 + <canvas ref="codeImg" class="code-img"></canvas>
14 + </div>
15 + <!-- 滑块 -->
16 + <div class="sliderBox">
17 + <div class="sliderF">
18 + <div class="sliderS" @touchstart.prevent="handleTouch">
19 + <div class="btn">&gt;&gt;</div>
20 + </div>
21 + </div>
22 + <div class="bgC">
23 + {{ tips }}
24 + <div class="bgC_left"></div>
25 + </div>
26 + </div>
27 + </div>
28 + </div>
29 +</template>
30 +
31 +<script>
32 +import img1 from './images/1.jpg'
33 +import img2 from './images/2.jpg'
34 +import img3 from './images/3.jpg'
35 +import img4 from './images/4.jpg'
36 +import img5 from './images/5.jpg'
37 +import img6 from './images/6.jpg'
38 +
39 +export default {
40 + props: {
41 + isShow: {
42 + type: Boolean,
43 + default: false
44 + },
45 + options: {
46 + // 传入的参数不影响组件
47 + type: Object,
48 + default: () => ({})
49 + },
50 + imgList: { // 背景图片
51 + type: Array,
52 + default: () => {
53 + return [img1, img2, img3, img4, img5, img6]
54 + }
55 + }
56 + },
57 + data() {
58 + return {
59 + // 滑块x轴数据
60 + slider: {
61 + mx: 0,
62 + bx: 0
63 + },
64 + tips: '',
65 + visible: false,
66 + mainDom: '',
67 + blockDom: ''
68 + }
69 + },
70 + watch: {
71 + isShow: {
72 + handler(newVal) {
73 + this.visible = newVal
74 + if (newVal === true) {
75 + this.tips = '拖动左边滑块完成上方拼图'
76 + this.$nextTick(() => {
77 + this.getDom()
78 + this.canvasInit()
79 + })
80 + }
81 + },
82 + immediate: true
83 + }
84 + },
85 +
86 + methods: {
87 + // 获取 dom
88 + getDom() {
89 + this.mainDom = this.$refs.codeImg
90 + this.blockDom = this.$refs.sliderBlock
91 + },
92 +
93 + handleClose() {
94 + this.$emit('on-close', false)
95 + },
96 +
97 + // 刷新
98 + handleRefresh() {
99 + this.canvasInit()
100 + },
101 +
102 + // 移动端事件
103 + handleTouch(e) {
104 + const ev = e || window.event
105 + const dom = ev.target // dom元素
106 +
107 + const downCoordinate = {
108 + x: ev.touches[0].pageX,
109 + y: ev.touches[0].pageY
110 + }
111 + // 正确的滑块数据
112 + const checkx = Number(this.slider.mx) - 0
113 + // x轴数据
114 + let x = 0
115 + const move = (moveEV) => {
116 + x = moveEV.touches[0].pageX - downCoordinate.x
117 + // //y = moveEV.y - downCoordinate.y;
118 + if (x >= 251 || x <= 0) return false
119 + dom.style.left = x + 'px'
120 + // dom.style.top = y + "px";
121 + this.blockDom.style.left = x + 'px'
122 + }
123 +
124 + const up = () => {
125 + document.removeEventListener('touchmove', move)
126 + document.removeEventListener('touchend', up)
127 + dom.style.left = ''
128 +
129 + // console.log(x, checkx)
130 + const max = checkx - 5
131 + const min = checkx - 15
132 + // 允许正负误差1
133 + if ((max >= x && x >= min) || x === checkx) {
134 + this.tips = '验证成功'
135 + this.$emit('done', this.options.type)
136 + } else {
137 + this.tips = '验证失败,请重试'
138 + this.blockDom.style.left = 0
139 + this.canvasInit()
140 + }
141 + }
142 +
143 + document.addEventListener('touchmove', move)
144 + document.addEventListener('touchend', up)
145 + },
146 +
147 + // 拼图验证码初始化
148 + canvasInit() {
149 + // 生成指定区间的随机数
150 + const random = (min, max) => {
151 + return Math.floor(Math.random() * (max - min + 1) + min)
152 + }
153 + // x: 254, y: 109
154 + const mx = random(127, 230)
155 + const bx = random(10, 128)
156 + const y = random(10, 99)
157 +
158 + this.slider = { mx, bx }
159 +
160 + this.draw(mx, bx, y)
161 + },
162 +
163 + draw(mx = 200, bx = 20, y = 50) {
164 + const bg = this.mainDom.getContext('2d')
165 + const block = this.blockDom.getContext('2d')
166 +
167 + const width = this.mainDom.width
168 + const height = this.mainDom.height
169 +
170 + // 重新赋值,让canvas进行重新绘制
171 + this.blockDom.height = height
172 + this.mainDom.height = height
173 + this.mainDom.width = width
174 + // 随机背景图片
175 + const randomImg = () => {
176 + const num = Math.floor(Math.random() * this.imgList.length)
177 + return this.imgList[num]
178 + }
179 +
180 + const imgsrc = randomImg()
181 + const img = document.createElement('img')
182 + img.style.objectFit = 'scale-down'
183 + img.src = imgsrc
184 + img.onload = () => {
185 + bg.drawImage(img, 0, 0, width, height)
186 + block.drawImage(img, 0, 0, width, height)
187 + const ImageData = block.getImageData(mx, y, width, height)
188 + block.putImageData(ImageData, 0, y)
189 + }
190 +
191 + const mainxy = { x: mx, y: y, r: 9 }
192 + const blockxy = { x: mx, y: y, r: 9 }
193 + this.drawBlock(bg, mainxy, 'fill')
194 + this.drawBlock(block, blockxy, 'clip')
195 + },
196 +
197 + // 绘制拼图
198 + drawBlock(ctx, xy = { x: 254, y: 109, r: 9 }, type) {
199 + const x = xy.x
200 + const y = xy.y
201 + const r = xy.r
202 + const w = 40
203 + const PI = Math.PI
204 + // 绘制
205 + ctx.beginPath()
206 + // left
207 + // ctx.moveTo(x, y)
208 + // top
209 + ctx.arc(x + (w + 5) / 2, y, r, -PI, 0.15, true)
210 + ctx.lineTo(x + w + 5, y)
211 + // right
212 + ctx.arc(x + w + 5, y + w / 2, r, 1.5 * PI, 0.5 * PI, false)
213 + ctx.lineTo(x + w + 5, y + w)
214 + // bottom
215 + ctx.arc(x + (w + 5) / 2, y + w, r, 0, PI, false)
216 + ctx.lineTo(x, y + w)
217 + ctx.arc(x, y + w / 2, r, 0.5 * PI, 1.5 * PI, true)
218 + ctx.lineTo(x, y)
219 + // 修饰,没有会看不出效果
220 + ctx.lineWidth = 1
221 + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'
222 + ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'
223 + ctx.stroke()
224 + ctx[type]()
225 + ctx.globalCompositeOperation = 'xor'
226 + }
227 + }
228 +}
229 +</script>
230 +
231 +<style scoped lang="less">
232 +.sliderModel {
233 + position: fixed;
234 + left: 0;
235 + top: 0;
236 + width: 100%;
237 + height: 100%;
238 + background: rgba(0, 0, 0, 0.5);
239 + display: flex;
240 + justify-content: center;
241 + align-items: center;
242 +}
243 +
244 +.title {
245 + width: 100%;
246 + height: 60px;
247 + font-size: 18px;
248 + color: #333;
249 + display: flex;
250 + align-items: center;
251 + justify-content: center;
252 +}
253 +
254 +.cont {
255 + position: relative;
256 + background: #fff;
257 + width: 300px;
258 + border-radius: 8px;
259 + overflow: hidden;
260 + padding-bottom: 10px;
261 +}
262 +
263 +.imgWrap {
264 + position: relative;
265 + width: 280px;
266 + height: 150px;
267 + margin: 0 auto;
268 + overflow: hidden;
269 +
270 + .code-img,
271 + .slider-block {
272 + border-radius: 8px;
273 + height: inherit;
274 + }
275 +
276 + .code-img {
277 + width: 280px;
278 + margin: 0 auto;
279 + }
280 +
281 + .slider-block {
282 + position: absolute;
283 + z-index: 4000;
284 + left: 0;
285 + }
286 +}
287 +
288 +.slider-refresh {
289 + font-size: 14px;
290 + position: absolute;
291 + top: 20px;
292 + right: 20px;
293 + cursor: pointer;
294 + color: green;
295 +
296 + span {
297 + padding: 0 2px;
298 + }
299 +}
300 +
301 +.img {
302 + display: block;
303 + width: 100%;
304 + height: 100%;
305 +}
306 +
307 +.sliderOver {
308 + position: absolute;
309 + left: 0;
310 + top: 0;
311 + width: 50px;
312 + height: 50px;
313 + background: #ddd;
314 + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.3);
315 +}
316 +
317 +.smartImg {
318 + position: absolute;
319 + z-index: 2;
320 + left: 0;
321 + top: 0;
322 + width: 50px;
323 + height: 50px;
324 + overflow: hidden;
325 + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
326 +}
327 +
328 +.simg {
329 + position: absolute;
330 + display: block;
331 + width: 280px;
332 + height: 150px;
333 +}
334 +
335 +.sliderBox {
336 + width: 280px;
337 + margin: 15px auto 0;
338 + height: 36px;
339 + position: relative;
340 +}
341 +
342 +.sliderF {
343 + width: 100%;
344 + height: 100%;
345 + z-index: 3;
346 +}
347 +
348 +.sliderS {
349 + cursor: pointer;
350 + position: absolute;
351 + left: 0;
352 + top: 0;
353 + z-index: 2;
354 + height: 36px;
355 + width: 36px;
356 + border-radius: 36px;
357 + display: flex;
358 + justify-content: center;
359 + align-items: center;
360 +}
361 +
362 +.icon {
363 + width: 20px;
364 + height: 20px;
365 +}
366 +
367 +.bgC {
368 + position: absolute;
369 + z-index: 1;
370 + left: 0;
371 + top: 50%;
372 + transform: translateY(-50%);
373 + width: 100%;
374 + height: 30px;
375 + border-radius: 30px;
376 + line-height: 30px;
377 + font-size: 14px;
378 + color: #999999;
379 + box-shadow: inset 0 0 4px #ccc;
380 + text-align: center;
381 + overflow: hidden;
382 +}
383 +
384 +.bgC_left {
385 + position: absolute;
386 + left: 0px;
387 + top: 50%;
388 + transform: translateY(-50%);
389 + width: 0;
390 + height: 28px;
391 + border-top-left-radius: 28px;
392 + border-bottom-left-radius: 28px;
393 + line-height: 28px;
394 + font-size: 14px;
395 + background-color: #eee;
396 + box-shadow: inset 0 0 4px #ccc;
397 + text-align: center;
398 +}
399 +
400 +.showMessage {
401 + text-align: center;
402 + font-size: 14px;
403 + height: 30px;
404 + line-height: 30px;
405 +}
406 +
407 +#closeBtn {
408 + position: fixed;
409 + z-index: 10;
410 + bottom: 10px;
411 + left: 50%;
412 +}
413 +
414 +.btn {
415 + width: 36px;
416 + height: 36px;
417 + position: absolute;
418 + border: 1px solid #ccc;
419 + cursor: move;
420 + font-family: "宋体";
421 + text-align: center;
422 + line-height: 36px;
423 + background-color: #fff;
424 + user-select: none;
425 + color: #666;
426 + font-size: 16px;
427 +}
428 +</style>
1 +// ts script setup 写法
2 +<template>
3 + <div class="img-verify">
4 + <canvas ref="verify" :width="state.width" :height="state.height" @click="handleDraw" />
5 + </div>
6 +</template>
7 +
8 +<script setup lang="ts">
9 +import { reactive, onMounted, ref } from 'vue'
10 +
11 +const verify = ref({} as HTMLCanvasElement)
12 +const state = reactive({
13 + pool: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', // 字符串
14 + width: 120,
15 + height: 40,
16 + imgCode: ''
17 +})
18 +onMounted(() => {
19 + // 初始化绘制图片验证码
20 + state.imgCode = draw()
21 +})
22 +
23 +// 点击图片重新绘制
24 +const handleDraw = () => {
25 + state.imgCode = draw()
26 +}
27 +
28 +// 随机数
29 +const randomNum = (min: number, max: number) => {
30 + return parseInt((Math.random() * (max - min) + min + '') as string)
31 +}
32 +// 随机颜色
33 +const randomColor = (min: number, max: number) => {
34 + const r = randomNum(min, max)
35 + const g = randomNum(min, max)
36 + const b = randomNum(min, max)
37 + return `rgb(${r},${g},${b})`
38 +}
39 +
40 +// 绘制图片
41 +const draw = () => {
42 + // 3.填充背景颜色,背景颜色要浅一点
43 + const ctx = verify.value.getContext('2d') as any
44 + // 填充颜色
45 + ctx.fillStyle = randomColor(180, 230)
46 + // 填充的位置
47 + ctx.fillRect(0, 0, state.width, state.height)
48 + // 定义paramText
49 + let imgCode = ''
50 + // 4.随机产生字符串,并且随机旋转
51 + for (let i = 0; i < 4; i++) {
52 + // 随机的四个字
53 + const text = state.pool[randomNum(0, state.pool.length)]
54 + imgCode += text
55 + // 随机的字体大小
56 + const fontSize = randomNum(18, 40)
57 + // 字体随机的旋转角度
58 + const deg = randomNum(-30, 30)
59 + /*
60 + * 绘制文字并让四个文字在不同的位置显示的思路 :
61 + * 1、定义字体
62 + * 2、定义对齐方式
63 + * 3、填充不同的颜色
64 + * 4、保存当前的状态(以防止以上的状态受影响)
65 + * 5、平移translate()
66 + * 6、旋转 rotate()
67 + * 7、填充文字
68 + * 8、restore出栈
69 + * */
70 + ctx.font = fontSize + 'px Simhei'
71 + ctx.textBaseline = 'top'
72 + ctx.fillStyle = randomColor(80, 150)
73 + /*
74 + * save() 方法把当前状态的一份拷贝压入到一个保存图像状态的栈中。
75 + * 这就允许您临时地改变图像状态,
76 + * 然后,通过调用 restore() 来恢复以前的值。
77 + * save是入栈,restore是出栈。
78 + * 用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。 restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
79 + *
80 + * */
81 + ctx.save()
82 + ctx.translate(30 * i + 15, 15)
83 + ctx.rotate((deg * Math.PI) / 180)
84 + // fillText() 方法在画布上绘制填色的文本。文本的默认颜色是黑色。
85 + // 请使用 font 属性来定义字体和字号,并使用 fillStyle 属性以另一种颜色/渐变来渲染文本。
86 + // context.fillText(text,x,y,maxWidth);
87 + ctx.fillText(text, -15 + 5, -15)
88 + ctx.restore()
89 + }
90 + // 5.随机产生5条干扰线,干扰线的颜色要浅一点
91 + for (let i = 0; i < 5; i++) {
92 + ctx.beginPath()
93 + ctx.moveTo(randomNum(0, state.width), randomNum(0, state.height))
94 + ctx.lineTo(randomNum(0, state.width), randomNum(0, state.height))
95 + ctx.strokeStyle = randomColor(180, 230)
96 + ctx.closePath()
97 + ctx.stroke()
98 + }
99 + // 6.随机产生40个干扰的小点
100 + for (let i = 0; i < 40; i++) {
101 + ctx.beginPath()
102 + ctx.arc(randomNum(0, state.width), randomNum(0, state.height), 1, 0, 2 * Math.PI)
103 + ctx.closePath()
104 + ctx.fillStyle = randomColor(150, 200)
105 + ctx.fill()
106 + }
107 + return imgCode
108 +}
109 +
110 +</script>
111 +<style type="text/css">
112 +.img-verify canvas {
113 + cursor: pointer;
114 +}
115 +</style>
...@@ -39,6 +39,14 @@ ...@@ -39,6 +39,14 @@
39 </div> 39 </div>
40 40
41 <van-number-keyboard v-model="phone" :show="keyboard_show" :maxlength="11" @blur="onBlur" /> 41 <van-number-keyboard v-model="phone" :show="keyboard_show" :maxlength="11" @blur="onBlur" />
42 +
43 + <!-- 图片滑块验证 -->
44 + <image-slider-verify
45 + :isShow="sliderShow"
46 + @done="handleConfirm"
47 + @on-close="handleClose"
48 + >
49 + </image-slider-verify>
42 </template> 50 </template>
43 51
44 <script setup> 52 <script setup>
...@@ -199,11 +207,14 @@ const themeVars = { ...@@ -199,11 +207,14 @@ const themeVars = {
199 // FIXME: VUE2写法 207 // FIXME: VUE2写法
200 import mixin from 'common/mixin'; 208 import mixin from 'common/mixin';
201 209
210 +import ImageSliderVerify from '@/components/ImageSliderVerify/index.vue'
211 +
202 export default { 212 export default {
213 + components: { ImageSliderVerify },
203 mixins: [mixin.init], 214 mixins: [mixin.init],
204 data() { 215 data() {
205 return { 216 return {
206 - 217 + sliderShow: false
207 } 218 }
208 }, 219 },
209 mounted() { 220 mounted() {
...@@ -223,6 +234,17 @@ export default { ...@@ -223,6 +234,17 @@ export default {
223 icon: 'cross', 234 icon: 'cross',
224 }); 235 });
225 }) 236 })
237 + },
238 + // 滑块验证成功后回调
239 + handleConfirm (val) {
240 + this.sliderShow = false
241 + console.warn('验证成功');
242 + },
243 + handleClose () {
244 + this.sliderShow = false
245 + },
246 + imageVerify () {
247 + this.sliderShow = true;
226 } 248 }
227 }, 249 },
228 } 250 }
......