SL-WX-Captcha 行为验证组件(三合一)
插件市场
操作视频
组件简介
单一组件统一支持三种主流前端行为验证方式,通过mode属性零切换:
slide滑动验证:经典滑块,按住并拖动到底部最右侧即视为通过,失败自动回弹。puzzle拼图验证:背景图上随机生成一个缺口位置,拖动下方滑块让图片拼图块精准对齐缺口,容差可配置。click顺序点字验证:背景图上随机散布多个汉字(目标字 + 干扰字),用户按提示依次点击所有目标字(2~5 个可配)。
组件内部已兼容H5(鼠标 mousedown/move/up)与微信小程序(touchstart/move/end)两套事件链路,失败/成功回调、刷新、关闭、容差 / 目标字数等全部参数化可控制。
注:当前背景图使用
picsum.photos随机种子作为纯前端 mock 演示;生产环境中建议由后端生成带签名 / 随机水印的验证图与答案,并在success回调里携带后端校验。
目录结构
components/SL-WX-Captcha └── SL-WX-Captcha.vue三种模式对比
| 模式值 | 名称 | 成功判定 | 失败后处理 |
|---|---|---|---|
slide | 滑动验证 | 滑块进度 ≥ 98%(即已滑到最右,自动吸附 100%) | 400ms easeOutQuad 回弹到 0 |
puzzle | 拼图验证 | ` | 拼图块中心 − 缺口中心 |
click | 顺序点字 | 依次点中所有目标字,任意一步点错立即失败(也支持「确认」按钮手动提交) | 700~900ms 自动清空重试 |
实现要点 & 修复日志
滑动 / 拼图滑块显示与拖拽可用修复
| 问题 | 修复方案 |
|---|---|
滑块初始不可见(slideX=0时 thumb 完全在轨道外) | 原写法left: X%; transform: translate(-100%, 0)叠加时,X=0 → translate(-100%)会把 thumb 向左平移一个 thumb 自身宽度挪到轨道外。改为纯像素left: Npx+top:50%; translateY(-50%)垂直居中,slideThumbPx / puzzleThumbPx直接从 0 线性增长到轨道宽 - thumb 宽,保证两端视觉与比例完全对齐。 |
| 拼图块无内容、跟缺口对不上 | 原写法里.sl-puzzle-piece__img用width:100% mode=aspectFill,展示的是拼图块左上角对应底图的 (0,0) 位置——根本不对应缺口的像素。改为:piece 里的<image>宽高强制等于舞台宽高,再transform: translate(-pieceLeft, -pieceTop)反向偏移,这样 piece 滑到哪就显示底图对应哪一像素,对齐缺口时内容完全吻合。 |
| 缺口形状不明显、不显示 | 原缺口只是一个半透明方块,不够像拼图。改为clip-path: polygon()切出「正方形主体 + 右侧半圆凸块」的经典拼图形状,gap(缺口蒙版)和 piece(拼图块)共用同一份 clip-path,形状完全吻合。 |
| 首次渲染拖不动 | 原_sliderWidth依赖 SelectorQuery 异步回调结果,mounted首次 touchmove 时回调可能还没返回 → 0 导致 dx 被除。在onSlideStart / onPuzzleStart里先调用_ensureSizesFallback()按widthprop 先兜底换算一套可用的轨道/thumb 尺寸,保证拖拽启动瞬间就有正确行程。 |
| H5 拖出滑块外"卡住手势" | 在组件mounted时document.addEventListener('mousemove' / 'mouseup'),beforeDestroy统一解绑,避免 mousedown 后拖到滑块外松开导致的"再按下去无响应"。 |
| click 模式点字事件平台差异 | 小程序端用@touchend+changedTouches[0];H5 端单独绑定@tap.stop.prevent取e.clientX/Y,避免某一端取不到坐标。 |
失败回弹曲线
slide / puzzle 失败后不再用setTimeout + 一次性跳回,改用requestAnimationFrame + easeOutQuad逐帧回弹到 0,视觉更自然。progress(fail 回调)为换算后的百分比,便于日志。
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mode | String | 'slide' | 验证模式:slide/puzzle/click |
width | Number|String | 620 | 组件宽度;Number 视为 rpx,String 直接作为 CSS 值 |
puzzleHeight | Number | 320 | 拼图 / 点字模式图片舞台高度(rpx) |
trackHeight | Number | 72 | 滑块条高度(rpx) |
radius | Number | 16 | 拼图 / 点字模式圆角(rpx) |
pieceSize | Number | 100 | 拼图块边长(rpx),建议为舞台宽度的 1/5~1/6 |
puzzleTolerance | Number | 4 | 拼图容差(像素 px):拼图块中心与缺口中心的像素差。生产推荐 2 |
slideLabel | String | '请按住滑块,拖动到最右边' | 模式 1 的提示文案 |
targetCount | Number | 3 | 模式 3 的目标文字数量,必须为 2~5。总随机字数 =targetCount + distractCount |
distractCount | Number | 4 | 模式 3 的干扰字数量 |
showHeader | Boolean | true | 是否显示顶部标题与刷新栏 |
showClose | Boolean | false | 是否在顶部栏显示「✕」关闭按钮 |
title | String | '' | 自定义顶部标题,留空则根据模式自动取「滑动验证 / 补全图片验证 / 文字顺序验证」 |
Events
| 事件名 | 回调参数 | 说明 |
|---|---|---|
success | { mode, type, ...payload } | 验证通过触发,type为对应模式名:slide/puzzle/click |
fail | { mode, type, ...payload } | 验证失败触发 |
refresh | { mode } | 点击顶部刷新图标(⟳)触发 |
close | - | 点击顶部关闭按钮(✕)触发,需要showClose = true |
success / fail 的 payload 详情
// mode = 'slide'success:{mode:'slide',type:'slide'}fail:{mode:'slide',type:'slide',progress:Number}// progress 失败时的进度 0~100// mode = 'puzzle'success:{mode:'puzzle',type:'puzzle',diff:1.3,tolerance:4}// diff=像素偏差,tolerance=props设定fail:{mode:'puzzle',type:'puzzle',diff:18.6,tolerance:4}// mode = 'click'success:{mode:'click',type:'click',seq:[0,1,2]}// seq=用户依次点击的目标索引fail:{mode:'click',type:'click',reason:'wrong-order-or-miss'|'incomplete'}对外方法
通过$refs.captcha.xxx()调用:
| 方法名 | 说明 |
|---|---|
onRefresh() | 与点击顶部刷新按钮等效:重置所有状态、重新随机背景图、缺口位置、点字分布。模式 2 / 3 切换参数(如 targetCount)后建议手动调用一次。 |
onClose() | 触发close事件。 |
noop() | 空函数,用作事件占位。 |
使用示例
示例 1:基础滑块验证(登录/注册前)
<template> <view> <sl-wx-captcha ref="cap" mode="slide" :width="660" slide-label="请按住滑块,向右拖动完成验证" @success="onCapOk" @fail="onCapFail" /> </view> </template> <script> import SlWxCaptcha from '@/components/SL-WX-Captcha/SL-WX-Captcha.vue' export default { components: { SlWxCaptcha }, methods: { onCapOk() { uni.showToast({ title: '验证通过,正在登录', icon: 'none' }) this.submitLogin() // 验证成功后再调用登录接口 }, onCapFail(e) { console.log('滑块未到底:', e.progress + '%') } } } </script>示例 2:拼图验证(容差 6px,较宽松)
<template> <sl-wx-captcha ref="cap" mode="puzzle" :puzzle-tolerance="6" :width="620" :radius="20" @success="onSuccess" @fail="onFail" @refresh="onRefresh" /> </template>示例 3:顺序点字(4 字验证,作为重要操作二次确认)
<template> <sl-wx-captcha ref="cap" mode="click" :target-count="4" :distract-count="5" :show-close="true" title="请先完成二次验证" @success="onPay" @close="onCancel" /> </template> <script> export default { methods: { onPay(e) { // e.seq 可上报后端进行二次审计 console.log('验证通过,点字顺序:', e.seq) this.confirmPay() }, onCancel() { uni.navigateBack() } } } </script>示例 4:弹层(uni-popup / 自定义遮罩)内使用 + 关闭按钮 + 刷新
<template> <view class="mask" @tap="close"></view> <view class="pop" @tap.stop=""> <sl-wx-captcha ref="cap" :mode="mode" :show-close="true" @success="onOk" @close="close" @refresh="(e)=>console.log('刷新了', e.mode)" /> </view> </template>实现要点(可二次开发)
事件链路:
滑块 / 拼图使用
@touchstart/move/end/capture.stop.prevent保证在小程序端优先锁定手势,避免父容器 scroll-view 把横拖误识别为纵向滚动。H5 端在组件
mounted时document级绑定mousemove / mouseup,beforeDestroy自动解绑,防止鼠标抬出滑块外导致"卡着手势"。
测量:
prepareStage()中用uni.createSelectorQuery().in(this)取舞台和滑块条的真实像素宽度,兼容不同宽度配置下拼图比例/滑块行程正确。拼图实现:
用「相同背景图 × 2」一张铺底,另一张放入
.sl-puzzle-piece(overflow:hidden+ 固定宽高 = 拼图块),通过 piece 的top/left与滑块位置联动实现"切片效果",无需后端合成拼图。缺口用半透明白色蒙版 + 内描边,提示用户拼图目标位置。
点字命中判定:以用户点击像素为圆心,半径 26px 范围内取最近字作为命中候选,字的样式随机倾斜/颜色/大小/字体,贴近真实点字验证码观感。
无障碍与反馈:每次失败或成功都会在舞台顶部弹出半透明遮罩(绿色通过/红色失败),并且统一触发
success/fail事件便于接入业务。
注意事项
- 拼图背景图域名白名单:如果使用小程序端,请在微信公众平台配置
picsum.photos为合法 downloadFile 域名;生产时换成自己的 OSS/CDN 即可。 - 图片加载失败兜底:
onPuzzleImgError已经监听了底图失败(会走白底 + 缺口形状),交互依然可用,建议后端额外监控图片 404。 - 拼图容差与用户体验:
puzzleTolerance建议 2~4(难)~ 6~8(宽松),超过 10px 基本等同于无校验。 - 不要把安全校验放前端:本组件实现的是前端交互和基础判定,在前端判定成功后建议把
diff/seq/progress等摘要一并上报后端,使用后端接口做最终决策(或使用加密 token),避免被自动化脚本绕过。 - mode 切换时自动重绘:组件已 watch
mode,切换会立即resetAll + prepareStage,无需手动刷新;但如果是在同一 mode 下改了targetCount / puzzleTolerance等参数,建议调用this.$refs.cap.onRefresh()手动刷新一次题目。