vue项目中实现手势密码
tips:本文是做记录用的
思路:
本来应该全部都用canvas来实现的,但时间紧迫 写的时候只想着圆圈用li写,线用canvas,写到一半才想通,不过还好这一通下来还算比较顺利
第一步:页面中的9个点用v-for循环出来li,ul设置成宽高相等的正方形。给li设置margin,保证一行只能装得下三个li,然后ul用display:flex;justify-content: space-between; align-content: space-between;给子元素排成九宫格;
第二步:先获取九个点圆心的位置,在手指按下移动的方法中判断当前手指的坐标是否到li区域内,然后把对应位置的数字存进密码数组,调用画线的方法,把密码数组中的数字对应的点连起来,最后连到手指的位置
第三步:在手指松开的方法中进行各种判断,判断是创建密码还是登录,判断密码长度是否小于4,有错的话颜色变红,最后再判断密码是否正确
说明:
1、手势密码部分以组件的形式引入需要用到的页面中
2、父组件中不传值的话就默认是创建手势密码(创建时密码长度不能小于4),需要输入两次,两次密码必须一致,如果是登录,父组件就把密码传给子组件,子组件就会根据密码判断当前输入是否正确,执行时请看控制台
不足之处:
1、未处理创建或登录成功之后的事情(写一个emit就行了,把输入的密码传给父组件)
2、还有一个就是canvas的高度问题,这里是用的设备高度的86%,如果九个点下面紧挨着有其他按钮的话是点不了的,因为被canvas覆盖了
演示:
父组件未传值,此时是创建手势密码
登录时,父组件穿的值为<gestureUnlock :fatherPassword="[1,2,3,4,5]"></gestureUnlock>
下面分别是密码正确和密码错误的情况
密码组件:
1 <template> 2 <div class="gestureUnlock"> 3 <div class="gesture"> 4 <ul> 5 <li ref="selectLi" v-for="(item, index) in list" :key="item.id" 6 :class="{'selectedOuter': password.indexOf(index) !== -1 ? true : false, 7 'selectedOuter2': password.indexOf(index) !== -1 && redStyle ? true : false}"> 8 <span :class="{'selectedInside': password.indexOf(index) !== -1 ? true : false, 9 'selectedInside2': password.indexOf(index) !== -1 && redStyle ? true : false}"> 10 <!--圆心--> 11 <i ref="selectLiO"></i> 12 </span> 13 </li> 14 </ul> 15 </div> 16 <div class="canvasDiv"> 17 <!-- <canvas id="canvasClearTop">此浏览器不支持canvas</canvas> --> 18 <canvas id="canvas" @touchstart="start" @touchmove="move" @touchend="end">此浏览器不支持canvas</canvas> 19 </div> 20 <div class='incorrectTip'><span v-show="tips">incorrect pattern</span></div> 21 </div> 22 </template> 23 24 <script> 25 export default { 26 name: "GestureUnlock", 27 data () { 28 return { 29 list: [ 30 {id:0, top: 0, left: 0, isSelected: false}, 31 {id:1, top: 0, left: 0, isSelected: false}, 32 {id:2, top: 0, left: 0, isSelected: false}, 33 {id:3, top: 0, left: 0, isSelected: false}, 34 {id:4, top: 0, left: 0, isSelected: false}, 35 {id:5, top: 0, left: 0, isSelected: false}, 36 {id:6, top: 0, left: 0, isSelected: false}, 37 {id:7, top: 0, left: 0, isSelected: false}, 38 {id:8, top: 0, left: 0, isSelected: false}, 39 ], 40 left: [], // 圆心x坐标 41 top: [], // 圆心y坐标 42 password: [], // 用来存储创建密码,从上到下,从左到右依次是123,456,789 43 cas: '', // 画笔 44 casClearTop:'', // 上部清除线条的画布对象 45 clientWidth: 0, 46 clientHeight: 0, 47 isCorrect: true, // 密码是否且是否正确 48 redStyle: false, // li样式是否为红色 49 createPassword: Array, // 这个用来存一下父组件传过来的fatherPassword,因为子组件不能直接修改父组件传过来的值 50 radius: Number, // 半径 51 tips: false // 错误提示是否显示 52 } 53 }, 54 props: { 55 // 存储确认密码,变成组件后由父组件传过来,默认是空数组 56 fatherPassword: { 57 default: ()=>[], // 这个地方不能写成default: [] 58 type: Array 59 } 60 }, 61 created () { 62 // 存一下父组件传过来的fatherPassword,因为子组件不能直接修改父组件传过来的值 63 this.createPassword = this.fatherPassword 64 }, 65 mounted() { 66 // 获取到的是每个方块中心i标签的位置, 67 for (let i = 0; i < this.$refs.selectLiO.length; i++) { 68 this.left.push(this.$refs.selectLiO[i].getBoundingClientRect().left) 69 this.top.push(this.$refs.selectLiO[i].getBoundingClientRect().top) 70 } 71 this.radius = this.$refs.selectLiO[0].getBoundingClientRect().left - this.$refs.selectLi[0].getBoundingClientRect().left 72 console.log('半径为:', this.radius) 73 console.log(this.left) 74 console.log(this.top) 75 this.clientWidth = document.documentElement.clientWidth 76 this.clientHeight = document.documentElement.clientHeight 77 console.log('设备宽高:', this.clientWidth, this.clientHeight) 78 this.cas = document.getElementById('canvas').getContext('2d'); 79 document.getElementById('canvas').width = this.clientWidth; 80 // canvas高度为最后一个圆的圆心加半径乘以1.5,就是大于最后一行多一点 81 document.getElementById('canvas').height = this.top[this.top.length-1] + this.radius*1.5; 82 // this.casClearTop = document.getElementById('canvasClearTop').getContext('2d'); 83 // document.getElementById('canvasClearTop').width = this.clientWidth; 84 // document.getElementById('canvasClearTop').height = this.top[0] - this.radius*1.5; 85 }, 86 methods: { 87 // 手指点下 88 start (e) { 89 if(e.touches.length > 1 || e.scale && e.scale !== 1) { // 多点触碰或者缩放 90 console.log('这样不行', e) 91 } else { 92 console.log('start', e.touches[0].pageX , e.touches[0].pageY) 93 } 94 }, 95 // 手指移动 96 move (e) { 97 // this.casClearTop.clearRect(0,0,200,200); 98 let nowLeft = e.touches[0].pageX 99 let nowTop = e.touches[0].pageY 100 for (var i = 0; i < this.left.length; i++) { 101 // 圆心坐标 102 let oLeft = this.left[i] 103 let oTop = this.top[i] 104 if((oLeft - this.radius) <= nowLeft && nowLeft <= (oLeft + this.radius) && (oTop - this.radius) <= nowTop && nowTop <= (oTop + this.radius)) { 105 if (this.password.length === 0 && this.password.indexOf(i) === -1) { 106 this.password.push(i) // 直接存进密码 107 } else if(this.password.indexOf(i) === -1){ 108 console.log('连中的值:', this.password[this.password.length - 1]) 109 let value = this.password[this.password.length - 1] // 根据此值(下标)找出对应的this.left和this.top 110 // value是上一个点的值,i是当前连接点的值 111 // 1-9 9-1、3-7 7-3、2-8 8-2、4-6 6-4 112 if (i === 0 && value === 8 || i === 8 && value === 0 || 113 i === 2 && value === 6 || i === 6 && value === 2 || 114 i === 1 && value === 7 || i === 7 && value === 1 || 115 i === 3 && value === 5 || i === 5 && value === 3) { 116 // this.password中存的是下标 117 if (this.password.indexOf(4) === -1) {this.password.push(4)} 118 } else if(i === 2 && value === 0 || i === 0 && value === 2) { // 1-3 3-1 119 if (this.password.indexOf(1) === -1) {this.password.push(1)} 120 } else if(i === 6 && value === 8 || i === 8 && value === 6){ // 7-9 9-7 121 if (this.password.indexOf(7) === -1) {this.password.push(7)} 122 }else if(i === 0 && value === 6 || i === 6 && value === 0){ // 1-7 7-1 123 if (this.password.indexOf(3) === -1) {this.password.push(3)} 124 }else if(i === 2 && value === 8 || i === 8 && value === 2){ // 3-9 9-3 125 if (this.password.indexOf(5) === -1) {this.password.push(5)} 126 } 127 // 存密码 128 this.password.push(i) 129 } 130 } 131 } 132 this.paint(nowLeft, nowTop, true) 133 }, 134 // 画线的方法 135 paint (nowX, nowY, color) { 136 // console.log('paint') 137 // this.casClearTop.clearRect(0,0,200,200); // 因为不是在这个canvas上画的,所以清了也没用 138 this.cas.clearRect(0,0,this.clientWidth,this.clientHeight); // 每次画都清空整个画布 139 this.cas.beginPath(); 140 for (var i = 0; i < this.password .length; i++) { 141 this.cas.lineTo(this.left[this.password [i]], this.top[this.password [i]]); // 从这个开始 142 } 143 this.cas.lineTo(nowX, nowY); 144 if (!color) { 145 this.cas.strokeStyle = '#ff4b4b' 146 } else { 147 this.cas.strokeStyle = '#498bcb' 148 } 149 this.cas.lineJoin = "round" 150 this.cas.lineWidth = 2; 151 this.cas.stroke(); 152 // 清除li内圆形区域的线条 153 this.password.forEach((item) => { 154 this.clearArcFun(this.left[item], this.top[item], this.radius) 155 }) 156 }, 157 // 清除li内的圆形区域 158 clearArcFun (centerX, centerY, radius) { 159 var stepClear = 1; //别忘记这一步 160 var _this = this 161 clearArc(centerX, centerY, radius); 162 function clearArc(x, y, radius){ // 圆心x,y,半径radius 163 var calcWidth = radius - stepClear; 164 var calcHeight = Math.sqrt(radius * radius - calcWidth * calcWidth); 165 var posX = x - calcWidth; 166 var posY = y - calcHeight; 167 var widthX = 2 * calcWidth; 168 var heightY = 2 * calcHeight; 169 if(stepClear <= radius){ 170 _this.cas.clearRect(posX, posY, widthX, heightY); 171 stepClear += 1; 172 clearArc(x, y, radius); 173 } 174 } 175 }, 176 // 手指松开 177 end () { 178 console.log('end', this.password) 179 if (this.createPassword.length === 0) { // 创建密码的第一次 180 if(this.password.length >= 4) { 181 this.tips = false 182 // 此时再调用一次paint,传undefined, undefined,避免最后一条多余的线出现 183 this.paint(undefined, undefined, true) 184 // 不变红 185 this.redStyle = false 186 this.createPassword = this.password 187 this.$emit('firstDown', {success: true}) 188 // 500ms后清空样式 189 console.log('第一次设置密码createPassword:', this.createPassword) 190 console.log('第一次设置密码password:', this.password) 191 setTimeout(() => { 192 this.password = [] 193 this.cas.clearRect(0,0,this.clientWidth,this.clientHeight); 194 }, 500) 195 } else if(this.password.length < 4 && this.password.length !== 0) { 196 console.log('创建密码时长度小于4') 197 this.tips = true 198 this.paint(undefined, undefined, false) 199 // 长度小于4样式为红色 200 this.redStyle = true 201 // 清空画布,颜色变正常,不然下次输入还是红色 202 setTimeout(() => { 203 this.password = [] 204 this.cas.clearRect(0,0,this.clientWidth,this.clientHeight); 205 this.redStyle = false // 颜色变蓝,不然下次输入还是红色 206 }, 500) 207 } 208 } else { // 创建密码的第二次 或者 登录,不管是啥反正都是拿password和createPassword(第一次输入的密码或者父组件传过来的密码)比较 209 console.log('createPassword.length不为0,进入密码比较环节') 210 console.log('createPassword:', this.createPassword) 211 console.log('password:', this.password) 212 if (this.password.toString() === this.createPassword.toString()) { 213 this.tips = false 214 // 设置/登录成功 215 console.log('设置/登录成功') 216 this.$emit('onDrawDone', {success: true, pwd: this.password}) 217 setTimeout(() => { 218 this.password = [] 219 this.cas.clearRect(0,0,this.clientWidth,this.clientHeight); 220 this.redStyle = false // 没true好像就可以没有false,加上吧保险一点 221 }, 500) 222 } else if(this.password.length !== 0){ // 两次输入不一致/密码不正确 这里写this.password.length !== 0是为了防止点一下canvas也会出现输入错误的提示 223 this.tips = true 224 this.paint(undefined, undefined, false) 225 // 两次输入不一致/密码不正确 样式为红色 226 this.redStyle = true // 有true下面必得有false 227 console.log('失败') 228 // 清空画布,颜色变蓝 229 setTimeout(() => { 230 this.password = [] // 还有蓝色是因为前几个存在于那个数组,得把password清空 231 this.cas.clearRect(0,0,this.clientWidth,this.clientHeight); 232 this.redStyle = false 233 console.log(this.redStyle) 234 }, 500) 235 } 236 } 237 } 238 } 239 } 240 </script> 241 242 <style lang="less" scoped> 243 .incorrectTip{ 244 height: .5rem; 245 span{ 246 /*line-height: .8rem;*/ 247 color: #ff4b4b; 248 } 249 } 250 .gestureUnlock{ 251 margin: 0 auto; 252 } 253 .gesture{ 254 margin: 1.0rem auto 0; 255 ul{ 256 margin: auto; 257 display: flex; 258 width: 8.88rem; 259 height: 8.88rem; 260 justify-content: space-between; 261 align-content: space-between; 262 flex-wrap: wrap; 263 li{ 264 display: flex; 265 align-items:center; 266 justify-content:center; 267 margin: 0.45rem 0.45rem; 268 border-radius: 50%; 269 width: 1.2rem; 270 height: 1.2rem; 271 border: 0.08rem solid #e0e0e0; 272 /*宽度是1.2rem,边框是0.08rem,所以半径是0.68rem,1rem=37.5px,所以0.68x37.5 = 25.5px*/ 273 span{ 274 display: flex; 275 align-items:center; 276 justify-content:center; 277 width: 0.40rem; 278 height: 0.40rem; 279 border-radius: 50%; 280 i{ 281 display: inline-block; 282 width: 1px; 283 height: 1px; 284 } 285 } 286 } 287 /*被选中的样式*/ 288 .selectedOuter{ 289 border: 0.08rem solid #498bcb; 290 .selectedInside{ 291 background: #498bcb; 292 } 293 } 294 .selectedOuter2{ 295 border: 0.08rem solid #ff4b4b; 296 .selectedInside2{ 297 background: #ff4b4b; 298 } 299 } 300 } 301 } 302 .canvasDiv{ 303 position: fixed; 304 top:0; 305 left: 0; 306 // background: rgba(0,0,0,0.1); 307 z-index: 100; 308 #canvasClearTop{ 309 position: absolute; 310 top: 0; 311 left: 0; 312 background: rgba(255,0,0,0.2) 313 } 314 }315 </style>
父组件调用(创建密码):
1 <template> 2 <!--首次登陆设置手势密码--> 3 <div class="createGesture"> 4 <div class="picture"> 5 <img :src='logoImg' alt=""> 6 </div> 7 <div class="words"> 8 <p v-if="!isShowConfirm">{{$t('createGesture.createGesture')}}</p> 9 <p v-if="!isShowConfirm">{{$t('createGesture.drawTips')}}.</p> 10 <p v-if="isShowConfirm">{{$t('createGesture.confirmGesture')}}</p> 11 <p v-if="isShowConfirm">{{$t('createGesture.drawTips2')}}.</p> 12 </div> 13 <!--下面这是模拟登录时传密码过去--> 14 <!-- <gestureUnlock @firstDown="onceDraw" @onDrawDone='fromNinePoint' :fatherPassword="[1,2,3,4,5]"></gestureUnlock> --> 15 <!--此页面是创建密码,需要输入两次,组件不传值,fatherPassword默认是一个空数组--> 16 <gestureUnlock @firstDown="onceDraw" @onDrawDone='fromNinePoint'></gestureUnlock> 17 <!-- @firstDown="onceDraw"是第一次输入密码的事件 @onDrawDone='fromNinePoint'第二次完成密码的事件 --> 18 <div class="bottom"> 19 <p>{{$t('createGesture.bottomTips')}}.</p> 20 <div> 21 <a class="btn_text" @click="skip"> {{$t('createGesture.skip')}} </a> 22 </div> 23 </div> 24 </div> 25 </template> 26 27 <script> 28 import gestureUnlock from '../../components/gestureUnlock' 29 import Vue from 'vue'; 30 import { Grid, GridItem } from 'vant'; 31 Vue.use(Grid).use(GridItem); 32 export default { 33 name: "createGesture", 34 components: { 35 gestureUnlock 36 }, 37 data() { 38 return { 39 logoImg: require('./Zurich_logo.png'), 40 firstPwd: '', // 用来存创建密码时第一次输入的密码,便于和第二次比较 41 regOrLogin: 'reg', // 传给子组件用于判断是注册还是登陆 42 isShowConfirm: false, // 是否显示confirm密码 43 } 44 }, 45 methods: { 46 onceDraw (e) { 47 if (e.success) { 48 console.log('第一次') 49 this.isShowConfirm = true 50 } 51 }, 52 fromNinePoint (e) { 53 if(e.success) { 54 console.log('父组件:', e.pwd, '手势密码设置完成,登录') 55 this.send(e.pwd.join('')) 56 } 57 }, 58 send (gesPwd) { 59 console.log('手势密码:', gesPwd) 60 this.$axios.post('http://*****/*****/****/gesturePasswordSetup', { 61 username: '123', 62 gesturePassword: gesPwd, // 手势密码 63 }) 64 .then((res) => { 65 console.log('返回的数据:', res) 66 let flag = res.data.flagStr 67 if (flag === 'Succ') { 68 console.log('设置成功') 69 } 70 }) 71 .catch((res) => { 72 console.log('报错:', res) 73 }) 74 }, 75 // 跳过 76 skip () { 77 this.$router.push({name: '/'}) 78 } 79 } 80 } 81 </script> 82 83 <style lang="less" scoped> 84 .createGesture{ 85 height: 100%; 86 } 87 .picture{ 88 padding-top: 0.533rem; 89 text-align: center; 90 img { 91 height: 3rem; 92 } 93 } 94 .words{ 95 text-align: center; 96 color: #498bcb; 97 p:nth-child(1) { 98 margin: 0.267rem 0; 99 font-size: 0.723rem; 100 } 101 p:nth-child(2) { 102 font-size: 0.373rem; 103 } 104 } 105 .bottom{ 106 z-index: 2000; 107 margin-top: .3rem /* 30/37.5 */; 108 width: 100%; 109 p{ 110 padding: 0 0.5rem; 111 font-size: inherit; 112 } 113 div{ 114 margin: 0.353rem 0 0.337rem; 115 } 116 } 117 </style>
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步