← All Blocks Auth 鉴权

Login OTP 短信验证登录

PhoneInput 国家码 + OtpInput 6 位验证码 + 60s 倒计时重发。code 输满 6 位时自动 submit。

login-otp source
LoginOtp.vue vue
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue';
import { CfCard, CfButton, CfOtpInput, CfLink, CfPhoneInput } from '@chufix-design/vue';

const phone = ref('138 0013 8000');
const country = ref('CN');
const code = ref('');
const sent = ref(false);
const remaining = ref(0);
let timer: number | null = null;

function send() {
  sent.value = true;
  remaining.value = 60;
  if (timer) window.clearInterval(timer);
  timer = window.setInterval(() => {
    remaining.value -= 1;
    if (remaining.value <= 0 && timer) {
      window.clearInterval(timer);
      timer = null;
    }
  }, 1000);
}

watch(code, (v) => {
  if (v.length === 6) alert(`OTP submitted: ${v}`);
});

onBeforeUnmount(() => {
  if (timer) window.clearInterval(timer);
});
</script>

<template>
  <div class="otp">
    <CfCard class="otp__card">
      <h2>短信验证</h2>
      <p>我们会向 <strong>+{{ country === 'CN' ? '86' : '1' }} {{ phone }}</strong> 发送一次性验证码。</p>

      <div class="otp__phone">
        <CfPhoneInput v-model="phone" v-model:country="country" :disabled="sent" />
      </div>

      <div v-if="sent" class="otp__digits">
        <CfOtpInput v-model="code" :length="6" />
        <p class="otp__hint">
          <template v-if="remaining > 0">
            没收到?{{ remaining }}s 后可重发
          </template>
          <template v-else>
            <CfLink href="#" @click.prevent="send">重新发送验证码</CfLink>
          </template>
        </p>
      </div>

      <CfButton v-if="!sent" variant="primary" block @click="send">发送验证码</CfButton>
      <CfButton v-else variant="primary" block :disabled="code.length !== 6">确认</CfButton>
    </CfCard>
  </div>
</template>

<style scoped>
.otp {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  min-height: 480px;
  font-family: var(--font-sans);
}
.otp__card {
  width: 100%;
  max-width: 420px;
  padding: 28px;
}
.otp__card h2 {
  margin: 0;
  font-size: var(--t-22);
  font-weight: var(--w-medium);
  color: var(--fg-1);
}
.otp__card p {
  color: var(--fg-2);
  font-size: var(--t-13);
  margin: 4px 0 18px;
}
.otp__phone { margin-bottom: 16px; }
.otp__digits { margin-bottom: 16px; }
.otp__hint {
  margin: 8px 0 0;
  font-size: var(--t-12);
  color: var(--fg-3);
}
</style>
LoginOtp.tsx tsx
import { useEffect, useRef, useState } from 'react';
import { CfCard, CfButton, CfOtpInput, CfLink, CfPhoneInput } from '@chufix-design/react';

export function LoginOtp() {
  const [phone, setPhone] = useState('138 0013 8000');
  const [country, setCountry] = useState('CN');
  const [code, setCode] = useState('');
  const [sent, setSent] = useState(false);
  const [remaining, setRemaining] = useState(0);
  const timerRef = useRef<number | null>(null);

  function send() {
    setSent(true);
    setRemaining(60);
    if (timerRef.current) window.clearInterval(timerRef.current);
    timerRef.current = window.setInterval(() => {
      setRemaining((r) => {
        if (r <= 1 && timerRef.current) {
          window.clearInterval(timerRef.current);
          timerRef.current = null;
          return 0;
        }
        return r - 1;
      });
    }, 1000);
  }

  useEffect(() => {
    if (code.length === 6) alert(`OTP submitted: ${code}`);
  }, [code]);

  useEffect(() => () => {
    if (timerRef.current) window.clearInterval(timerRef.current);
  }, []);

  return (
    <div className="otp">
      <CfCard className="otp__card">
        <h2>短信验证</h2>
        <p>
          我们会向 <strong>+{country === 'CN' ? '86' : '1'} {phone}</strong> 发送一次性验证码。
        </p>

        <div className="otp__phone">
          <CfPhoneInput value={phone} onChange={setPhone} country={country} onCountryChange={setCountry} disabled={sent} />
        </div>

        {sent && (
          <div className="otp__digits">
            <CfOtpInput value={code} onChange={setCode} length={6} />
            <p className="otp__hint">
              {remaining > 0 ? (
                <>没收到?{remaining}s 后可重发</>
              ) : (
                <CfLink href="#" onClick={(e) => { e.preventDefault(); send(); }}>重新发送验证码</CfLink>
              )}
            </p>
          </div>
        )}

        {!sent ? (
          <CfButton variant="primary" block onClick={send}>发送验证码</CfButton>
        ) : (
          <CfButton variant="primary" block disabled={code.length !== 6}>确认</CfButton>
        )}
      </CfCard>
    </div>
  );
}