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>