feat(注册页面): 添加手机号验证和验证码功能
增加手机号输入字段的验证逻辑,确保输入格式正确。添加验证码输入字段和获取验证码按钮,并实现倒计时功能。优化表单提交逻辑,确保所有必填字段都已填写且手机号格式正确。
Showing
1 changed file
with
79 additions
and
17 deletions
| ... | @@ -27,15 +27,44 @@ | ... | @@ -27,15 +27,44 @@ |
| 27 | 27 | ||
| 28 | <div> | 28 | <div> |
| 29 | <label for="phone" class="block text-sm font-medium text-gray-700"> | 29 | <label for="phone" class="block text-sm font-medium text-gray-700"> |
| 30 | - 手机号 | 30 | + 手机号 <span class="text-red-500">*</span> |
| 31 | </label> | 31 | </label> |
| 32 | <input | 32 | <input |
| 33 | id="phone" | 33 | id="phone" |
| 34 | v-model="formData.phone" | 34 | v-model="formData.phone" |
| 35 | autocomplete="tel" | 35 | autocomplete="tel" |
| 36 | type="tel" | 36 | type="tel" |
| 37 | + required | ||
| 38 | + pattern="^1[3-9]\d{9}$" | ||
| 39 | + @blur="validatePhone" | ||
| 37 | class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500" | 40 | class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500" |
| 41 | + :class="{ 'border-red-300': phoneError }" | ||
| 38 | /> | 42 | /> |
| 43 | + <p v-if="phoneError" class="mt-1 text-sm text-red-600">{{ phoneError }}</p> | ||
| 44 | + </div> | ||
| 45 | + | ||
| 46 | + <div> | ||
| 47 | + <label for="verificationCode" class="block text-sm font-medium text-gray-700"> | ||
| 48 | + 验证码 <span class="text-red-500">*</span> | ||
| 49 | + </label> | ||
| 50 | + <div class="flex space-x-2"> | ||
| 51 | + <input | ||
| 52 | + id="verificationCode" | ||
| 53 | + v-model="formData.verificationCode" | ||
| 54 | + type="text" | ||
| 55 | + required | ||
| 56 | + maxlength="6" | ||
| 57 | + class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-green-500 focus:border-green-500" | ||
| 58 | + /> | ||
| 59 | + <button | ||
| 60 | + type="button" | ||
| 61 | + :disabled="countdown > 0 || !isPhoneValid" | ||
| 62 | + @click="sendVerificationCode" | ||
| 63 | + class="mt-1 px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" | ||
| 64 | + > | ||
| 65 | + {{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }} | ||
| 66 | + </button> | ||
| 67 | + </div> | ||
| 39 | </div> | 68 | </div> |
| 40 | 69 | ||
| 41 | <div> | 70 | <div> |
| ... | @@ -161,39 +190,72 @@ useTitle($route.meta.title); | ... | @@ -161,39 +190,72 @@ useTitle($route.meta.title); |
| 161 | const router = useRouter() | 190 | const router = useRouter() |
| 162 | const { login } = useAuth() | 191 | const { login } = useAuth() |
| 163 | 192 | ||
| 193 | +const countdown = ref(0) | ||
| 194 | + | ||
| 164 | const formData = reactive({ | 195 | const formData = reactive({ |
| 165 | name: '', | 196 | name: '', |
| 166 | phone: '', | 197 | phone: '', |
| 198 | + verificationCode: '', | ||
| 167 | password: '', | 199 | password: '', |
| 168 | confirmPassword: '', | 200 | confirmPassword: '', |
| 169 | agreeTerms: false | 201 | agreeTerms: false |
| 170 | }) | 202 | }) |
| 171 | 203 | ||
| 172 | -const error = ref('') | 204 | +const startCountdown = () => { |
| 173 | -const loading = ref(false) | 205 | + countdown.value = 60 |
| 174 | -const showTerms = ref(false) | 206 | + const timer = setInterval(() => { |
| 175 | -const showPrivacy = ref(false) | 207 | + countdown.value-- |
| 176 | -const popupTitle = ref('') | 208 | + if (countdown.value <= 0) { |
| 177 | -const popupType = ref('') | 209 | + clearInterval(timer) |
| 178 | - | 210 | + } |
| 179 | -const openTerms = () => { | 211 | + }, 1000) |
| 180 | - popupTitle.value = '用户协议' | 212 | +} |
| 181 | - popupType.value = 'terms' | 213 | + |
| 182 | - showTerms.value = true | 214 | +const phoneError = ref('') |
| 215 | +const isPhoneValid = ref(false) | ||
| 216 | + | ||
| 217 | +const validatePhone = () => { | ||
| 218 | + if (!formData.phone) { | ||
| 219 | + phoneError.value = '请输入手机号' | ||
| 220 | + isPhoneValid.value = false | ||
| 221 | + return | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + if (!/^1[3-9]\d{9}$/.test(formData.phone)) { | ||
| 225 | + phoneError.value = '请输入正确的手机号' | ||
| 226 | + isPhoneValid.value = false | ||
| 227 | + return | ||
| 228 | + } | ||
| 229 | + | ||
| 230 | + phoneError.value = '' | ||
| 231 | + isPhoneValid.value = true | ||
| 183 | } | 232 | } |
| 184 | 233 | ||
| 185 | -const openPrivacy = () => { | 234 | +const sendVerificationCode = async () => { |
| 186 | - popupTitle.value = '隐私政策' | 235 | + if (!isPhoneValid.value) { |
| 187 | - popupType.value = 'privacy' | 236 | + return |
| 188 | - showPrivacy.value = true | 237 | + } |
| 238 | + | ||
| 239 | + try { | ||
| 240 | + // TODO: 调用发送验证码API | ||
| 241 | + startCountdown() | ||
| 242 | + } catch (err) { | ||
| 243 | + console.error('Send verification code error:', err) | ||
| 244 | + error.value = '发送验证码失败,请稍后重试' | ||
| 245 | + } | ||
| 189 | } | 246 | } |
| 190 | 247 | ||
| 191 | const handleSubmit = async () => { | 248 | const handleSubmit = async () => { |
| 192 | - if (!formData.name || !formData.password) { | 249 | + if (!formData.name || !formData.password || !formData.phone || !formData.verificationCode) { |
| 193 | error.value = '请填写所有必填字段' | 250 | error.value = '请填写所有必填字段' |
| 194 | return | 251 | return |
| 195 | } | 252 | } |
| 196 | 253 | ||
| 254 | + if (!/^1[3-9]\d{9}$/.test(formData.phone)) { | ||
| 255 | + error.value = '请输入正确的手机号' | ||
| 256 | + return | ||
| 257 | + } | ||
| 258 | + | ||
| 197 | if (formData.password !== formData.confirmPassword) { | 259 | if (formData.password !== formData.confirmPassword) { |
| 198 | error.value = '两次输入的密码不一致' | 260 | error.value = '两次输入的密码不一致' |
| 199 | return | 261 | return | ... | ... |
-
Please register or login to post a comment