569 lines
17 KiB
Vue
569 lines
17 KiB
Vue
<template>
|
||
<div class="personal-center">
|
||
<div class="page-header">
|
||
<h2>账户信息</h2>
|
||
</div>
|
||
|
||
<div class="content-wrapper">
|
||
<el-card class="card avatar-card">
|
||
<div class="avatar-box">
|
||
<div class="avatar-wrapper">
|
||
<img v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar-image" alt="用户头像" />
|
||
<el-icon v-else class="avatar-icon">
|
||
<UserFilled />
|
||
</el-icon>
|
||
</div>
|
||
|
||
<div class="info-text">
|
||
<div class="username-row">
|
||
<span class="username-text">{{ userInfo.username }}</span>
|
||
<el-button link type="primary" size="small" @click="showEditDialog('username')">修改</el-button>
|
||
</div>
|
||
<div class="login-time">最后登录:{{ userInfo.last_login }}</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card class="card info-card">
|
||
<div class="info-list">
|
||
<div class="info-item">
|
||
<span class="label">邮箱</span>
|
||
<span class="value">{{ userInfo.email || '未设置' }}</span>
|
||
<template v-if="!userInfo.email">
|
||
<el-button link type="primary" @click="showEditDialog('email')">去绑定</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<el-tag type="success" size="small">已绑定</el-tag>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="info-item">
|
||
<span class="label">手机号</span>
|
||
<span class="value">{{ userInfo.mobile || '未绑定' }}</span>
|
||
<el-button v-if="!userInfo.mobile" link type="primary" @click="showEditDialog('mobile')">
|
||
绑定
|
||
</el-button>
|
||
<template v-else>
|
||
<el-tag type="success" size="small">已绑定</el-tag>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="info-item">
|
||
<span class="label">密码</span>
|
||
<el-button link type="primary" @click="showPasswordDialog">修改</el-button>
|
||
</div>
|
||
|
||
<div class="info-item">
|
||
<span class="label">语言</span>
|
||
<span class="value">选择您偏好的通讯语言</span>
|
||
<el-select v-model="userInfo.lang_type" @change="handleLanguageChange" style="width: 130px" size="small">
|
||
<el-option label="简体中文" :value="1" />
|
||
<el-option label="English" :value="2" />
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
|
||
<!-- 编辑弹窗 -->
|
||
<Dialog :visible="dialogVisible" :title="dialogTitle" width="500px" @confirm="handleEditConfirm"
|
||
@cancel="handleEditDialogReset" @close="handleEditDialogReset">
|
||
<el-form ref="editFormRef" :model="editForm" :rules="getEditRules()" label-width="80px" label-position="top">
|
||
<el-form-item :label="editForm.label" prop="value">
|
||
<el-input v-if="editField !== 'language'" v-model="editForm.value" :placeholder="`请输入${editForm.label}`"
|
||
size="large" />
|
||
</el-form-item>
|
||
|
||
<UserCaptcha v-if="['email', 'mobile'].includes(editField)" v-model="editForm.code"
|
||
@sendCode="handleSendCaptcha" :show-send-button="showSendButton" :send-state="sendState" />
|
||
</el-form>
|
||
</Dialog>
|
||
|
||
<!-- 密码修改弹窗 → 验证码模式 -->
|
||
<Dialog :visible="passwordDialogVisible" title="修改密码" width="500px" @confirm="handlePasswordConfirm"
|
||
@cancel="handlePasswordDialogReset" @close="handlePasswordDialogReset">
|
||
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordFormRules" label-width="100px"
|
||
label-position="top">
|
||
<!-- 选择验证方式 -->
|
||
<el-form-item label="验证方式" prop="verifyType">
|
||
<el-radio-group v-model="passwordForm.verifyType" @change="onVerifyTypeChange">
|
||
<el-radio label="mobile" :disabled="!userInfo.mobile">手机验证</el-radio>
|
||
<el-radio label="email" :disabled="!userInfo.email">邮箱验证</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<!-- 验证账号 -->
|
||
<el-form-item label="验证账号">
|
||
<span style="font-weight:500;color:#333">
|
||
{{ passwordForm.verifyType === 'mobile' ? userInfo.mobile : userInfo.email }}
|
||
</span>
|
||
</el-form-item>
|
||
|
||
<!-- 验证码 -->
|
||
<UserCaptcha prop="code" v-model="passwordForm.code" @sendCode="handleSendPwdCaptcha"
|
||
:show-send-button="showPwdSendButton" :send-state="pwdSendState" />
|
||
|
||
<!-- 新密码1 -->
|
||
<el-form-item label="新密码" prop="login_password1">
|
||
<el-input v-model="passwordForm.login_password1" type="password" show-password placeholder="6-12位,含大小写+特殊字符"
|
||
size="large" />
|
||
</el-form-item>
|
||
|
||
<!-- 新密码2 -->
|
||
<el-form-item label="确认密码" prop="login_password2">
|
||
<el-input v-model="passwordForm.login_password2" type="password" show-password placeholder="请再次输入新密码"
|
||
size="large" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</Dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
|
||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||
import { UserFilled } from '@element-plus/icons-vue';
|
||
import Dialog from '@/components/common/Dialog.vue';
|
||
import UserCaptcha from '@/views/user/components/Captcha.vue';
|
||
import { useUserStore } from '@/stores/modules/user';
|
||
import { useLanguageStore } from '@/stores/modules/language';
|
||
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha';
|
||
import { codeRules } from '@/views/user/config/rules';
|
||
|
||
// 类型定义
|
||
import type {
|
||
UserInfo,
|
||
EditForm,
|
||
PasswordForm,
|
||
EditLangTypeParams,
|
||
BindMobileParams,
|
||
BindEmailParams,
|
||
EditUsernameParams,
|
||
UpdatePwdByCaptchaParams
|
||
} from '@/api/types/profile';
|
||
|
||
// 接口
|
||
import {
|
||
getUserInfoApi,
|
||
editLangTypeApi,
|
||
bindMobileApi,
|
||
bindEmailApi,
|
||
editUsernameApi,
|
||
updatePwdByCaptchaApi
|
||
} from '@/api/modules/profile/personalCenter';
|
||
|
||
const userInfo = reactive<UserInfo>({
|
||
id: 0,
|
||
username: '',
|
||
email: '',
|
||
mobile: '',
|
||
lang_type: 1,
|
||
avatar: '',
|
||
last_login: ''
|
||
});
|
||
|
||
const dialogVisible = ref(false);
|
||
const passwordDialogVisible = ref(false);
|
||
const editFormRef = ref<FormInstance>();
|
||
const passwordFormRef = ref<FormInstance>();
|
||
const editField = ref<string>('');
|
||
const dialogTitle = ref('');
|
||
|
||
const editForm = reactive<EditForm>({ label: '', value: '', code: '' });
|
||
|
||
// 密码表单
|
||
const passwordForm = reactive<PasswordForm>({
|
||
verifyType: 'mobile',
|
||
code: '',
|
||
login_password1: '',
|
||
login_password2: ''
|
||
});
|
||
|
||
// 验证码状态
|
||
const showSendButton = ref(true);
|
||
const sendState = ref({ isCounting: false, countdown: 0 });
|
||
const phoneCaptchaState = initCaptchaState('phone');
|
||
const emailCaptchaState = initCaptchaState('email');
|
||
|
||
const showPwdSendButton = ref(true);
|
||
const pwdSendState = ref({ isCounting: false, countdown: 0 });
|
||
const pwdMobileCaptchaState = initCaptchaState('phone');
|
||
const pwdEmailCaptchaState = initCaptchaState('email');
|
||
|
||
const userStore = useUserStore();
|
||
const languageStore = useLanguageStore();
|
||
|
||
// 校验规则
|
||
const getEditRules = (): FormRules => {
|
||
const rules: FormRules = {};
|
||
if (['email', 'mobile'].includes(editField.value)) {
|
||
rules.code = codeRules().code;
|
||
}
|
||
switch (editField.value) {
|
||
case 'email':
|
||
rules.value = [
|
||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
|
||
];
|
||
return rules;
|
||
case 'mobile':
|
||
rules.value = [
|
||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||
{ pattern: /^\+?\d{5,20}$/, message: '手机号格式不正确', trigger: 'blur' }
|
||
];
|
||
return rules;
|
||
case 'username':
|
||
rules.value = [
|
||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||
{ min: 2, max: 20, message: '用户名长度 2-20 位', trigger: 'blur' }
|
||
];
|
||
return rules;
|
||
default:
|
||
return {};
|
||
}
|
||
};
|
||
|
||
// 密码强度校验
|
||
const passwordFormRules = ref<FormRules>({
|
||
verifyType: [{ required: true, message: '请选择验证方式', trigger: 'change' }],
|
||
code: codeRules().code,
|
||
login_password1: [
|
||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||
{ min: 6, max: 12, message: '长度 6-12 位', trigger: 'blur' },
|
||
{
|
||
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{6,12}$/,
|
||
message: '必须包含大小写+特殊字符', trigger: 'blur'
|
||
}
|
||
],
|
||
login_password2: [
|
||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
if (value !== passwordForm.login_password1) {
|
||
callback(new Error('两次密码不一致'));
|
||
} else {
|
||
callback();
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
]
|
||
});
|
||
|
||
// 获取用户信息
|
||
const fetchUserProfile = async () => {
|
||
try {
|
||
const res = await getUserInfoApi();
|
||
Object.assign(userInfo, res);
|
||
languageStore.changeLang(res.lang_type === 1 ? 'zh-CN' : 'en-US');
|
||
} catch (error) {
|
||
ElMessage.error('获取用户信息失败');
|
||
console.error('获取用户信息失败:', error);
|
||
}
|
||
};
|
||
|
||
// 打开编辑弹窗
|
||
const showEditDialog = (field: 'email' | 'mobile' | 'username') => {
|
||
editField.value = field;
|
||
editForm.code = '';
|
||
switch (field) {
|
||
case 'email':
|
||
editForm.label = '邮箱';
|
||
editForm.value = userInfo.email;
|
||
dialogTitle.value = '绑定邮箱';
|
||
break;
|
||
case 'mobile':
|
||
editForm.label = '手机号';
|
||
editForm.value = userInfo.mobile;
|
||
dialogTitle.value = userInfo.mobile ? '重新绑定手机号' : '绑定手机号';
|
||
break;
|
||
case 'username':
|
||
editForm.label = '用户名';
|
||
editForm.value = userInfo.username;
|
||
dialogTitle.value = '修改用户名';
|
||
break;
|
||
}
|
||
dialogVisible.value = true;
|
||
updateSendState();
|
||
};
|
||
|
||
// 发送绑定验证码
|
||
const handleSendCaptcha = async () => {
|
||
const type = editField.value === 'email' ? 'email' : 'phone';
|
||
const receiver = editForm.value?.trim();
|
||
if (!receiver) {
|
||
ElMessage.warning('请输入接收验证码的账号');
|
||
return;
|
||
}
|
||
const currentState = type === 'email' ? emailCaptchaState : phoneCaptchaState;
|
||
await sendCaptcha(type, receiver, currentState, () => {
|
||
return userStore.sendCode({
|
||
reg_type: type === 'email' ? 1 : 2,
|
||
[type === 'email' ? 'email' : 'mobile']: receiver,
|
||
type: type === 'email' ? 'transfer_code' : 'bindMobile_code'
|
||
});
|
||
});
|
||
updateSendState();
|
||
};
|
||
|
||
const updateSendState = () => {
|
||
const t = editField.value === 'email' ? emailCaptchaState : phoneCaptchaState;
|
||
sendState.value = { isCounting: t.isCounting.value, countdown: t.countdown.value };
|
||
};
|
||
|
||
// 语言切换
|
||
const handleLanguageChange = async (val: number) => {
|
||
try {
|
||
await editLangTypeApi({ lang_type: val });
|
||
userInfo.lang_type = val;
|
||
languageStore.changeLang(val === 1 ? 'zh-CN' : 'en-US');
|
||
} catch (error) {
|
||
console.error('语言切换失败:', error);
|
||
}
|
||
};
|
||
|
||
// 提交修改
|
||
const handleEditConfirm = async () => {
|
||
if (!editFormRef.value) return;
|
||
try {
|
||
await editFormRef.value.validate();
|
||
|
||
if (editField.value === 'username') {
|
||
const params: EditUsernameParams = { username: editForm.value };
|
||
await editUsernameApi(params);
|
||
userInfo.username = editForm.value;
|
||
}
|
||
if (editField.value === 'mobile') {
|
||
const params: BindMobileParams = { mobile: editForm.value, code: editForm.code };
|
||
await bindMobileApi(params);
|
||
userInfo.mobile = editForm.value;
|
||
}
|
||
if (editField.value === 'email') {
|
||
const params: BindEmailParams = { email: editForm.value, code: editForm.code };
|
||
await bindEmailApi(params);
|
||
userInfo.email = editForm.value;
|
||
}
|
||
dialogVisible.value = false;
|
||
editFormRef.value.resetFields();
|
||
} catch (err) {
|
||
// 表单验证失败或API调用失败时,不显示错误消息,因为ElMessage已经在其他地方处理
|
||
console.error('操作失败:', err);
|
||
}
|
||
};
|
||
|
||
// ==================== 密码修改逻辑 ====================
|
||
const showPasswordDialog = () => {
|
||
if (!userInfo.mobile && !userInfo.email) {
|
||
ElMessage.warning('请先绑定手机或邮箱');
|
||
return;
|
||
}
|
||
passwordForm.verifyType = userInfo.mobile ? 'mobile' : 'email';
|
||
passwordForm.code = '';
|
||
passwordForm.login_password1 = '';
|
||
passwordForm.login_password2 = '';
|
||
passwordDialogVisible.value = true;
|
||
updatePwdSendState();
|
||
|
||
// 修复:打开弹窗后清除验证状态,避免一进入就触发验证
|
||
nextTick(() => {
|
||
passwordFormRef.value?.clearValidate();
|
||
});
|
||
};
|
||
|
||
const onVerifyTypeChange = () => {
|
||
passwordForm.code = '';
|
||
updatePwdSendState();
|
||
|
||
// 切换验证方式时清除验证状态
|
||
nextTick(() => {
|
||
passwordFormRef.value?.clearValidate(['code']);
|
||
});
|
||
};
|
||
|
||
// 发送修改密码验证码
|
||
const handleSendPwdCaptcha = async () => {
|
||
const type = passwordForm.verifyType === 'email' ? 'email' : 'phone';
|
||
const receiver = passwordForm.verifyType === 'email' ? userInfo.email : userInfo.mobile;
|
||
if (!receiver) {
|
||
ElMessage.warning('请先绑定验证账号');
|
||
return;
|
||
}
|
||
const currentState = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
|
||
|
||
await sendCaptcha(type, receiver, currentState, () => {
|
||
return userStore.sendCode({
|
||
reg_type: passwordForm.verifyType === 'email' ? 1 : 2,
|
||
[passwordForm.verifyType]: receiver,
|
||
type: 'resetPass_code'
|
||
});
|
||
});
|
||
updatePwdSendState();
|
||
};
|
||
|
||
const updatePwdSendState = () => {
|
||
const t = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
|
||
pwdSendState.value = { isCounting: t.isCounting.value, countdown: t.countdown.value };
|
||
};
|
||
|
||
// 提交修改密码
|
||
const handlePasswordConfirm = async () => {
|
||
if (!passwordFormRef.value) return;
|
||
try {
|
||
await passwordFormRef.value.validate();
|
||
|
||
const params: UpdatePwdByCaptchaParams = {
|
||
reg_type: passwordForm.verifyType === 'email' ? 1 : 2,
|
||
email: passwordForm.verifyType === 'email' ? userInfo.email : undefined,
|
||
mobile: passwordForm.verifyType === 'mobile' ? userInfo.mobile : undefined,
|
||
code: passwordForm.code,
|
||
login_password1: passwordForm.login_password1,
|
||
login_password2: passwordForm.login_password2
|
||
};
|
||
|
||
await updatePwdByCaptchaApi(params);
|
||
passwordDialogVisible.value = false;
|
||
passwordFormRef.value.resetFields();
|
||
} catch (err) {
|
||
// 表单验证失败或API调用失败时,不显示错误消息,因为ElMessage已经在其他地方处理
|
||
console.error('密码修改失败:', err);
|
||
}
|
||
};
|
||
|
||
const handleEditDialogReset = () => {
|
||
dialogVisible.value = false;
|
||
editFormRef.value?.resetFields();
|
||
};
|
||
|
||
const handlePasswordDialogReset = () => {
|
||
passwordDialogVisible.value = false;
|
||
passwordFormRef.value?.resetFields();
|
||
};
|
||
|
||
onMounted(() => {
|
||
fetchUserProfile();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
clearCaptchaTimer(phoneCaptchaState);
|
||
clearCaptchaTimer(emailCaptchaState);
|
||
clearCaptchaTimer(pwdMobileCaptchaState);
|
||
clearCaptchaTimer(pwdEmailCaptchaState);
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.personal-center {
|
||
padding: 24px;
|
||
|
||
.page-header {
|
||
margin-bottom: 20px;
|
||
|
||
h2 {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #1f2329;
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.content-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.card {
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
||
border: none;
|
||
}
|
||
|
||
.avatar-card {
|
||
.avatar-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
|
||
.avatar-wrapper {
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 50%;
|
||
background: #f2f3f5;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
|
||
.avatar-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.avatar-icon {
|
||
font-size: 48px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
|
||
.info-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
|
||
.username-row {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 8px;
|
||
|
||
.username-text {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #1f2329;
|
||
line-height: 1.2;
|
||
}
|
||
}
|
||
|
||
.login-time {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-card {
|
||
.info-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid #f2f3f5;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #606266;
|
||
min-width: 70px;
|
||
}
|
||
|
||
.value {
|
||
flex: 1;
|
||
padding: 0 12px;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
</style> |