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

680 lines
21 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>{{ t('personalCenter.pageTitle') }}</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')">{{
t('personalCenter.modify') }}</el-button>
</div>
<div class="login-time">{{ t('personalCenter.lastLogin') }}{{ 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">{{ t('personalCenter.email') }}</span>
<span class="value">{{ userInfo.email || t('personalCenter.notSet') }}</span>
<template v-if="!userInfo.email">
<el-button link type="primary" @click="showEditDialog('email')">{{ t('personalCenter.goToBind')
}}</el-button>
</template>
<template v-else>
<el-tag type="success" size="small">{{ t('personalCenter.bound') }}</el-tag>
</template>
</div>
<div class="info-item">
<span class="label">{{ t('personalCenter.mobile') }}</span>
<span class="value">{{ userInfo.mobile || t('personalCenter.notBound') }}</span> {{ }}
<template v-if="!userInfo.mobile">
<el-button link type="primary" @click="showEditDialog('mobile')">{{ t('personalCenter.bind')
}}</el-button>
</template>
<template v-else>
<el-tag type="success" size="small">{{ t('personalCenter.bound') }}</el-tag>
</template>
</div>
<div class="info-item">
<span class="label">{{ t('personalCenter.password') }}</span>
<el-button link type="primary" @click="showPasswordDialog">{{ t('personalCenter.modify') }}</el-button>
</div>
<div class="info-item">
<span class="label">{{ t('personalCenter.language') }}</span>
<span class="value">{{ t('personalCenter.languageDesc') }}</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 v-if="editField === 'mobile'" :label="editForm.label" prop="value" class="phone-item">
<el-select v-model="editForm.countryCode" :teleported="false" value-key="code" placeholder="国家" size="large">
<el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code">
<div class="country-option">
<span class="country-name">{{ item.name }}</span>
<span class="country-code">{{ item.code }}</span>
</div>
</el-option>
</el-select>
<el-input v-model="editForm.value" :placeholder="`${t('common.pleaseInput')}${editForm.label}`"
size="large" />
</el-form-item>
<el-form-item v-else-if="editField !== 'language'" :label="editForm.label" prop="value">
<el-input v-model="editForm.value" :placeholder="`${t('common.pleaseInput')}${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="t('personalCenter.modifyPassword')" 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="t('personalCenter.verifyMethod')" prop="verifyType">
<el-radio-group v-model="passwordForm.verifyType" @change="onVerifyTypeChange">
<el-radio label="mobile" :disabled="!userInfo.mobile">{{ t('personalCenter.mobileVerify') }}</el-radio>
<el-radio label="email" :disabled="!userInfo.email">{{ t('personalCenter.emailVerify') }}</el-radio>
</el-radio-group>
</el-form-item>
<!-- 验证账号 -->
<el-form-item :label="t('personalCenter.verifyAccount')">
<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="t('personalCenter.newPassword')" prop="login_password1">
<el-input v-model="passwordForm.login_password1" type="password" show-password
:placeholder="t('personalCenter.passwordPlaceholder')" size="large" />
</el-form-item>
<!-- 新密码2 -->
<el-form-item :label="t('personalCenter.confirmPassword')" prop="login_password2">
<el-input v-model="passwordForm.login_password2" type="password" show-password
:placeholder="t('personalCenter.confirmPasswordPlaceholder')" size="large" />
</el-form-item>
</el-form>
</Dialog>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'PersonalCenter'
})
import { ref, reactive, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { UserFilled } from '@element-plus/icons-vue';
import { useI18n } from 'vue-i18n';
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 { countryOptions } from '@/views/user/config/mock';
const { t, locale } = useI18n()
// 类型定义
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: '', countryCode: '+86' });
// 密码表单
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 = reactive({ isCounting: false, countdown: 0 });
const pwdMobileCaptchaState = initCaptchaState('phone');
const pwdEmailCaptchaState = initCaptchaState('email');
const userStore = useUserStore();
const languageStore = useLanguageStore();
// 判断是否可以绑定/重新绑定邮箱
const canBindEmail = computed(() => {
return false
// return userStore.userInfo?.reg_type === 2; // 手机注册用户可以绑定/重新绑定邮箱
});
// 判断是否可以绑定/重新绑定手机号
const canBindMobile = computed(() => {
return false
// return userStore.userInfo?.reg_type === 1; // 邮箱注册用户可以绑定/重新绑定手机号
});
// 校验规则
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: t('personalCenter.pleaseInputEmail'), trigger: 'blur' },
{ type: 'email', message: t('personalCenter.emailFormatError'), trigger: 'blur' }
];
return rules;
case 'mobile':
rules.value = [
{ required: true, message: t('personalCenter.pleaseInputMobile'), trigger: 'blur' },
{ pattern: /^\+?\d{5,20}$/, message: t('personalCenter.mobileFormatError'), trigger: 'blur' }
];
return rules;
case 'username':
rules.value = [
{ required: true, message: t('personalCenter.pleaseInputUsername'), trigger: 'blur' },
{ min: 2, max: 20, message: t('personalCenter.usernameLengthError'), trigger: 'blur' }
];
return rules;
default:
return {};
}
};
// 密码强度校验
const passwordFormRules = ref<FormRules>({
verifyType: [{ required: true, message: t('personalCenter.pleaseSelectVerifyMethod'), trigger: 'change' }],
code: codeRules().code,
login_password1: [
{ required: true, message: t('personalCenter.pleaseInputNewPassword'), trigger: 'blur' },
{ min: 6, max: 12, message: t('personalCenter.passwordLengthError'), trigger: 'blur' },
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{6,12}$/,
message: t('personalCenter.passwordComplexityError'), trigger: 'blur'
}
],
login_password2: [
{ required: true, message: t('personalCenter.pleaseConfirmNewPassword'), trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== passwordForm.login_password1) {
callback(new Error(t('personalCenter.passwordMismatchError')));
} 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(t('personalCenter.getUserInfoFailed'));
console.error('获取用户信息失败:', error);
}
};
// 打开编辑弹窗
const showEditDialog = (field: 'email' | 'mobile' | 'username') => {
editField.value = field;
editForm.code = '';
editForm.countryCode = '+86';
switch (field) {
case 'email':
editForm.label = t('personalCenter.email');
editForm.value = userInfo.email;
dialogTitle.value = t('personalCenter.bindEmail');
break;
case 'mobile':
editForm.label = t('personalCenter.mobile');
editForm.value = '';
dialogTitle.value = userInfo.mobile ? t('personalCenter.rebindMobile') : t('personalCenter.bindMobile');
break;
case 'username':
editForm.label = t('personalCenter.username');
editForm.value = userInfo.username;
dialogTitle.value = t('personalCenter.modifyUsername');
break;
}
dialogVisible.value = true;
updateSendState();
};
// 发送绑定验证码
const handleSendCaptcha = async () => {
const type = editField.value === 'email' ? 'email' : 'phone';
let receiver = editForm.value?.trim();
if (!receiver) {
ElMessage.warning(t('personalCenter.pleaseInputReceiverAccount'));
return;
}
// 如果是手机号,需要组合区号
if (type === 'phone') {
receiver = editForm.countryCode + receiver;
}
const currentState = type === 'email' ? emailCaptchaState : phoneCaptchaState;
await sendCaptcha(type, receiver, currentState, () => {
const params: any = {
reg_type: userStore.userInfo!.reg_type,
type: type === 'email' ? 'transfer_code' : 'bindMobile_code'
};
if (type === 'email') {
params.email = receiver;
} else {
params.mobile = receiver;
params.mobile_area = editForm.countryCode;
}
return userStore.sendCode(params);
});
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;
// 更新全局用户信息
userStore.updateUserInfo({ username: editForm.value });
}
if (editField.value === 'mobile') {
const mobile = editForm.countryCode + editForm.value;
const params: BindMobileParams = { mobile, code: editForm.code, mobile_area: editForm.countryCode };
await bindMobileApi(params);
userInfo.mobile = mobile;
}
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(t('personalCenter.pleaseBindMobileOrEmail'));
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';
let receiver = passwordForm.verifyType === 'email' ? userInfo.email : userInfo.mobile;
if (!receiver) {
ElMessage.warning(t('personalCenter.pleaseInputVerifyAccount'));
return;
}
const currentState = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
await sendCaptcha(type, receiver, currentState, () => {
const params: any = {
reg_type: userStore.userInfo!.reg_type,
type: 'resetPass_code'
};
if (type === 'email') {
params.email = receiver;
} else {
params.mobile = receiver;
// 从手机号中提取区号(假设手机号格式为 +8613800138000
const match = receiver.match(/^\+(\d{1,4})/);
params.mobile_area = match ? '+' + match[1] : '+86';
}
return userStore.sendCode(params);
});
updatePwdSendState();
};
const updatePwdSendState = () => {
const t = passwordForm.verifyType === 'email' ? pwdEmailCaptchaState : pwdMobileCaptchaState;
pwdSendState.isCounting = t.isCounting.value;
pwdSendState.countdown = t.countdown.value;
};
// 监听倒计时状态变化实时同步到UI
watch(
() => pwdMobileCaptchaState.countdown.value,
() => {
if (passwordForm.verifyType === 'mobile') {
updatePwdSendState();
}
}
);
watch(
() => pwdEmailCaptchaState.countdown.value,
() => {
if (passwordForm.verifyType === 'email') {
updatePwdSendState();
}
}
);
// 提交修改密码
const handlePasswordConfirm = async () => {
if (!passwordFormRef.value) return;
try {
await passwordFormRef.value.validate();
const params: UpdatePwdByCaptchaParams = {
reg_type: userStore.userInfo!.reg_type,
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
};
// 如果是手机验证,添加 mobile_area 参数
if (passwordForm.verifyType === 'mobile' && userInfo.mobile) {
const match = userInfo.mobile.match(/^\+(\d{1,4})/);
params.mobile_area = match ? '+' + match[1] : '+86';
}
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();
};
// 监听语言切换,同步更新 i18n 的 locale
watch(
() => languageStore.lang,
(newLang) => {
locale.value = newLang
},
{ immediate: true }
)
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;
}
}
}
.phone-item {
:deep(.el-form-item__content) {
flex-wrap: nowrap;
}
.el-select {
width: 120px;
margin-right: 10px;
.country-option {
width: 120px;
display: flex;
justify-content: space-between;
}
:deep(.el-select-dropdown__item) {
padding: 0 16px 0 16px;
}
}
}
</style>