hookehuyr

feat(profile): 添加手机号修改功能

新增手机号修改页面及路由配置,包含表单验证和短信验证码功能
更新登录页面的logo和样式
在设置页面添加手机号修改入口
1 /* 1 /*
2 * @Date: 2025-03-20 20:36:36 2 * @Date: 2025-03-20 20:36:36
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-05-08 12:06:46 4 + * @LastEditTime: 2025-06-13 11:24:11
5 * @FilePath: /mlaj/src/router/routes.js 5 * @FilePath: /mlaj/src/router/routes.js
6 * @Description: 路由地址映射配置 6 * @Description: 路由地址映射配置
7 */ 7 */
...@@ -157,6 +157,12 @@ export const routes = [ ...@@ -157,6 +157,12 @@ export const routes = [
157 meta: { title: '修改用户名' }, 157 meta: { title: '修改用户名' },
158 }, 158 },
159 { 159 {
160 + path: '/profile/settings/phone',
161 + name: 'PhoneSetting',
162 + component: () => import('../views/profile/settings/PhoneSettingPage.vue'),
163 + meta: { title: '修改手机号' },
164 + },
165 + {
160 path: '/profile/settings/password', 166 path: '/profile/settings/password',
161 name: 'PasswordSetting', 167 name: 'PasswordSetting',
162 component: () => import('../views/profile/settings/PasswordSettingPage.vue'), 168 component: () => import('../views/profile/settings/PasswordSettingPage.vue'),
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
3 class="min-h-screen flex flex-col bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 py-8 px-4 sm:px-6 lg:px-8" 3 class="min-h-screen flex flex-col bg-gradient-to-br from-green-50 via-teal-50 to-blue-50 py-8 px-4 sm:px-6 lg:px-8"
4 > 4 >
5 <div class="sm:mx-auto sm:w-full sm:max-w-md text-center"> 5 <div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
6 - <van-icon name="https://cdn.ipadbiz.cn/mlaj/icon/new-logo.png" size="5rem" style="margin-bottom: 0.5rem;" /> 6 + <van-icon name="https://cdn.ipadbiz.cn/mlaj/icon/behalo-logo-1.png" size="10rem" style="margin-bottom: 0.5rem;" />
7 <h1 class="text-center text-3xl font-bold text-gray-800 mb-2">美乐爱觉教育</h1> 7 <h1 class="text-center text-3xl font-bold text-gray-800 mb-2">美乐爱觉教育</h1>
8 - <h2 class="text-center text-xl font-medium text-gray-600">欢迎回来</h2> 8 + <!-- <h2 class="text-center text-xl font-medium text-gray-600">欢迎回来</h2> -->
9 </div> 9 </div>
10 10
11 <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> 11 <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
......
1 <!-- 1 <!--
2 * @Date: 2025-03-24 13:04:21 2 * @Date: 2025-03-24 13:04:21
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-06-09 11:36:07 4 + * @LastEditTime: 2025-06-13 11:23:19
5 * @FilePath: /mlaj/src/views/profile/SettingsPage.vue 5 * @FilePath: /mlaj/src/views/profile/SettingsPage.vue
6 * @Description: 用户设置页面 6 * @Description: 用户设置页面
7 --> 7 -->
...@@ -86,6 +86,17 @@ ...@@ -86,6 +86,17 @@
86 <ChevronRightIcon class="w-5 h-5 text-gray-400" /> 86 <ChevronRightIcon class="w-5 h-5 text-gray-400" />
87 </div> 87 </div>
88 </div> --> 88 </div> -->
89 +
90 + <!-- 修改手机号 -->
91 + <div class="p-4" @click="router.push('/profile/settings/phone')">
92 + <div class="flex items-center justify-between">
93 + <div>
94 + <h3 class="text-base font-medium text-gray-900">修改手机号</h3>
95 + <p class="text-sm text-gray-500">修改手机号</p>
96 + </div>
97 + <ChevronRightIcon class="w-5 h-5 text-gray-400" />
98 + </div>
99 + </div>
89 </div> 100 </div>
90 </FrostedGlass> 101 </FrostedGlass>
91 </div> 102 </div>
......
1 +<!--
2 + * @Date: 2025-06-13 11:23:38
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-06-13 11:26:53
5 + * @FilePath: /mlaj/src/views/profile/settings/PhoneSettingPage.vue
6 + * @Description: 手机号修改页面
7 +-->
8 +<template>
9 + <AppLayout title="">
10 + <div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
11 + <div class="px-4 py-6">
12 + <FrostedGlass class="rounded-xl overflow-hidden">
13 + <div class="p-4">
14 + <div class="space-y-4">
15 + <!-- 当前手机号显示 -->
16 + <div class="mb-6">
17 + <label class="block text-sm font-medium text-gray-700 mb-2">当前手机号</label>
18 + <div class="w-full px-4 py-3 bg-gray-100 border border-gray-300 rounded-lg text-gray-600">
19 + {{ currentPhone || '未设置' }}
20 + </div>
21 + </div>
22 +
23 + <!-- 新手机号输入 -->
24 + <div class="mb-4">
25 + <label for="phone" class="block text-sm font-medium text-gray-700 mb-2">
26 + 新手机号 <span class="text-red-500">*</span>
27 + </label>
28 + <input
29 + id="phone"
30 + v-model="formData.phone"
31 + autocomplete="tel"
32 + type="tel"
33 + required
34 + pattern="^1[3-9]\d{9}$"
35 + maxlength="11"
36 + placeholder="请输入新的手机号"
37 + @input="formData.phone = formData.phone.replace(/\D/g, '')"
38 + @blur="validatePhone"
39 + class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
40 + />
41 + <div v-if="phoneError" class="text-red-500 text-sm mt-1">{{ phoneError }}</div>
42 + </div>
43 +
44 + <!-- 验证码输入 -->
45 + <div class="mb-6">
46 + <label for="verificationCode" class="block text-sm font-medium text-gray-700 mb-2">
47 + 验证码 <span class="text-red-500">*</span>
48 + </label>
49 + <div class="flex space-x-2">
50 + <input
51 + id="verificationCode"
52 + v-model="formData.verificationCode"
53 + type="text"
54 + required
55 + maxlength="6"
56 + placeholder="请输入验证码"
57 + class="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
58 + />
59 + <button
60 + type="button"
61 + :disabled="countdown > 0 || !isPhoneValid"
62 + @click="sendVerificationCode"
63 + class="px-4 py-3 border border-transparent text-sm font-medium rounded-lg 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>
68 + </div>
69 +
70 + <!-- 保存按钮 -->
71 + <van-button
72 + @click="handlePhoneChange"
73 + type="primary"
74 + block
75 + round
76 + :loading="loading"
77 + >
78 + 保存修改
79 + </van-button>
80 + </div>
81 + </div>
82 + </FrostedGlass>
83 + </div>
84 + </div>
85 + </AppLayout>
86 +</template>
87 +
88 +<script setup>
89 +import { ref, onMounted, computed } from 'vue';
90 +import { useRoute } from 'vue-router';
91 +import AppLayout from '@/components/layout/AppLayout.vue';
92 +import FrostedGlass from '@/components/ui/FrostedGlass.vue';
93 +import { getUserInfoAPI, updateUserInfoAPI } from '@/api/users';
94 +import { smsAPI } from '@/api/common';
95 +import { showToast } from 'vant';
96 +import { useTitle } from '@vueuse/core';
97 +import { useAuth } from '@/contexts/auth';
98 +
99 +const $route = useRoute();
100 +useTitle($route.meta.title);
101 +
102 +// 获取用户认证状态
103 +const { currentUser } = useAuth();
104 +
105 +// 表单数据
106 +const formData = ref({
107 + phone: '',
108 + verificationCode: ''
109 +});
110 +
111 +// 当前手机号
112 +const currentPhone = ref('');
113 +
114 +// 状态管理
115 +const loading = ref(false);
116 +const countdown = ref(0);
117 +const phoneError = ref('');
118 +
119 +// 手机号验证
120 +const isPhoneValid = computed(() => {
121 + const phoneRegex = /^1[3-9]\d{9}$/;
122 + return phoneRegex.test(formData.value.phone);
123 +});
124 +
125 +/**
126 + * 验证手机号格式
127 + */
128 +const validatePhone = () => {
129 + if (!formData.value.phone) {
130 + phoneError.value = '请输入手机号';
131 + return false;
132 + }
133 + if (!isPhoneValid.value) {
134 + phoneError.value = '请输入正确的手机号格式';
135 + return false;
136 + }
137 + if (formData.value.phone === currentPhone.value) {
138 + phoneError.value = '新手机号不能与当前手机号相同';
139 + return false;
140 + }
141 + phoneError.value = '';
142 + return true;
143 +};
144 +
145 +/**
146 + * 发送验证码
147 + */
148 +const sendVerificationCode = async () => {
149 + if (!validatePhone()) {
150 + return;
151 + }
152 +
153 + try {
154 + // 调用发送验证码API
155 + const { code } = await smsAPI({ mobile: formData.value.phone });
156 + if (code) {
157 + showToast('验证码已发送');
158 + // 开始倒计时
159 + countdown.value = 60;
160 + const timer = setInterval(() => {
161 + countdown.value--;
162 + if (countdown.value <= 0) {
163 + clearInterval(timer);
164 + }
165 + }, 1000);
166 + }
167 + } catch (error) {
168 + console.error('发送验证码失败:', error);
169 + showToast('发送验证码失败,请稍后重试');
170 + }
171 +};
172 +
173 +/**
174 + * 处理手机号修改
175 + */
176 +const handlePhoneChange = async () => {
177 + if (!validatePhone()) {
178 + return;
179 + }
180 +
181 + if (!formData.value.verificationCode) {
182 + showToast('请输入验证码');
183 + return;
184 + }
185 +
186 + if (formData.value.verificationCode.length !== 6) {
187 + showToast('请输入6位验证码');
188 + return;
189 + }
190 +
191 + loading.value = true;
192 +
193 + try {
194 + const { code, data } = await updateUserInfoAPI({
195 + mobile: formData.value.phone,
196 + sms_code: formData.value.verificationCode
197 + });
198 +
199 + if (code) {
200 + // 更新auth上下文中的用户信息
201 + currentUser.value = {
202 + ...currentUser.value,
203 + mobile: formData.value.phone
204 + };
205 + // 更新localStorage中的用户信息
206 + localStorage.setItem('currentUser', JSON.stringify(currentUser.value));
207 +
208 + // 更新当前显示的手机号
209 + currentPhone.value = formData.value.phone;
210 +
211 + // 清空表单
212 + formData.value.phone = '';
213 + formData.value.verificationCode = '';
214 +
215 + showToast('手机号修改成功');
216 + }
217 + } catch (error) {
218 + console.error('手机号修改失败:', error);
219 + showToast('手机号修改失败,请重试');
220 + } finally {
221 + loading.value = false;
222 + }
223 +};
224 +
225 +// 获取用户信息
226 +onMounted(async () => {
227 + try {
228 + const response = await getUserInfoAPI();
229 + if (response.data) {
230 + currentPhone.value = response.data.user.mobile || '';
231 + }
232 + } catch (error) {
233 + console.error('获取用户信息失败:', error);
234 + }
235 +});
236 +</script>
237 +
238 +<style scoped>
239 +/* 自定义样式 */
240 +.disabled {
241 + opacity: 0.5;
242 + cursor: not-allowed;
243 +}
244 +</style>