passcode-grid.vue 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <template>
  2. <view class="passcode-grid" :class="'passcode-grid--' + theme" @tap="focusInput">
  3. <view
  4. v-for="(_, idx) in cells"
  5. :key="idx"
  6. class="passcode-grid__cell"
  7. :class="{
  8. 'passcode-grid__cell--cursor': focused && idx === cursorAt,
  9. 'passcode-grid__cell--filled': idx < length,
  10. 'passcode-grid__cell--alert': alert && !disabled
  11. }"
  12. >
  13. <text v-if="cells[idx]" class="passcode-grid__char">{{ cells[idx] }}</text>
  14. <view v-else-if="focused && idx === cursorAt" class="passcode-grid__caret" />
  15. </view>
  16. <input
  17. ref="capture"
  18. class="passcode-grid__capture"
  19. type="number"
  20. :value="value"
  21. :focus="focused"
  22. :maxlength="cellCount"
  23. :disabled="disabled"
  24. @input="onCapture"
  25. @blur="onBlur"
  26. @focus="onFocus"
  27. />
  28. </view>
  29. </template>
  30. <script>
  31. export default {
  32. name: 'PasscodeGrid',
  33. props: {
  34. value: { type: String, default: '' },
  35. cellCount: { type: Number, default: 6 },
  36. disabled: { type: Boolean, default: false },
  37. alert: { type: Boolean, default: false },
  38. /** dark:通行证深色页;light:账号安全浅色页 */
  39. theme: { type: String, default: 'dark' }
  40. },
  41. data() {
  42. return {
  43. focused: false
  44. }
  45. },
  46. computed: {
  47. length() {
  48. return String(this.value || '').length
  49. },
  50. cursorAt() {
  51. return Math.min(this.length, this.cellCount - 1)
  52. },
  53. cells() {
  54. const slots = new Array(this.cellCount).fill('')
  55. const seq = String(this.value || '').slice(0, this.cellCount).split('')
  56. seq.forEach((ch, i) => {
  57. slots[i] = ch
  58. })
  59. return slots
  60. }
  61. },
  62. methods: {
  63. focusInput() {
  64. if (this.disabled) return
  65. this.focused = true
  66. this.$nextTick(() => {
  67. const node = this.$refs.capture
  68. if (node && typeof node.focus === 'function') node.focus()
  69. })
  70. this.$emit('focus')
  71. },
  72. onFocus() {
  73. this.focused = true
  74. },
  75. onBlur() {
  76. this.focused = false
  77. },
  78. onCapture(e) {
  79. const raw = String(e.detail.value || '').replace(/\D+/g, '').slice(0, this.cellCount)
  80. this.$emit('input', raw)
  81. if (raw.length === this.cellCount) this.$emit('settled', raw)
  82. }
  83. }
  84. }
  85. </script>
  86. <style lang="scss">
  87. .passcode-grid {
  88. position: relative;
  89. display: flex;
  90. justify-content: space-between;
  91. }
  92. .passcode-grid__cell {
  93. width: 78rpx;
  94. height: 96rpx;
  95. border-radius: 22rpx;
  96. border: 2rpx solid rgba(255, 255, 255, 0.22);
  97. background: rgba(255, 255, 255, 0.08);
  98. box-sizing: border-box;
  99. display: flex;
  100. align-items: center;
  101. justify-content: center;
  102. }
  103. .passcode-grid--light .passcode-grid__cell {
  104. border-color: #e5e9f2;
  105. background: #f7f9fc;
  106. }
  107. .passcode-grid__cell--filled {
  108. border-color: rgba(255, 255, 255, 0.38);
  109. background: rgba(255, 255, 255, 0.12);
  110. }
  111. .passcode-grid--light .passcode-grid__cell--filled {
  112. border-color: #4a8cff;
  113. background: #eef4ff;
  114. }
  115. .passcode-grid__cell--cursor {
  116. border-color: #ffb547;
  117. box-shadow: 0 0 24rpx rgba(255, 181, 71, 0.35);
  118. }
  119. .passcode-grid--light .passcode-grid__cell--cursor {
  120. border-color: #4a8cff;
  121. box-shadow: 0 0 16rpx rgba(74, 140, 255, 0.25);
  122. }
  123. .passcode-grid__cell--alert {
  124. border-color: #ff4d6a;
  125. background: rgba(255, 77, 106, 0.12);
  126. }
  127. .passcode-grid--light .passcode-grid__cell--alert {
  128. border-color: #ef4444;
  129. background: #fef2f2;
  130. }
  131. .passcode-grid__char {
  132. font-size: 40rpx;
  133. font-weight: 700;
  134. color: #fff;
  135. }
  136. .passcode-grid--light .passcode-grid__char {
  137. color: #1a1d26;
  138. }
  139. .passcode-grid__caret {
  140. width: 4rpx;
  141. height: 38rpx;
  142. background: #ffb547;
  143. border-radius: 4rpx;
  144. animation: passcodeBlink 1s steps(2, start) infinite;
  145. }
  146. .passcode-grid--light .passcode-grid__caret {
  147. background: #4a8cff;
  148. }
  149. .passcode-grid__capture {
  150. position: absolute;
  151. left: 0;
  152. right: 0;
  153. top: 0;
  154. bottom: 0;
  155. width: 100%;
  156. height: 100%;
  157. opacity: 0;
  158. color: transparent;
  159. background: transparent;
  160. border: 0;
  161. caret-color: transparent;
  162. z-index: 2;
  163. }
  164. @keyframes passcodeBlink {
  165. 0% { opacity: 0; }
  166. 50% { opacity: 1; }
  167. 100% { opacity: 0; }
  168. }
  169. </style>