<script lang="ts" setup>
import { ref } from 'vue'
const PHONE_NUMBER_REGEX = /^1[0-9]{10}$/
const PHONE_NUMBER_MESSAGE = '手机号格式不正确'
const VERIFICATION_CODE_REGEX = /^[0-9]{6}$/
const LOGIN_ERROR_MESSAGE = '登录失败,请检查网络连接并重试'
const GENERATE_CODE_ERROR_MESSAGE = '无法生成验证码,请检查网络连接并重试'
const phoneNumber = ref('')
const verificationCode = ref('')
const countdown = ref(0)
const isLoggingIn = ref(false)
const errorMessage = ref('')
const handleSubmit = async () => {
// 格式验证
if (!PHONE_NUMBER_REGEX.test(phoneNumber.value)) {
errorMessage.value = PHONE_NUMBER_MESSAGE
return
}
if (!VERIFICATION_CODE_REGEX.test(verificationCode.value)) {
errorMessage.value = '验证码格式不正确'
return
}
isLoggingIn.value = true
errorMessage.value = ''
try {
// 并行执行发送登录请求和验证验证码请求
const [loginResult, verifyResult] = await Promise.all([
fetch(`https://example.com/api/login?phone=${phoneNumber.value}&code=${verificationCode.value}`),
fetch(`https://example.com/api/verify?phone=${phoneNumber.value}&code=${verificationCode.value}`),
])
if (!loginResult.ok || !verifyResult.ok) {
errorMessage.value = LOGIN_ERROR_MESSAGE
return
}
// 登录成功后重置表单
phoneNumber.value = ''
verificationCode.value = ''
}
catch (error) {
errorMessage.value = LOGIN_ERROR_MESSAGE
return
}
finally {
isLoggingIn.value = false
}
}
const generateCode = async () => {
// 格式验证
if (!PHONE_NUMBER_REGEX.test(phoneNumber.value)) {
errorMessage.value = PHONE_NUMBER_MESSAGE
return
}
// 先禁用按钮
countdown.value = 60
const timer = setInterval(() => {
countdown.value -= 1
if (countdown.value === 0)
clearInterval(timer)
}, 1000)
try {
const response = await fetch(`https://example.com/api/sendcode?phone=${phoneNumber.value}`)
if (!response.ok)
throw new Error(GENERATE_CODE_ERROR_MESSAGE)
}
catch (error) {
errorMessage.value = GENERATE_CODE_ERROR_MESSAGE // error.message
countdown.value = 0
clearInterval(timer)
}
}
</script>
<template>
<div class="form">
<h3 class="form__title">
登录
</h3>
<div class="form__group">
<label class="form__label" for="phone">电话</label>
<input id="phone" v-model.trim="phoneNumber" class="form__input" type="tel" :class="{ 'is-invalid': errorMessage && !PHONE_NUMBER_REGEX.test(phoneNumber) }">
<!-- <div v-if="errorMessage && !PHONE_NUMBER_REGEX.test(phoneNumber)" class="form__error">
手机号格式不正确
</div> -->
</div>
<div class="form__group">
<label class="form__label" for="verification-code">验证码</label>
<div class="form__group form__group--inline">
<input
id="verification-code"
v-model.trim="verificationCode"
class="form__input form__input--inline"
type="text"
:class="{ 'is-invalid': errorMessage && !VERIFICATION_CODE_REGEX.test(verificationCode) }"
>
<button
class="form__button form__button--inline"
type="button"
:disabled="countdown > 0"
@click="generateCode"
>
{{ countdown > 0 ? `${countdown}秒后可重新获取` : '获取验证码' }}
</button>
</div>
<!-- <div v-if="errorMessage && !VERIFICATION_CODE_REGEX.test(verificationCode)" class="form__error">
验证码格式不正确
</div> -->
</div>
<div v-if="errorMessage" class="form__error">
{{ errorMessage }}
</div>
<button
class="form__button"
type="button"
:disabled="isLoggingIn"
@click="handleSubmit"
>
{{ isLoggingIn ? '登录中...' : '登录' }}
</button>
</div>
</template>
<style scoped>
/* 清除默认样式 */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* 设置容器最小宽度为 320px */
body {
min-width: 320px;
}
/* 设置字体 */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 16px;
}
/* 设置颜色 */
body {
color: #333;
background-color: #f9f9f9;
}
/* 设置表单样式 */
.form {
margin: 1rem;
padding: 1rem;
background-color: #fff;
border-radius: 0.25rem;
box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.2);
}
.form__title {
font-size: 1.5rem;
margin: 0 0 1rem;
}
.form__group {
margin-bottom: 1rem;
}
.form__label {
display: block;
margin-bottom: 0.25rem;
font-size: 1rem;
}
.form__input {
display: block;
width: 100%;
padding: 0.5rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
background-color: #fff;
transition: border-color 0.2s ease-in-out;
}
.form__input:focus {
outline: none;
border-color: #007bff;
}
.form__group--inline {
display: flex;
align-items: center;
}
.form__group--2col {
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 1rem;
}
.form__button {
display: block;
width: 100%;
padding: 0.5rem;
font-size: 1rem;
font-weight: 600;
text-align: center;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 0.25rem;
transition: background-color 0.2s ease-in-out;
cursor: pointer;
}
.form__button:focus,
.form__button:active,
.form__button:hover {
outline: none;
background-color: #0069d9;
}
.form__button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.form__button--inline {
margin-left: 0.5rem;
}
.form__error {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #dc3545;
}
</style>