Bootstrap无法直接实现手势密码,因其组件禁用触摸行为、栅格干扰坐标计算、缩放导致点位漂移;必须用canvas+原生触摸事件绘制连线,并用数组存取路径校验。

Bootstrap 本身不提供手势密码(九宫格连线解锁)功能,也没有内置的触摸轨迹识别能力;纯用 Bootstrap CSS + 原生 JS 才能实现可用的手势密码界面,且必须绕开 Bootstrap 的响应式栅格和组件限制来处理触摸事件。
为什么不能直接用 bootstrap 的 btn 或 grid 实现手势连线
手势密码的核心是捕获连续的触摸/鼠标移动路径,并动态绘制连接线、标记选中点。Bootstrap 的 .btn 默认禁用默认触摸行为(如 touchstart 被拦截),.row/.col 会干扰绝对定位与事件坐标计算,且所有栅格基于百分比和 padding,导致点位坐标难以精确映射到 3×3 网格上。
-
pointer-events: none可能被 Bootstrap 某些工具类意外覆盖,导致touchmove失效 - 使用
col-4布局的“点”在不同设备缩放下实际像素位置漂移,无法稳定判断连线顺序 - Bootstrap 5 默认启用
prefers-reduced-motion相关样式,可能抑制过渡动画,影响连线反馈
用 canvas + 原生 touchstart/touchmove/touchend 绘制连线
放弃用 HTML 元素模拟点阵,改用 <canvas></canvas> 绘制固定坐标点和实时连线——这是唯一能精准控制触摸响应、避免布局干扰的方式。
- 初始化 canvas 宽高设为父容器
clientWidth/clientHeight,并监听resize重绘点位 - 3×3 点阵坐标用等比计算:
const gap = Math.min(w, h) * 0.25; const ox = (w - gap * 2) / 2; const oy = (h - gap * 2) / 2;,确保横竖间距一致 - 在
touchstart中记录首个接触点索引,在touchmove中遍历所有点,用Math.hypot(dx, dy) 判断是否“吸附”到新点(radius 建议取 <code>gap * 0.3) - 避免重复添加同一索引:检查
currentPath[currentPath.length - 1] !== newIndex
ctx.beginPath();
ctx.moveTo(points[path[0]].x, points[path[0]].y);
path.forEach((i, idx) => {
if (idx) ctx.lineTo(points[i].x, points[i].y);
});
ctx.stroke();如何存储和校验手势密码(不依赖后端)
手势密码本质是点序列表,不是字符串。直接存 [0,1,4,5,8] 这样的数组比转成字符串更安全(避免 "01458" 和 "0,1,4,5,8" 解析歧义),校验时用 JSON.stringify(a) === JSON.stringify(b) 即可。
- 首次设置时,把
currentPath存入localStorage.setItem('gesture_pin', JSON.stringify(path)) - 解锁时对比
JSON.stringify(inputPath) === localStorage.getItem('gesture_pin') - 注意:空数组
[]和未设置状态要区分,建议初始存null,设置后才写入数组 - 不要用
Array.prototype.join(''),否则[10, 1]和[1, 0, 1]会冲突
真正麻烦的是 touch 事件在部分 Android WebView(尤其是微信内嵌浏览器)里会触发两次 touchstart,导致第一个点被注册两次;需要加 event.preventDefault() 在所有三个事件上,并用 event.touches[0].identifier 做去重,这个细节几乎没人提,但线上用户一定会遇到。


















