hookehuyr

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

新增手机号修改页面及路由配置,包含表单验证和短信验证码功能
更新登录页面的logo和样式
在设置页面添加手机号修改入口
/*
* @Date: 2025-03-20 20:36:36
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-05-08 12:06:46
* @LastEditTime: 2025-06-13 11:24:11
* @FilePath: /mlaj/src/router/routes.js
* @Description: 路由地址映射配置
*/
......@@ -157,6 +157,12 @@ export const routes = [
meta: { title: '修改用户名' },
},
{
path: '/profile/settings/phone',
name: 'PhoneSetting',
component: () => import('../views/profile/settings/PhoneSettingPage.vue'),
meta: { title: '修改手机号' },
},
{
path: '/profile/settings/password',
name: 'PasswordSetting',
component: () => import('../views/profile/settings/PasswordSettingPage.vue'),
......
......@@ -3,9 +3,9 @@
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"
>
<div class="sm:mx-auto sm:w-full sm:max-w-md text-center">
<van-icon name="https://cdn.ipadbiz.cn/mlaj/icon/new-logo.png" size="5rem" style="margin-bottom: 0.5rem;" />
<van-icon name="https://cdn.ipadbiz.cn/mlaj/icon/behalo-logo-1.png" size="10rem" style="margin-bottom: 0.5rem;" />
<h1 class="text-center text-3xl font-bold text-gray-800 mb-2">美乐爱觉教育</h1>
<h2 class="text-center text-xl font-medium text-gray-600">欢迎回来</h2>
<!-- <h2 class="text-center text-xl font-medium text-gray-600">欢迎回来</h2> -->
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
......
<!--
* @Date: 2025-03-24 13:04:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-09 11:36:07
* @LastEditTime: 2025-06-13 11:23:19
* @FilePath: /mlaj/src/views/profile/SettingsPage.vue
* @Description: 用户设置页面
-->
......@@ -86,6 +86,17 @@
<ChevronRightIcon class="w-5 h-5 text-gray-400" />
</div>
</div> -->
<!-- 修改手机号 -->
<div class="p-4" @click="router.push('/profile/settings/phone')">
<div class="flex items-center justify-between">
<div>
<h3 class="text-base font-medium text-gray-900">修改手机号</h3>
<p class="text-sm text-gray-500">修改手机号</p>
</div>
<ChevronRightIcon class="w-5 h-5 text-gray-400" />
</div>
</div>
</div>
</FrostedGlass>
</div>
......
<!--
* @Date: 2025-06-13 11:23:38
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-13 11:26:53
* @FilePath: /mlaj/src/views/profile/settings/PhoneSettingPage.vue
* @Description: 手机号修改页面
-->
<template>
<AppLayout title="">
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
<div class="px-4 py-6">
<FrostedGlass class="rounded-xl overflow-hidden">
<div class="p-4">
<div class="space-y-4">
<!-- 当前手机号显示 -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">当前手机号</label>
<div class="w-full px-4 py-3 bg-gray-100 border border-gray-300 rounded-lg text-gray-600">
{{ currentPhone || '未设置' }}
</div>
</div>
<!-- 新手机号输入 -->
<div class="mb-4">
<label for="phone" class="block text-sm font-medium text-gray-700 mb-2">
新手机号 <span class="text-red-500">*</span>
</label>
<input
id="phone"
v-model="formData.phone"
autocomplete="tel"
type="tel"
required
pattern="^1[3-9]\d{9}$"
maxlength="11"
placeholder="请输入新的手机号"
@input="formData.phone = formData.phone.replace(/\D/g, '')"
@blur="validatePhone"
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"
/>
<div v-if="phoneError" class="text-red-500 text-sm mt-1">{{ phoneError }}</div>
</div>
<!-- 验证码输入 -->
<div class="mb-6">
<label for="verificationCode" class="block text-sm font-medium text-gray-700 mb-2">
验证码 <span class="text-red-500">*</span>
</label>
<div class="flex space-x-2">
<input
id="verificationCode"
v-model="formData.verificationCode"
type="text"
required
maxlength="6"
placeholder="请输入验证码"
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"
/>
<button
type="button"
:disabled="countdown > 0 || !isPhoneValid"
@click="sendVerificationCode"
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"
>
{{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }}
</button>
</div>
</div>
<!-- 保存按钮 -->
<van-button
@click="handlePhoneChange"
type="primary"
block
round
:loading="loading"
>
保存修改
</van-button>
</div>
</div>
</FrostedGlass>
</div>
</div>
</AppLayout>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import AppLayout from '@/components/layout/AppLayout.vue';
import FrostedGlass from '@/components/ui/FrostedGlass.vue';
import { getUserInfoAPI, updateUserInfoAPI } from '@/api/users';
import { smsAPI } from '@/api/common';
import { showToast } from 'vant';
import { useTitle } from '@vueuse/core';
import { useAuth } from '@/contexts/auth';
const $route = useRoute();
useTitle($route.meta.title);
// 获取用户认证状态
const { currentUser } = useAuth();
// 表单数据
const formData = ref({
phone: '',
verificationCode: ''
});
// 当前手机号
const currentPhone = ref('');
// 状态管理
const loading = ref(false);
const countdown = ref(0);
const phoneError = ref('');
// 手机号验证
const isPhoneValid = computed(() => {
const phoneRegex = /^1[3-9]\d{9}$/;
return phoneRegex.test(formData.value.phone);
});
/**
* 验证手机号格式
*/
const validatePhone = () => {
if (!formData.value.phone) {
phoneError.value = '请输入手机号';
return false;
}
if (!isPhoneValid.value) {
phoneError.value = '请输入正确的手机号格式';
return false;
}
if (formData.value.phone === currentPhone.value) {
phoneError.value = '新手机号不能与当前手机号相同';
return false;
}
phoneError.value = '';
return true;
};
/**
* 发送验证码
*/
const sendVerificationCode = async () => {
if (!validatePhone()) {
return;
}
try {
// 调用发送验证码API
const { code } = await smsAPI({ mobile: formData.value.phone });
if (code) {
showToast('验证码已发送');
// 开始倒计时
countdown.value = 60;
const timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
}
}, 1000);
}
} catch (error) {
console.error('发送验证码失败:', error);
showToast('发送验证码失败,请稍后重试');
}
};
/**
* 处理手机号修改
*/
const handlePhoneChange = async () => {
if (!validatePhone()) {
return;
}
if (!formData.value.verificationCode) {
showToast('请输入验证码');
return;
}
if (formData.value.verificationCode.length !== 6) {
showToast('请输入6位验证码');
return;
}
loading.value = true;
try {
const { code, data } = await updateUserInfoAPI({
mobile: formData.value.phone,
sms_code: formData.value.verificationCode
});
if (code) {
// 更新auth上下文中的用户信息
currentUser.value = {
...currentUser.value,
mobile: formData.value.phone
};
// 更新localStorage中的用户信息
localStorage.setItem('currentUser', JSON.stringify(currentUser.value));
// 更新当前显示的手机号
currentPhone.value = formData.value.phone;
// 清空表单
formData.value.phone = '';
formData.value.verificationCode = '';
showToast('手机号修改成功');
}
} catch (error) {
console.error('手机号修改失败:', error);
showToast('手机号修改失败,请重试');
} finally {
loading.value = false;
}
};
// 获取用户信息
onMounted(async () => {
try {
const response = await getUserInfoAPI();
if (response.data) {
currentPhone.value = response.data.user.mobile || '';
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
});
</script>
<style scoped>
/* 自定义样式 */
.disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>