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;
userProfile?: string;
userEmail?: string;
userPhone?: string;
unionId?: string;
mpOpenId?: string;
createTime?: string;
updateTime?: string;
[key: string]: any;

View File

@@ -152,9 +152,9 @@
<a-modal
v-model:visible="showPasswordModal"
title="修改密码"
@ok="handlePasswordSubmit"
@before-ok="handlePasswordSubmit"
@cancel="handlePasswordReset"
:confirm-loading="passwordLoading"
:ok-loading="passwordLoading"
>
<a-form :model="passwordForm" layout="vertical">
<a-form-item label="当前密码" field="oldPassword">
@@ -185,9 +185,9 @@
<a-modal
v-model:visible="showPhoneModal"
:title="loginUser.userPhone ? '更换手机号' : '绑定手机号'"
@ok="handlePhoneSubmit"
@before-ok="handlePhoneSubmit"
@cancel="handlePhoneReset"
:confirm-loading="phoneLoading"
:ok-loading="phoneLoading"
>
<a-form :model="phoneForm" layout="vertical">
<a-form-item label="手机号" field="phone">
@@ -198,13 +198,21 @@
/>
</a-form-item>
<a-form-item label="验证码" field="code">
<a-input-search
v-model="phoneForm.code"
placeholder="请输入验证码"
button-text="发送验证码"
@search="handleSendPhoneCode"
:loading="codeSending"
/>
<a-input-group style="width: 100%">
<a-input
v-model="phoneForm.code"
placeholder="请输入验证码"
:max-length="6"
/>
<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>
</a-modal>
@@ -213,9 +221,9 @@
<a-modal
v-model:visible="showEmailModal"
:title="loginUser.userEmail ? '更换邮箱' : '绑定邮箱'"
@ok="handleEmailSubmit"
@before-ok="handleEmailSubmit"
@cancel="handleEmailReset"
:confirm-loading="emailLoading"
:ok-loading="emailLoading"
>
<a-form :model="emailForm" layout="vertical">
<a-form-item label="邮箱地址" field="email">
@@ -226,13 +234,21 @@
/>
</a-form-item>
<a-form-item label="验证码" field="code">
<a-input-search
v-model="emailForm.code"
placeholder="请输入验证码"
button-text="发送验证码"
@search="handleSendEmailCode"
:loading="codeSending"
/>
<a-input-group style="width: 100%">
<a-input
v-model="emailForm.code"
placeholder="请输入验证码"
:max-length="6"
/>
<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>
</a-modal>
@@ -240,10 +256,16 @@
</template>
<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 { 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 { Message } from '@arco-design/web-vue';
import {
@@ -265,6 +287,12 @@ const phoneLoading = ref(false);
const emailLoading = 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 showPhoneModal = ref(false);
@@ -329,8 +357,8 @@ const maskPhone = (phone: string) => {
const maskEmail = (email: string) => {
if (!email) return '';
const parts = email.split('@');
if (parts.length !== 2) return email;
const [name, domain] = parts;
if (parts.length !== 2 || !parts[0] || !parts[1]) return email;
const [name = '', domain = ''] = parts;
if (name.length <= 2) return email;
return `${name.slice(0, 2)}***@${domain}`;
};
@@ -418,9 +446,23 @@ const computeFileHash = (file: File): Promise<string> => {
// 提交资料表单
const handleSubmit = async () => {
// TODO: 等后端接口提供后对接
Message.info('更新用户信息接口开发中...');
console.log('提交数据:', formData);
try {
loading.value = true;
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 () => {
if (!passwordForm.oldPassword) {
Message.warning('请输入当前密码');
return;
return false;
}
if (!passwordForm.newPassword) {
Message.warning('请输入新密码');
return;
return false;
}
if (passwordForm.newPassword.length < 6 || passwordForm.newPassword.length > 20) {
Message.warning('新密码长度应为6-20位');
return;
return false;
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
Message.warning('两次输入的密码不一致');
return;
return false;
}
if (passwordForm.oldPassword === passwordForm.newPassword) {
Message.warning('新密码不能与当前密码相同');
return;
return false;
}
// TODO: 等后端接口提供后对接
Message.info('修改密码接口开发中...');
console.log('修改密码:', {
oldPassword: passwordForm.oldPassword,
newPassword: passwordForm.newPassword,
});
try {
passwordLoading.value = true;
const res = await updateUserPassword(
passwordForm.oldPassword,
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 () => {
if (!phoneForm.phone) {
Message.warning('请输入手机号');
return;
return false;
}
if (!/^1[3-9]\d{9}$/.test(phoneForm.phone)) {
Message.warning('请输入正确的手机号');
return;
return false;
}
if (!phoneForm.code) {
Message.warning('请输入验证码');
return;
return false;
}
// TODO: 等后端接口提供后对接
Message.info('绑定手机号接口开发中...');
console.log('绑定手机号:', phoneForm);
return false;
};
// 发送手机验证码
@@ -500,32 +561,63 @@ const handleSendPhoneCode = async () => {
// TODO: 等后端接口提供后对接
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 = () => {
phoneForm.phone = '';
phoneForm.code = '';
// 清理倒计时
if (phoneTimer) {
clearInterval(phoneTimer);
phoneTimer = null;
}
phoneCountdown.value = 0;
};
// 绑定邮箱
const handleEmailSubmit = async () => {
if (!emailForm.email) {
Message.warning('请输入邮箱');
return;
return false;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailForm.email)) {
Message.warning('请输入正确的邮箱地址');
return;
return false;
}
if (!emailForm.code) {
Message.warning('请输入验证码');
return;
return false;
}
// TODO: 等后端接口提供后对接
Message.info('绑定邮箱接口开发中...');
console.log('绑定邮箱:', emailForm);
try {
emailLoading.value = true;
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;
}
// TODO: 等后端接口提供后对接
Message.info('发送验证码接口开发中...');
try {
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 = () => {
emailForm.email = '';
emailForm.code = '';
// 清理倒计时
if (emailTimer) {
clearInterval(emailTimer);
emailTimer = null;
}
emailCountdown.value = 0;
};
// 绑定微信
@@ -564,6 +683,18 @@ const handleUnbindWechat = () => {
onMounted(() => {
userStore.getLoginUser();
});
// 清理定时器
onUnmounted(() => {
if (emailTimer) {
clearInterval(emailTimer);
emailTimer = null;
}
if (phoneTimer) {
clearInterval(phoneTimer);
phoneTimer = null;
}
});
</script>
<style scoped lang="scss">
@@ -642,6 +773,18 @@ onMounted(() => {
:deep(.arco-textarea) {
border-radius: 8px;
}
// 验证码输入组样式
:deep(.arco-input-group) {
.arco-input {
flex: 1;
}
.arco-btn {
flex-shrink: 0;
min-width: 100px;
}
}
}
.security-card {