feat(登录页): 添加验证码登录功能
在登录页面中添加验证码登录选项,用户可以选择使用验证码或密码进行登录。新增验证码输入框、发送验证码按钮以及相关逻辑,包括手机号验证、验证码发送和倒计时功能
Showing
1 changed file
with
92 additions
and
5 deletions
| ... | @@ -33,11 +33,12 @@ | ... | @@ -33,11 +33,12 @@ |
| 33 | placeholder="请输入手机号" | 33 | placeholder="请输入手机号" |
| 34 | @input="mobile = $event.target.value.replace(/\D/g, '')" | 34 | @input="mobile = $event.target.value.replace(/\D/g, '')" |
| 35 | @focus="handleInputFocus" | 35 | @focus="handleInputFocus" |
| 36 | + @blur="validatePhone" | ||
| 36 | 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" | 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" |
| 37 | /> | 38 | /> |
| 38 | </div> | 39 | </div> |
| 39 | 40 | ||
| 40 | - <div> | 41 | + <div v-if="!isVerifyCodeLogin"> |
| 41 | <label for="password" class="block text-sm font-medium text-gray-700"> | 42 | <label for="password" class="block text-sm font-medium text-gray-700"> |
| 42 | 密码 <span class="text-red-500">*</span> | 43 | 密码 <span class="text-red-500">*</span> |
| 43 | </label> | 44 | </label> |
| ... | @@ -53,7 +54,32 @@ | ... | @@ -53,7 +54,32 @@ |
| 53 | /> | 54 | /> |
| 54 | </div> | 55 | </div> |
| 55 | 56 | ||
| 56 | - <div class="flex justify-end"> | 57 | + <div v-else> |
| 58 | + <label for="verificationCode" class="block text-sm font-medium text-gray-700"> | ||
| 59 | + 验证码 <span class="text-red-500">*</span> | ||
| 60 | + </label> | ||
| 61 | + <div class="flex space-x-2"> | ||
| 62 | + <input | ||
| 63 | + id="verificationCode" | ||
| 64 | + v-model="verificationCode" | ||
| 65 | + type="text" | ||
| 66 | + required | ||
| 67 | + maxlength="6" | ||
| 68 | + placeholder="请输入验证码" | ||
| 69 | + 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" | ||
| 70 | + /> | ||
| 71 | + <button | ||
| 72 | + type="button" | ||
| 73 | + :disabled="countdown > 0 || !isPhoneValid" | ||
| 74 | + @click="sendVerificationCode" | ||
| 75 | + 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" | ||
| 76 | + > | ||
| 77 | + {{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }} | ||
| 78 | + </button> | ||
| 79 | + </div> | ||
| 80 | + </div> | ||
| 81 | + | ||
| 82 | + <div class="flex justify-end space-x-4"> | ||
| 57 | <div class="text-sm"> | 83 | <div class="text-sm"> |
| 58 | <router-link | 84 | <router-link |
| 59 | to="/forgotPwd" | 85 | to="/forgotPwd" |
| ... | @@ -62,6 +88,15 @@ | ... | @@ -62,6 +88,15 @@ |
| 62 | 忘记密码? | 88 | 忘记密码? |
| 63 | </router-link> | 89 | </router-link> |
| 64 | </div> | 90 | </div> |
| 91 | + <div class="text-sm"> | ||
| 92 | + <button | ||
| 93 | + type="button" | ||
| 94 | + class="font-medium text-green-600 hover:text-green-500" | ||
| 95 | + @click="isVerifyCodeLogin = !isVerifyCodeLogin" | ||
| 96 | + > | ||
| 97 | + {{ isVerifyCodeLogin ? '密码登录' : '验证码登录' }} | ||
| 98 | + </button> | ||
| 99 | + </div> | ||
| 65 | </div> | 100 | </div> |
| 66 | 101 | ||
| 67 | <div> | 102 | <div> |
| ... | @@ -133,6 +168,8 @@ import FrostedGlass from "@/components/ui/FrostedGlass.vue"; | ... | @@ -133,6 +168,8 @@ import FrostedGlass from "@/components/ui/FrostedGlass.vue"; |
| 133 | import { useAuth } from "@/contexts/auth"; | 168 | import { useAuth } from "@/contexts/auth"; |
| 134 | import { loginAPI, getUserInfoAPI } from "@/api/users"; | 169 | import { loginAPI, getUserInfoAPI } from "@/api/users"; |
| 135 | import { useTitle } from "@vueuse/core"; | 170 | import { useTitle } from "@vueuse/core"; |
| 171 | +import { smsAPI } from "@/api/common"; | ||
| 172 | +import { showToast } from "vant"; | ||
| 136 | 173 | ||
| 137 | const handleInputFocus = () => { | 174 | const handleInputFocus = () => { |
| 138 | setTimeout(() => { | 175 | setTimeout(() => { |
| ... | @@ -152,12 +189,60 @@ const { login } = useAuth(); | ... | @@ -152,12 +189,60 @@ const { login } = useAuth(); |
| 152 | 189 | ||
| 153 | const mobile = ref(""); | 190 | const mobile = ref(""); |
| 154 | const password = ref(""); | 191 | const password = ref(""); |
| 192 | +const verificationCode = ref(""); | ||
| 155 | const error = ref(""); | 193 | const error = ref(""); |
| 156 | const loading = ref(false); | 194 | const loading = ref(false); |
| 195 | +const isVerifyCodeLogin = ref(false); | ||
| 196 | +const countdown = ref(0); | ||
| 197 | +const isPhoneValid = ref(false); | ||
| 198 | + | ||
| 199 | +const validatePhone = () => { | ||
| 200 | + if (!mobile.value) { | ||
| 201 | + error.value = '请输入手机号'; | ||
| 202 | + isPhoneValid.value = false; | ||
| 203 | + return; | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + if (!/^1[3-9]\d{9}$/.test(mobile.value)) { | ||
| 207 | + error.value = '请输入正确的手机号'; | ||
| 208 | + isPhoneValid.value = false; | ||
| 209 | + return; | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + error.value = ''; | ||
| 213 | + isPhoneValid.value = true; | ||
| 214 | +}; | ||
| 215 | + | ||
| 216 | +const startCountdown = () => { | ||
| 217 | + countdown.value = 60; | ||
| 218 | + const timer = setInterval(() => { | ||
| 219 | + countdown.value--; | ||
| 220 | + if (countdown.value <= 0) { | ||
| 221 | + clearInterval(timer); | ||
| 222 | + } | ||
| 223 | + }, 1000); | ||
| 224 | +}; | ||
| 225 | + | ||
| 226 | +const sendVerificationCode = async () => { | ||
| 227 | + if (!isPhoneValid.value) { | ||
| 228 | + return; | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + try { | ||
| 232 | + const { code } = await smsAPI({ mobile: mobile.value }); | ||
| 233 | + if (code) { | ||
| 234 | + showToast('验证码已发送'); | ||
| 235 | + startCountdown(); | ||
| 236 | + return; | ||
| 237 | + } | ||
| 238 | + } catch (err) { | ||
| 239 | + console.error('Send verification code error:', err); | ||
| 240 | + error.value = '发送验证码失败,请稍后重试'; | ||
| 241 | + } | ||
| 242 | +}; | ||
| 157 | 243 | ||
| 158 | -// 原登录逻辑 | ||
| 159 | const handleSubmit = async () => { | 244 | const handleSubmit = async () => { |
| 160 | - if (!mobile.value || !password.value) { | 245 | + if (!mobile.value || (!isVerifyCodeLogin.value && !password.value) || (isVerifyCodeLogin.value && !verificationCode.value)) { |
| 161 | error.value = "请填写所有字段"; | 246 | error.value = "请填写所有字段"; |
| 162 | return; | 247 | return; |
| 163 | } | 248 | } |
| ... | @@ -169,7 +254,9 @@ const handleSubmit = async () => { | ... | @@ -169,7 +254,9 @@ const handleSubmit = async () => { |
| 169 | // 调用登录接口 | 254 | // 调用登录接口 |
| 170 | const response = await loginAPI({ | 255 | const response = await loginAPI({ |
| 171 | mobile: mobile.value, | 256 | mobile: mobile.value, |
| 172 | - password: password.value, | 257 | + ...(isVerifyCodeLogin.value |
| 258 | + ? { sms_code: verificationCode.value } | ||
| 259 | + : { password: password.value }), | ||
| 173 | }); | 260 | }); |
| 174 | 261 | ||
| 175 | if (response.code !== 1) { | 262 | if (response.code !== 1) { | ... | ... |
-
Please register or login to post a comment