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>
相关推荐
zyk_5204 分钟前
前端渲染pdf文件解决方案-pdf.js
前端·javascript·pdf
Apifox.11 分钟前
Apifox 4月更新|Apifox在线文档支持LLMs.txt、评论支持使用@提及成员、支持为团队配置「IP 允许访问名单」
前端·人工智能·后端·ai·ai编程
划水不带桨18 分钟前
大数据去重
前端
沉迷...22 分钟前
手动实现legend 与 echarts图交互 通过js事件实现图标某项的高亮 显示与隐藏
前端·javascript·echarts
可观测性用观测云37 分钟前
观测云数据在Grafana展示的最佳实践
前端
uwvwko1 小时前
ctfhow——web入门214~218(时间盲注开始)
前端·数据库·mysql·ctf
Json____1 小时前
使用vue2开发一个医疗预约挂号平台-前端静态网站项目练习
前端·vue2·网站模板·静态网站·项目练习·挂号系统
littleplayer1 小时前
iOS Swift Redux 架构详解
前端·设计模式·架构
工呈士1 小时前
HTML 模板技术与服务端渲染
前端·html
皮实的芒果1 小时前
前端实时通信方案对比:WebSocket vs SSE vs setInterval 轮询
前端·javascript·性能优化