hookehuyr

feat(登录页): 添加验证码登录功能

在登录页面中添加验证码登录选项,用户可以选择使用验证码或密码进行登录。新增验证码输入框、发送验证码按钮以及相关逻辑,包括手机号验证、验证码发送和倒计时功能
...@@ -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) {
......