vue项目中实现手势密码

tips:本文是做记录用的

思路:

  本来应该全部都用canvas来实现的,但时间紧迫 写的时候只想着圆圈用li写,线用canvas,写到一半才想通,不过还好这一通下来还算比较顺利

  第一步:页面中的9个点用v-for循环出来li,ul设置成宽高相等的正方形。给li设置margin,保证一行只能装得下三个li,然后ul用display:flex;justify-contentspace-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>
posted @ 2019-12-06 18:01  万变不离许嵩  阅读(3468)  评论(0编辑  收藏  举报