| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- <template>
- <view class="passcode-grid" :class="'passcode-grid--' + theme" @tap="focusInput">
- <view
- v-for="(_, idx) in cells"
- :key="idx"
- class="passcode-grid__cell"
- :class="{
- 'passcode-grid__cell--cursor': focused && idx === cursorAt,
- 'passcode-grid__cell--filled': idx < length,
- 'passcode-grid__cell--alert': alert && !disabled
- }"
- >
- <text v-if="cells[idx]" class="passcode-grid__char">{{ cells[idx] }}</text>
- <view v-else-if="focused && idx === cursorAt" class="passcode-grid__caret" />
- </view>
- <input
- ref="capture"
- class="passcode-grid__capture"
- type="number"
- :value="value"
- :focus="focused"
- :maxlength="cellCount"
- :disabled="disabled"
- @input="onCapture"
- @blur="onBlur"
- @focus="onFocus"
- />
- </view>
- </template>
- <script>
- export default {
- name: 'PasscodeGrid',
- props: {
- value: { type: String, default: '' },
- cellCount: { type: Number, default: 6 },
- disabled: { type: Boolean, default: false },
- alert: { type: Boolean, default: false },
- /** dark:通行证深色页;light:账号安全浅色页 */
- theme: { type: String, default: 'dark' }
- },
- data() {
- return {
- focused: false
- }
- },
- computed: {
- length() {
- return String(this.value || '').length
- },
- cursorAt() {
- return Math.min(this.length, this.cellCount - 1)
- },
- cells() {
- const slots = new Array(this.cellCount).fill('')
- const seq = String(this.value || '').slice(0, this.cellCount).split('')
- seq.forEach((ch, i) => {
- slots[i] = ch
- })
- return slots
- }
- },
- methods: {
- focusInput() {
- if (this.disabled) return
- this.focused = true
- this.$nextTick(() => {
- const node = this.$refs.capture
- if (node && typeof node.focus === 'function') node.focus()
- })
- this.$emit('focus')
- },
- onFocus() {
- this.focused = true
- },
- onBlur() {
- this.focused = false
- },
- onCapture(e) {
- const raw = String(e.detail.value || '').replace(/\D+/g, '').slice(0, this.cellCount)
- this.$emit('input', raw)
- if (raw.length === this.cellCount) this.$emit('settled', raw)
- }
- }
- }
- </script>
- <style lang="scss">
- .passcode-grid {
- position: relative;
- display: flex;
- justify-content: space-between;
- }
- .passcode-grid__cell {
- width: 78rpx;
- height: 96rpx;
- border-radius: 22rpx;
- border: 2rpx solid rgba(255, 255, 255, 0.22);
- background: rgba(255, 255, 255, 0.08);
- box-sizing: border-box;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .passcode-grid--light .passcode-grid__cell {
- border-color: #e5e9f2;
- background: #f7f9fc;
- }
- .passcode-grid__cell--filled {
- border-color: rgba(255, 255, 255, 0.38);
- background: rgba(255, 255, 255, 0.12);
- }
- .passcode-grid--light .passcode-grid__cell--filled {
- border-color: #4a8cff;
- background: #eef4ff;
- }
- .passcode-grid__cell--cursor {
- border-color: #ffb547;
- box-shadow: 0 0 24rpx rgba(255, 181, 71, 0.35);
- }
- .passcode-grid--light .passcode-grid__cell--cursor {
- border-color: #4a8cff;
- box-shadow: 0 0 16rpx rgba(74, 140, 255, 0.25);
- }
- .passcode-grid__cell--alert {
- border-color: #ff4d6a;
- background: rgba(255, 77, 106, 0.12);
- }
- .passcode-grid--light .passcode-grid__cell--alert {
- border-color: #ef4444;
- background: #fef2f2;
- }
- .passcode-grid__char {
- font-size: 40rpx;
- font-weight: 700;
- color: #fff;
- }
- .passcode-grid--light .passcode-grid__char {
- color: #1a1d26;
- }
- .passcode-grid__caret {
- width: 4rpx;
- height: 38rpx;
- background: #ffb547;
- border-radius: 4rpx;
- animation: passcodeBlink 1s steps(2, start) infinite;
- }
- .passcode-grid--light .passcode-grid__caret {
- background: #4a8cff;
- }
- .passcode-grid__capture {
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- width: 100%;
- height: 100%;
- opacity: 0;
- color: transparent;
- background: transparent;
- border: 0;
- caret-color: transparent;
- z-index: 2;
- }
- @keyframes passcodeBlink {
- 0% { opacity: 0; }
- 50% { opacity: 1; }
- 100% { opacity: 0; }
- }
- </style>
|