共计 8640 个字符,预计需要花费 22 分钟才能阅读完成。
微信、支付宝领取明码时的明码输入框大家都很相熟,它由 6 个小格子组成,输出一个明码后会主动跳转到另一个格子,明码输出实现后就能够提交数据,在 web 并没有原生的这种输入框,要想应用这种输入框须要本人去实现。
最近的一个我的项目中须要应用这种输入框,因为我的项目比拟小,因而就没有用其余的 UI 库,于是就本人造了一个明码输入框的轮子,成果如下图:
安卓设施:
iOS 设施:
在 ios 上有一个小小的问题:输入框取得焦点后无奈主动弹窗软键盘
,有大神晓得解决办法的话请评论区留言告知!!
1、DOM 构造
dom 构造这块比较简单,次要由:
- 暗藏的
input
次要作用是:调起零碎软键盘以及输出内容 - 6 个小格子
- 虚构光标
3 局部组成。
<template> | |
<div class="bs-password-input" ref="passwordInputRef"> | |
<input | |
ref="realInput" | |
type="number" | |
inputmode="numeric" | |
class="hidden-input" | |
@input="onInput" | |
@blur="blur"> | |
<ul | |
class="bs-password-input-security" | |
:class="{'has-gap': hasGap}"@click="focus"> | |
<li | |
class="bs-password-input-item" | |
:class="{'is-focus': focusInputIndex === index}"v-for="(pwd, index) in passwords":key="index"> | |
<i v-if="mask && (pwd !==' ')" class="password-input-dot"></i> | |
<template v-if="!mask">{{pwd}}</template> | |
<div | |
v-if="showInputCursor" | |
class="bs-password-input-cursor"></div> | |
</li> | |
</ul> | |
<div class="bs-password-input-info" v-if="info">{{info}}</div> | |
</div> | |
</template> |
2、显示明码格子
界面上的明码格子数量次要通过父组件传递的明码及明码长度来计算
let passwords = computed(function () { | |
let value = props.modelValue; | |
if (typeof value !== 'string' && typeof value !== 'number') {value = '';} else {value = value + '';} | |
// console.log('value', value); | |
let resultArr = value.split(''); | |
let len = props.length; // 明码长度限度,默认为 6 位 | |
let diff = value.length - props.length; | |
if (diff > 0) {resultArr = value.substr(0, len).split(''); | |
} else if (diff < 0) {diff = Math.abs(diff); | |
// 如果传递的 modelValue 长度小于明码长度,则填补补空格,以达到界面上明码格子数量等于明码长度 | |
while (diff > 0) {resultArr.push(' '); | |
diff--; | |
} | |
} | |
return resultArr; | |
}); |
3、计算取得焦点的格子的索引
当在输出时、删除时、用户点击格子时都须要晓得以后哪个格子应该取得焦点,格子取得焦点后虚构光标才会显示进去。如果 calcFocusInputIndex()
执行后的值为 -1
则阐明明码输出实现,此时没人任何格子须要取得焦点
let calcFocusInputIndex = function () { | |
let pwdVal = passwords.value; | |
let index = -1; | |
let realPwdVal = trim(pwdVal.join('')); | |
console.log('realPwdVal', realPwdVal, realPwdVal.length, pwdVal); | |
for (let i = 0, len = pwdVal.length; i < len; i++) {// pwdVal[i]为空格示意该格子并没有真正的值 | |
if (pwdVal[i] === ' ' && realPwdVal.length !== props.length) { | |
index = i; | |
break; | |
} | |
} | |
console.log('index', index); | |
return index; | |
}; |
4、监听输出
在用户输出明码时如果输出的不是数字,则需清空 input
里的内容
当用户输出的明码长度等于 props.length
时立刻实现输出
let onInput = function (evt) { | |
let inputValue = evt.target.value; | |
if (inputValue && !numberReg.test(inputValue)) { // 如果输出的不是数字则清空输入框 | |
evt.target.value = ''; | |
return; | |
} | |
console.log('输出的字符为:', inputValue); | |
let password = passwords.value.join(''); | |
password = trim(password); | |
password += inputValue; | |
evt.target.value = ''; | |
ctx.emit('update:modelValue', password); | |
if (password.length == props.length) {ctx.emit('complete', password); | |
} | |
// 暗藏输入框焦点 | |
nextTick(function () {let inputIndex = calcFocusInputIndex(); | |
if (inputIndex == -1) {blur(); | |
} else {focusInputIndex.value = inputIndex;} | |
}); | |
console.log('更新 modelValue', password); | |
}; |
5、暗藏掉 input
的光标
因为格子自身就有虚构光标,那么此时 input
的光标就不须要再显示进去了。另外,就算没有虚构光标也很难将 input
的光标精确的定位到每一个格子两头。
暗藏掉 input
光标次要靠 3 个技巧:
- 用相对定位来暗藏
input
,而不是通过display: none;
暗藏 - 字体色彩设为通明,字体暗影设为 0
text-indent
设置一个略微大点的负值(次要解决 ios 设施中光标暗藏不了问题)
.hidden-input{ | |
position: absolute; | |
top: 5px; | |
z-index: 1; | |
/* 暗藏光标 start */ | |
color: transparent; | |
text-shadow: 0 0 0 #000; | |
/* 暗藏光标 end */ | |
/* 暗藏 ios 设施光标 start */ | |
text-indent: -999em; | |
margin-left: -40%; | |
/* 暗藏 ios 设施光标 end */ | |
} |
6、残缺代码
PasswordInput.vue
<template> | |
<div class="bs-password-input" ref="passwordInputRef"> | |
<input | |
ref="realInput" | |
type="number" | |
inputmode="numeric" | |
class="hidden-input" | |
@input="onInput" | |
@blur="blur"> | |
<ul | |
class="bs-password-input-security" | |
:class="{'has-gap': hasGap}"@click="focus"> | |
<li | |
class="bs-password-input-item" | |
:class="{'is-focus': focusInputIndex === index}"v-for="(pwd, index) in passwords":key="index"> | |
<i v-if="mask && (pwd !==' ')" class="password-input-dot"></i> | |
<template v-if="!mask">{{pwd}}</template> | |
<div | |
v-if="showInputCursor" | |
class="bs-password-input-cursor"></div> | |
</li> | |
</ul> | |
<div class="bs-password-input-info" v-if="info">{{info}}</div> | |
</div> | |
</template> | |
<script> | |
import { | |
ref, | |
computed, | |
onMounted, | |
onUnmounted, | |
nextTick | |
} from 'vue'; | |
const trim = function (str) {if (typeof str !== 'string' || str.length === 0) {return str;} | |
str += ''; | |
// 革除字符串两端空格,蕴含换行符、制表符 | |
return str.replace(/(^[\s\n\t]+|[\s\n\t]+$)/g, ''); | |
} | |
export default { | |
name: "PasswordInput", | |
props: { | |
modelValue: { // 明码值 | |
type: [String, Number], | |
default: '' | |
}, | |
hasGap: { // 是否有间隙 | |
type: Boolean, | |
default: false | |
}, | |
mask: { // 是否暗藏明码内容 | |
type: Boolean, | |
default: true | |
}, | |
length: { // 明码最大长度 | |
type: Number, | |
default: 6 | |
}, | |
info: { // 输入框下方文字提醒 | |
type: String, | |
default: '' | |
} | |
}, | |
setup (props, ctx) {let passwordInputRef = ref(null); | |
let realInput = ref(null); | |
let passwords = computed(function () { | |
let value = props.modelValue; | |
if (typeof value !== 'string' && typeof value !== 'number') {value = '';} else {value = value + '';} | |
// console.log('value', value); | |
let resultArr = value.split(''); | |
let len = props.length; | |
let diff = value.length - props.length; | |
if (diff > 0) {resultArr = value.substr(0, len).split(''); | |
} else if (diff < 0) {diff = Math.abs(diff); | |
while (diff > 0) {resultArr.push(' '); | |
diff--; | |
} | |
} | |
return resultArr; | |
}); | |
// 计算取得焦点的虚构输入框的索引 | |
let calcFocusInputIndex = function () { | |
let pwdVal = passwords.value; | |
let index = -1; | |
let realPwdVal = trim(pwdVal.join('')); | |
console.log('realPwdVal', realPwdVal, realPwdVal.length, pwdVal); | |
for (let i = 0, len = pwdVal.length; i < len; i++) {if (pwdVal[i] === ' ' && realPwdVal.length !== props.length) { | |
index = i; | |
break; | |
} | |
} | |
console.log('index', index); | |
return index; | |
}; | |
let nativeInputFocus = ref(false); | |
let showInputCursor = ref(false); | |
let focusInputIndex = ref(null); | |
let focus = function () {let index = calcFocusInputIndex(); | |
if (index > -1) {realInput.value.focus(); | |
nativeInputFocus.value = true; | |
showInputCursor.value = true; | |
focusInputIndex.value = index; | |
} else {realInput.value.focus(); | |
nativeInputFocus.value = true; | |
} | |
}; | |
let blur = function () { | |
showInputCursor.value = false; | |
focusInputIndex.value = null; | |
realInput.value.blur(); | |
realInput.value.value = ''; | |
nativeInputFocus.value = false; | |
}; | |
let numberReg = /^\d+$/; | |
let onInput = function (evt) { | |
let inputValue = evt.target.value; | |
if (inputValue && !numberReg.test(inputValue)) { // 如果输出的不是数字则清空输入框 | |
evt.target.value = ''; | |
return; | |
} | |
console.log('输出的字符为:', inputValue); | |
let password = passwords.value.join(''); | |
password = trim(password); | |
password += inputValue; | |
evt.target.value = ''; | |
ctx.emit('update:modelValue', password); | |
if (password.length == props.length) {ctx.emit('complete', password); | |
} | |
// 暗藏输入框焦点 | |
nextTick(function () {let inputIndex = calcFocusInputIndex(); | |
if (inputIndex == -1) {blur(); | |
} else {focusInputIndex.value = inputIndex;} | |
}); | |
console.log('更新 modelValue', password); | |
}; | |
let keydownEvent = function (evt) { | |
let keyCode = evt.keyCode; | |
console.log('keyCode', keyCode); | |
if (!nativeInputFocus.value) {console.log('原生输入框未取得焦点'); | |
return; | |
} | |
if (keyCode == 8) { // 删除键 | |
let password = passwords.value.join(''); | |
password = trim(password); | |
if (password.length == 0) {return;} | |
password = password.substr(0, password.length - 1); | |
console.log('new password', password); | |
ctx.emit('update:modelValue', password); | |
// 暗藏输入框焦点 | |
nextTick(function () {let inputIndex = calcFocusInputIndex(); | |
if (inputIndex == -1) {blur(); | |
} else { | |
focusInputIndex.value = inputIndex; | |
focus();} | |
}); | |
} | |
}; | |
onMounted(function () {document.addEventListener('keydown', keydownEvent, false); | |
}); | |
onUnmounted(function () {document.removeEventListener('keydown', keydownEvent, false); | |
}); | |
return { | |
realInput, | |
passwordInputRef, | |
passwords, | |
showInputCursor, | |
focusInputIndex, | |
blur, | |
focus, | |
onInput | |
}; | |
} | |
}; | |
</script> | |
<style lang="less"> | |
@import "password-input"; | |
</style> |
password-input.less
.bs-password-input{ | |
position: relative; | |
overflow: hidden; | |
.hidden-input{ | |
position: absolute; | |
top: 5px; | |
z-index: 1; | |
/* 暗藏光标 start */ | |
color: transparent; | |
text-shadow: 0 0 0 #000; | |
/* 暗藏光标 end */ | |
/* 暗藏 ios 设施光标 start */ | |
text-indent: -999em; | |
margin-left: -40%; | |
/* 暗藏 ios 设施光标 end */ | |
} | |
} | |
.bs-password-input-security{ | |
position: relative; | |
z-index: 5; | |
display: flex; | |
height: 40px; | |
user-select: none; | |
background-color: #fff; | |
} | |
.bs-password-input-item{ | |
position: relative; | |
z-index: 5; | |
display: flex; | |
flex: 1; | |
justify-content: center; | |
align-items: center; | |
height: 100%; | |
cursor: pointer; | |
font-size: 20px; | |
background-color: #F2F2F2; | |
&:not(:first-child)::before{ | |
position: absolute; | |
top: 0; | |
left: 0; | |
bottom: 0; | |
content: ' '; | |
width: 1px;/*no*/ | |
background-color: #ececec; | |
} | |
&.is-focus{ | |
.password-input-dot{visibility: hidden;} | |
.bs-password-input-cursor{display: block;} | |
} | |
} | |
.password-input-dot{ | |
width: 12px; | |
height: 12px; | |
border-radius: 50%; | |
background-color: #000; | |
} | |
.bs-password-input-cursor{ | |
display: none; | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
width: 1px;/*no*/ | |
height: 40%; | |
transform: translate(-50%, -50%); | |
cursor: pointer; | |
background-color: rgba(32,32,32,3); | |
animation: 1s cursor-flicker infinite; | |
} | |
.bs-password-input-security{ | |
&.has-gap{ | |
.bs-password-input-item{ | |
border-radius: 4px; | |
&::before{display: none;} | |
&:not(:first-child){margin-left: 15px;} | |
} | |
} | |
} | |
.bs-password-input-info { | |
margin-top: 15px; | |
color: #999; | |
text-align: center; | |
} | |
@keyframes cursor-flicker { | |
0% {opacity: 0;} | |
50% {opacity: 1;} | |
100% {opacity: 0;} | |
} |
应用
<template> | |
<div class="pay"> | |
<PasswordInput | |
ref="passwordInputRef" | |
v-model="password" | |
has-gap | |
@complete="onPasswordInputComplete"></PasswordInput> | |
</div> | |
</template> | |
<script> | |
import { | |
ref, | |
onMounted | |
} from 'vue'; | |
import PasswordInput from './PasswordInput'; | |
export default { | |
name: "PayDialog", | |
components: {PasswordInput}, | |
setup (props, ctx) {let passwordInputRef = ref(null); | |
let password = ref(''); | |
let onPasswordInputComplete = function (pwd) {console.log('明码输出实现:', pwd); | |
}; | |
onMounted(function () { | |
// 让明码框取得焦点 | |
passwordInputRef.value.focus();}); | |
return { | |
password, | |
passwordInputRef, | |
onPasswordInputComplete | |
}; | |
} | |
}; | |
</script> |
正文完