foreign-admin-ui/src/views/profile/personalCenter/index.vue

569 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>