uniapp+vue3移动端实现输入验证码

代码实现如上效果(不能灵活删除)

ts 复制代码
<template>
  <view class="uni-flex uni-flex-center vcode-input-body">
    <view
      class="vcode-input-item vcode-input-line"
      v-for="(v, index) in sum"
      :key="index"
      :style="getStyle(index)"
    >
      {{ text[index] ? text[index] : '' }}
      <view v-if="focus && text.length === index" class="cursor"></view>
    </view>
    <input
      ref="VcodeInput"
      type="number"
      class="hidden-input"
      :focus="focus"
      :maxlength="sum"
      @input="inputVal"
      @blur="setBlur"
      :password="isPassword"
      placeholder="验证码"
    />
  </view>
</template>

<script>
export default {
  name: 'VcodeInput',
  props: {
    sum: {
      type: Number,
      default: 6,
    },
    isBorderLine: {
      type: Boolean,
      default: false,
    },
    borderColor: {
      type: String,
      default: '#DADADA',
    },
    borderValueColor: {
      type: String,
      default: '#424456',
    },
    borderActiveColor: {
      type: String,
      default: '#000000',
    },
    isAutoComplete: {
      type: Boolean,
      default: true,
    },
    isPassword: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      focus: false,
      text: [],
      currentIndex: 0,
    }
  },
  created() {
    setTimeout(() => {
      this.focus = true
    }, 300)
  },
  methods: {
    getStyle(index) {
      let style = {}
      style.borderColor = this.borderColor
      if (this.text.length > index) {
        style.borderColor = this.borderValueColor
        style.color = this.borderValueColor
      }
      if (this.currentIndex === index) {
        style.borderColor = this.borderActiveColor
      }
      return style
    },
    setBlur() {
      this.focus = false
    },
    setFocus(index) {
      this.focus = true
      this.currentIndex = index
      // 如果点击的位置在已输入字符之后,则移动到最后一个字符的位置
      if (index > this.text.length) {
        this.currentIndex = this.text.length
      }
      // 设置输入框的值,允许修改当前位置的内容
      const input = this.$refs.VcodeInput
      if (input) {
        input.value = this.text.join('')
        // 设置光标位置
        setTimeout(() => {
          input.setSelectionRange(this.currentIndex, this.currentIndex)
        }, 0)
      }
    },
    inputVal(e) {
      let value = e.detail.value
      if (this.isAutoComplete) {
        if (value.length >= this.sum) {
          this.focus = false
          this.$emit('vcodeInput', value)
        }
      } else {
        this.$emit('vcodeInput', value)
      }
      if (this.isPassword) {
        let val = ''
        for (let i = 0; i < value.length; i++) {
          val += '●'
        }
        this.text = val
      } else {
        this.text = value
      }
      // 更新光标位置到输入位置
      this.currentIndex = value.length
    },
  },
}
</script>

<style lang="scss" scoped>
.vcode-input-body {
  width: 100%;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: row;
  justify-content: center;
  margin: 0 auto;
  .vcode-input-item {
    width: 76rpx;
    height: 76rpx;
    margin-left: 12rpx;
    margin-right: 12rpx;
    line-height: 76rpx;
    text-align: center;
    font-weight: 500;
    position: relative;
  }
  .vcode-input-border {
    border-style: solid;
    border-width: 2rpx;
    border-color: $uni-border-color;
    border-radius: 4rpx;
  }
  .vcode-input-line {
    border-bottom-style: solid;
    border-bottom-width: 2rpx;
    border-color: $uni-border-color;
  }
  .hidden-input {
    width: 1px;
    height: 1px;
    position: absolute;
    left: -1px;
    top: -1px;
  }
  .cursor {
    position: absolute;
    left: 30%;
    bottom: 18rpx;
    width: 2rpx;
    height: 30rpx;
    background-color: #000000;
    transform: translateX(-50%);
    animation: blink 1s infinite;
  }
}

@keyframes blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}
</style>

灵活删除代码实现(区分ios和安卓端)

复制代码
<template>
  <view v-if="isIos">
    <input
      ref="codeInputRef"
      class="hidden-input"
      type="number"
      :focus="isFocused"
      :maxlength="maxLength"
      v-model="verificationCode"
      @input="handleCodeInput"
      @focus="isFocused = true"
      @blur="isFocused = false"
    />
    <view class="verification-code-container">
      <view class="code-display">
        <view
          v-for="(_, index) in maxLength"
          :key="index"
          class="code-cell"
          :class="{
            active: isFocused && index === verificationCode.length,
            filled: index < verificationCode.length,
          }"
          @click="isFocused = true"
        >
          {{ verificationCode[index] || '' }}
        </view>
      </view>
    </view>
  </view>
  <view class="code-input-container" v-if="!isIos">
    <view v-for="(digit, index) in props.maxLength" :key="index" class="code-input">
      <input
        type="number"
        maxlength="1"
        class="phone-code-input"
        v-model="passwordDigits[index]"
        :focus="index === focusIndex"
        @input="handleInput(index)"
        @paste="handlePaste"
      />
    </view>
  </view>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits, watch, onMounted, onUnmounted } from 'vue'
const props = defineProps({
  maxLength: {
    type: Number,
    default: 0,
  },
})
const emit = defineEmits(['complete'])
const verificationCode = ref('')
const codeInputRef = ref()
const isFocused = ref(true)

const handleCodeInput = (e) => {
  const value = e.detail.value.replace(/\D/g, '') // 只保留数字
  verificationCode.value = value.slice(0, props.maxLength) // 限制位数
  // 输入完成
  if (value.length === props.maxLength) {
    onCodeComplete()
  }
}
// 验证码输入完成
const onCodeComplete = () => {
  // submitForm()
  emit('complete', verificationCode.value)
  uni.hideKeyboard()
}

// 设备类型
const isIos = ref(uni.getSystemInfoSync().platform === 'ios')

// 密码数组,初始化为空
const passwordDigits = ref(Array(props.maxLength).fill('')) // 处理输入

// 当前聚焦的输入框索引
const focusIndex = ref(0)

// 处理输入事件
const handleInput = (index: number) => {
  // 如果当前输入框有值,并且不是最后一个输入框,则自动聚焦到下一个输入框
  if (passwordDigits.value[index] !== '' && index < props.maxLength - 1) {
    focusIndex.value = index + 1
  }
  if (passwordDigits.value[index] === '' && index > 0) {
    focusIndex.value = index - 1
  }
}

// 键盘删除事件
const handleKeyUp = (event: PlusKeyKeyEvent) => {
  if (
    event.keyCode === 67 &&
    passwordDigits.value[focusIndex.value] === '' &&
    focusIndex.value > 0
  ) {
    focusIndex.value -= 1
  }
}
onMounted(() => {
  if (!isIos.value) {
    plus.key.addEventListener('keyup', handleKeyUp)
  }
})

onUnmounted(() => {
  if (!isIos.value) {
    plus.key.removeEventListener('keyup', handleKeyUp)
  }
})

const handlePaste = (event: Event) => {
  // 获取剪贴板内容(在某些平台上可能无法直接访问 clipboardData)
  if (uni.getClipboardData) {
    uni.getClipboardData({
      success: (res) => {
        // 提取粘贴内容中的所有数字
        const pasteContent = res.data.trim() // 去除可能的空格
        const extractedNumbers = pasteContent.replace(/\D/g, '') // 只保留数字,替换掉非数字字符
        // 如果提取的数字是6位,继续处理
        if (extractedNumbers.length === props.maxLength) {
          console.log('extractedNumbers', extractedNumbers)
          // 填充验证码数组
          passwordDigits.value = extractedNumbers.split('')
          onCodeComplete()
        }
      },
      fail: (err) => {
        console.log('获取剪贴板数据失败', err)
      },
    })
  } else {
    console.log('当前平台不支持获取剪贴板数据')
  }
}

// 监听密码输入完成
watch(
  passwordDigits,
  (newValue) => {
    // 检查是否所有数字都已输入
    const isAllFilled = newValue.every((digit) => digit !== '')
    if (isAllFilled) {
      // 短暂延迟以确保用户看到最后一个数字
      setTimeout(() => {
        emit('complete', passwordDigits.value.join(''))
      }, 200)
    }
  },
  { deep: true },
)
</script>
<style lang="scss" scoped>
.verification-code-container {
  position: relative;
  padding: 20rpx;
}

.hidden-input {
  position: absolute;
  left: -9999rpx;
  opacity: 0;
  width: 0;
  height: 0;
}

.code-display {
  display: flex;
  justify-content: center;
}

.code-cell {
  width: 80rpx;
  height: 80rpx;
  margin: 0 10rpx;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 40rpx;
  border-bottom: 2rpx solid #ddd;
  position: relative;
}

.code-cell.filled {
  border-bottom-color: #007aff;
}

.code-cell.active::after {
  content: '';
  position: absolute;
  bottom: 5rpx;
  left: 50%;
  transform: translateX(-50%);
  width: 40rpx;
  height: 4rpx;
  background-color: #007aff;
  animation: blink 1s infinite;
}

@keyframes blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}

.code-input-container {
  display: flex;
  justify-content: space-between;
  position: relative;
  margin-bottom: 40rpx;
}

.code-input {
  width: 80rpx;
  height: 100rpx;
  text-align: center;
  font-size: 40rpx;
  outline: none;
  background: transparent;
}

.phone-code-input {
  width: 80rpx;
  height: 100rpx;
  text-align: center;
  font-size: 40rpx;
  outline: none;
  background: transparent;
  border-bottom: 2rpx solid #ccc;
}
</style>
相关推荐
却尘2 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare2 小时前
浅浅看一下设计模式
前端
Lee川2 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix2 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人3 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl3 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人3 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼3 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空3 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust