feat(用户资料): 完善用户资料页面功能

- 在用户类型定义中新增userPhone、unionId和mpOpenId字段
- 重构验证码输入组件,使用a-input-group替代a-input-search
- 实现用户资料更新、密码修改和邮箱绑定功能
- 添加验证码发送倒计时功能
- 优化表单验证和错误处理
- 清理定时器防止内存泄漏
This commit is contained in:
2026-01-18 17:29:03 +08:00
parent 783ea21d55
commit edcc8611e5
2 changed files with 195 additions and 49 deletions

View File

@@ -7,6 +7,9 @@ export interface LoginUesr {
userRole?: string; userRole?: string;
userProfile?: string; userProfile?: string;
userEmail?: string; userEmail?: string;
userPhone?: string;
unionId?: string;
mpOpenId?: string;
createTime?: string; createTime?: string;
updateTime?: string; updateTime?: string;
[key: string]: any; [key: string]: any;

View File

@@ -152,9 +152,9 @@
<a-modal <a-modal
v-model:visible="showPasswordModal" v-model:visible="showPasswordModal"
title="修改密码" title="修改密码"
@ok="handlePasswordSubmit" @before-ok="handlePasswordSubmit"
@cancel="handlePasswordReset" @cancel="handlePasswordReset"
:confirm-loading="passwordLoading" :ok-loading="passwordLoading"
> >
<a-form :model="passwordForm" layout="vertical"> <a-form :model="passwordForm" layout="vertical">
<a-form-item label="当前密码" field="oldPassword"> <a-form-item label="当前密码" field="oldPassword">
@@ -185,9 +185,9 @@
<a-modal <a-modal
v-model:visible="showPhoneModal" v-model:visible="showPhoneModal"
:title="loginUser.userPhone ? '更换手机号' : '绑定手机号'" :title="loginUser.userPhone ? '更换手机号' : '绑定手机号'"
@ok="handlePhoneSubmit" @before-ok="handlePhoneSubmit"
@cancel="handlePhoneReset" @cancel="handlePhoneReset"
:confirm-loading="phoneLoading" :ok-loading="phoneLoading"
> >
<a-form :model="phoneForm" layout="vertical"> <a-form :model="phoneForm" layout="vertical">
<a-form-item label="手机号" field="phone"> <a-form-item label="手机号" field="phone">
@@ -198,13 +198,21 @@
/> />
</a-form-item> </a-form-item>
<a-form-item label="验证码" field="code"> <a-form-item label="验证码" field="code">
<a-input-search <a-input-group style="width: 100%">
v-model="phoneForm.code" <a-input
placeholder="请输入验证码" v-model="phoneForm.code"
button-text="发送验证码" placeholder="请输入验证码"
@search="handleSendPhoneCode" :max-length="6"
:loading="codeSending" />
/> <a-button
type="outline"
:loading="codeSending"
:disabled="phoneCountdown > 0 || !phoneForm.phone"
@click="handleSendPhoneCode"
>
{{ phoneCountdown > 0 ? `${phoneCountdown}秒后重试` : '发送验证码' }}
</a-button>
</a-input-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
@@ -213,9 +221,9 @@
<a-modal <a-modal
v-model:visible="showEmailModal" v-model:visible="showEmailModal"
:title="loginUser.userEmail ? '更换邮箱' : '绑定邮箱'" :title="loginUser.userEmail ? '更换邮箱' : '绑定邮箱'"
@ok="handleEmailSubmit" @before-ok="handleEmailSubmit"
@cancel="handleEmailReset" @cancel="handleEmailReset"
:confirm-loading="emailLoading" :ok-loading="emailLoading"
> >
<a-form :model="emailForm" layout="vertical"> <a-form :model="emailForm" layout="vertical">
<a-form-item label="邮箱地址" field="email"> <a-form-item label="邮箱地址" field="email">
@@ -226,13 +234,21 @@
/> />
</a-form-item> </a-form-item>
<a-form-item label="验证码" field="code"> <a-form-item label="验证码" field="code">
<a-input-search <a-input-group style="width: 100%">
v-model="emailForm.code" <a-input
placeholder="请输入验证码" v-model="emailForm.code"
button-text="发送验证码" placeholder="请输入验证码"
@search="handleSendEmailCode" :max-length="6"
:loading="codeSending" />
/> <a-button
type="outline"
:loading="codeSending"
:disabled="emailCountdown > 0 || !emailForm.email"
@click="handleSendEmailCode"
>
{{ emailCountdown > 0 ? `${emailCountdown}秒后重试` : '发送验证码' }}
</a-button>
</a-input-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
@@ -240,10 +256,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch, reactive } from 'vue'; import { ref, computed, onMounted, onUnmounted, watch, reactive } from 'vue';
import { useUserStore } from '@/store/user'; import { useUserStore } from '@/store/user';
import { uploadFile, getFileById } from '@/api/file/file'; import { uploadFile, getFileById } from '@/api/file/file';
import { updateUserAvatar } from '@/api/auth/user'; import {
updateUserAvatar,
updateUserPassword,
updateUserProfile,
sendEmailVerifyCode,
bindEmail,
} from '@/api/auth/user';
import { isApiSuccess } from '@/api/response'; import { isApiSuccess } from '@/api/response';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { import {
@@ -265,6 +287,12 @@ const phoneLoading = ref(false);
const emailLoading = ref(false); const emailLoading = ref(false);
const codeSending = ref(false); const codeSending = ref(false);
// 验证码倒计时
const emailCountdown = ref(0);
const phoneCountdown = ref(0);
let emailTimer: number | null = null;
let phoneTimer: number | null = null;
// 弹窗显示状态 // 弹窗显示状态
const showPasswordModal = ref(false); const showPasswordModal = ref(false);
const showPhoneModal = ref(false); const showPhoneModal = ref(false);
@@ -329,8 +357,8 @@ const maskPhone = (phone: string) => {
const maskEmail = (email: string) => { const maskEmail = (email: string) => {
if (!email) return ''; if (!email) return '';
const parts = email.split('@'); const parts = email.split('@');
if (parts.length !== 2) return email; if (parts.length !== 2 || !parts[0] || !parts[1]) return email;
const [name, domain] = parts; const [name = '', domain = ''] = parts;
if (name.length <= 2) return email; if (name.length <= 2) return email;
return `${name.slice(0, 2)}***@${domain}`; return `${name.slice(0, 2)}***@${domain}`;
}; };
@@ -418,9 +446,23 @@ const computeFileHash = (file: File): Promise<string> => {
// 提交资料表单 // 提交资料表单
const handleSubmit = async () => { const handleSubmit = async () => {
// TODO: 等后端接口提供后对接 try {
Message.info('更新用户信息接口开发中...'); loading.value = true;
console.log('提交数据:', formData); const res = await updateUserProfile({
userName: formData.userName,
userProfile: formData.userProfile,
});
if (isApiSuccess(res)) {
Message.success('资料更新成功');
await userStore.getLoginUser();
} else {
Message.error(res.message || '更新失败');
}
} catch (err: any) {
Message.error('更新出错: ' + (err.message || '未知错误'));
} finally {
loading.value = false;
}
}; };
// 重置资料表单 // 重置资料表单
@@ -433,31 +475,49 @@ const handleReset = () => {
const handlePasswordSubmit = async () => { const handlePasswordSubmit = async () => {
if (!passwordForm.oldPassword) { if (!passwordForm.oldPassword) {
Message.warning('请输入当前密码'); Message.warning('请输入当前密码');
return; return false;
} }
if (!passwordForm.newPassword) { if (!passwordForm.newPassword) {
Message.warning('请输入新密码'); Message.warning('请输入新密码');
return; return false;
} }
if (passwordForm.newPassword.length < 6 || passwordForm.newPassword.length > 20) { if (passwordForm.newPassword.length < 6 || passwordForm.newPassword.length > 20) {
Message.warning('新密码长度应为6-20位'); Message.warning('新密码长度应为6-20位');
return; return false;
} }
if (passwordForm.newPassword !== passwordForm.confirmPassword) { if (passwordForm.newPassword !== passwordForm.confirmPassword) {
Message.warning('两次输入的密码不一致'); Message.warning('两次输入的密码不一致');
return; return false;
} }
if (passwordForm.oldPassword === passwordForm.newPassword) { if (passwordForm.oldPassword === passwordForm.newPassword) {
Message.warning('新密码不能与当前密码相同'); Message.warning('新密码不能与当前密码相同');
return; return false;
} }
// TODO: 等后端接口提供后对接 try {
Message.info('修改密码接口开发中...'); passwordLoading.value = true;
console.log('修改密码:', { const res = await updateUserPassword(
oldPassword: passwordForm.oldPassword, passwordForm.oldPassword,
newPassword: passwordForm.newPassword, passwordForm.newPassword
}); );
if (isApiSuccess(res)) {
Message.success('密码修改成功,请重新登录');
handlePasswordReset();
// 修改密码后跳转到登录页
setTimeout(() => {
window.location.href = '/user/login';
}, 1500);
return true;
} else {
Message.error(res.message || '修改密码失败');
return false;
}
} catch (err: any) {
Message.error('修改密码出错: ' + (err.message || '未知错误'));
return false;
} finally {
passwordLoading.value = false;
}
}; };
// 重置密码表单 // 重置密码表单
@@ -471,20 +531,21 @@ const handlePasswordReset = () => {
const handlePhoneSubmit = async () => { const handlePhoneSubmit = async () => {
if (!phoneForm.phone) { if (!phoneForm.phone) {
Message.warning('请输入手机号'); Message.warning('请输入手机号');
return; return false;
} }
if (!/^1[3-9]\d{9}$/.test(phoneForm.phone)) { if (!/^1[3-9]\d{9}$/.test(phoneForm.phone)) {
Message.warning('请输入正确的手机号'); Message.warning('请输入正确的手机号');
return; return false;
} }
if (!phoneForm.code) { if (!phoneForm.code) {
Message.warning('请输入验证码'); Message.warning('请输入验证码');
return; return false;
} }
// TODO: 等后端接口提供后对接 // TODO: 等后端接口提供后对接
Message.info('绑定手机号接口开发中...'); Message.info('绑定手机号接口开发中...');
console.log('绑定手机号:', phoneForm); console.log('绑定手机号:', phoneForm);
return false;
}; };
// 发送手机验证码 // 发送手机验证码
@@ -500,32 +561,63 @@ const handleSendPhoneCode = async () => {
// TODO: 等后端接口提供后对接 // TODO: 等后端接口提供后对接
Message.info('发送验证码接口开发中...'); Message.info('发送验证码接口开发中...');
// 模拟倒计时(实际对接后端后移到成功回调中)
phoneCountdown.value = 60;
if (phoneTimer) clearInterval(phoneTimer);
phoneTimer = window.setInterval(() => {
phoneCountdown.value--;
if (phoneCountdown.value <= 0) {
if (phoneTimer) clearInterval(phoneTimer);
phoneTimer = null;
}
}, 1000);
}; };
// 重置手机表单 // 重置手机表单
const handlePhoneReset = () => { const handlePhoneReset = () => {
phoneForm.phone = ''; phoneForm.phone = '';
phoneForm.code = ''; phoneForm.code = '';
// 清理倒计时
if (phoneTimer) {
clearInterval(phoneTimer);
phoneTimer = null;
}
phoneCountdown.value = 0;
}; };
// 绑定邮箱 // 绑定邮箱
const handleEmailSubmit = async () => { const handleEmailSubmit = async () => {
if (!emailForm.email) { if (!emailForm.email) {
Message.warning('请输入邮箱'); Message.warning('请输入邮箱');
return; return false;
} }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailForm.email)) { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailForm.email)) {
Message.warning('请输入正确的邮箱地址'); Message.warning('请输入正确的邮箱地址');
return; return false;
} }
if (!emailForm.code) { if (!emailForm.code) {
Message.warning('请输入验证码'); Message.warning('请输入验证码');
return; return false;
} }
// TODO: 等后端接口提供后对接 try {
Message.info('绑定邮箱接口开发中...'); emailLoading.value = true;
console.log('绑定邮箱:', emailForm); const res = await bindEmail(emailForm.email, emailForm.code);
if (isApiSuccess(res)) {
Message.success('邮箱绑定成功');
handleEmailReset();
await userStore.getLoginUser();
return true;
} else {
Message.error(res.message || '绑定失败');
return false;
}
} catch (err: any) {
Message.error('绑定邮箱出错: ' + (err.message || '未知错误'));
return false;
} finally {
emailLoading.value = false;
}
}; };
// 发送邮箱验证码 // 发送邮箱验证码
@@ -539,14 +631,41 @@ const handleSendEmailCode = async () => {
return; return;
} }
// TODO: 等后端接口提供后对接 try {
Message.info('发送验证码接口开发中...'); codeSending.value = true;
const res = await sendEmailVerifyCode(emailForm.email);
if (isApiSuccess(res)) {
Message.success('验证码已发送,请查收邮箱');
// 开始倒计时
emailCountdown.value = 60;
if (emailTimer) clearInterval(emailTimer);
emailTimer = window.setInterval(() => {
emailCountdown.value--;
if (emailCountdown.value <= 0) {
if (emailTimer) clearInterval(emailTimer);
emailTimer = null;
}
}, 1000);
} else {
Message.error(res.message || '发送失败');
}
} catch (err: any) {
Message.error('发送验证码出错: ' + (err.message || '未知错误'));
} finally {
codeSending.value = false;
}
}; };
// 重置邮箱表单 // 重置邮箱表单
const handleEmailReset = () => { const handleEmailReset = () => {
emailForm.email = ''; emailForm.email = '';
emailForm.code = ''; emailForm.code = '';
// 清理倒计时
if (emailTimer) {
clearInterval(emailTimer);
emailTimer = null;
}
emailCountdown.value = 0;
}; };
// 绑定微信 // 绑定微信
@@ -564,6 +683,18 @@ const handleUnbindWechat = () => {
onMounted(() => { onMounted(() => {
userStore.getLoginUser(); userStore.getLoginUser();
}); });
// 清理定时器
onUnmounted(() => {
if (emailTimer) {
clearInterval(emailTimer);
emailTimer = null;
}
if (phoneTimer) {
clearInterval(phoneTimer);
phoneTimer = null;
}
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -642,6 +773,18 @@ onMounted(() => {
:deep(.arco-textarea) { :deep(.arco-textarea) {
border-radius: 8px; border-radius: 8px;
} }
// 验证码输入组样式
:deep(.arco-input-group) {
.arco-input {
flex: 1;
}
.arco-btn {
flex-shrink: 0;
min-width: 100px;
}
}
} }
.security-card { .security-card {