front/src/components/top-nav/RightTopUser.vue
zzh c6a4484448 feat: 查价价卡展示贸易条款与产品分组,并优化 H5/登录与顾问体验
销售产品可配置 DDP/DDU;查价返回分组与贸易条款并展示在价卡上。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 11:48:36 +08:00

283 lines
7.7 KiB
Vue

<template>
<a-dropdown trigger="hover" placement="bottomRight" popup-class="user-account-dropdown">
<div class="user-dropdown-trigger">
<span class="user-name">{{ displayName }}</span>
<IconSettings class="user-icon" />
</div>
<template #content>
<a-doption v-if="isCustomerPortal" @click="() => onMenuClick({ key: 'userinfo' })">
<span class="user-menu-item">
<span class="user-menu-item__icon"><i class="fa fa-user" /></span>
<span class="user-menu-item__label">{{ t('menu.user.profile') }}</span>
</span>
</a-doption>
<a-doption v-if="!isCustomerPortal" @click="() => onMenuClick({ key: 'settings' })">
<span class="user-menu-item">
<span class="user-menu-item__icon"><i class="fa fa-cog" /></span>
<span class="user-menu-item__label">页面配置</span>
</span>
</a-doption>
<a-doption @click="() => onMenuClick({ key: 'changePassword' })">
<span class="user-menu-item">
<span class="user-menu-item__icon"><i class="fa fa-lock" /></span>
<span class="user-menu-item__label">{{ t('profile.changePassword') }}</span>
</span>
</a-doption>
<a-doption v-if="isCustomerPortal" @click="() => onMenuClick({ key: 'settings' })">
<span class="user-menu-item">
<span class="user-menu-item__icon"><i class="fa fa-cog" /></span>
<span class="user-menu-item__label">页面配置</span>
</span>
</a-doption>
<a-doption @click="() => onMenuClick({ key: 'logout' })">
<span class="user-menu-item is-danger">
<span class="user-menu-item__icon"><i class="fa fa-sign-in" /></span>
<span class="user-menu-item__label">退出登录</span>
</span>
</a-doption>
</template>
</a-dropdown>
<a-modal
v-model:visible="changePasswordVisible"
:title="t('profile.changePassword')"
:mask-closable="false"
:confirm-loading="changePasswordLoading"
@ok="handleChangePasswordSubmit"
@cancel="handleChangePasswordCancel"
>
<a-form
ref="pwdFormRef"
:model="pwdForm"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 16 }"
>
<a-form-item
field="oldPassword"
:label="t('profile.oldPassword')"
:rules="[{ required: true, message: t('profile.oldPassword') }]"
>
<a-input-password
v-model="pwdForm.oldPassword"
:placeholder="t('profile.oldPassword')"
allow-clear
/>
</a-form-item>
<a-form-item
field="newPassword"
:label="t('profile.newPassword')"
:rules="[{ required: true, message: t('profile.newPassword') }]"
>
<a-input-password
v-model="pwdForm.newPassword"
:placeholder="t('profile.newPassword')"
allow-clear
/>
</a-form-item>
<a-form-item
field="confirmPassword"
:label="t('profile.confirmPassword')"
:rules="[
{ required: true, message: t('profile.confirmPassword') },
{ validator: validateConfirmPassword },
]"
>
<a-input-password
v-model="pwdForm.confirmPassword"
:placeholder="t('profile.confirmPassword')"
allow-clear
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Message } from '@arco-design/web-vue';
import { useUserStore, useAppStore } from '@/store';
import { useRbacUserStore } from '@/store/modules/rbac-user';
import { AuthService } from '@/api/rbac';
import { IconSettings } from '@arco-design/web-vue/es/icon';
import { resolvePostLogoutLogin } from '@/utils/login-portal';
const { t } = useI18n();
const router = useRouter();
const userStore = useUserStore();
const appStore = useAppStore();
const rbacUserStore = useRbacUserStore();
const isCustomerPortal = computed(() => rbacUserStore.isCustomerPortal());
const displayName = computed(() => {
return userStore.name || '--';
});
const changePasswordVisible = ref(false);
const changePasswordLoading = ref(false);
const pwdFormRef = ref();
const pwdForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
function validateConfirmPassword(
_rule: unknown,
value: string,
callback: (error?: string) => void
) {
if (value !== pwdForm.newPassword) {
callback(t('profile.passwordMismatch'));
} else {
callback();
}
}
function resetPasswordForm() {
pwdForm.oldPassword = '';
pwdForm.newPassword = '';
pwdForm.confirmPassword = '';
}
function openChangePasswordModal() {
resetPasswordForm();
changePasswordVisible.value = true;
}
function handleChangePasswordCancel() {
changePasswordVisible.value = false;
resetPasswordForm();
}
async function handleChangePasswordSubmit() {
try {
await pwdFormRef.value?.validate();
} catch {
return;
}
changePasswordLoading.value = true;
try {
await AuthService.postRbacAuthChangePassword({
oldPassword: pwdForm.oldPassword,
newPassword: pwdForm.newPassword,
});
Message.success(t('profile.changePasswordSuccess'));
changePasswordVisible.value = false;
resetPasswordForm();
const loginType = rbacUserStore.resolveLoginTypeForRedirect();
const currentPath = router.currentRoute.value.fullPath;
rbacUserStore.logout();
userStore.logout();
const target = resolvePostLogoutLogin({ loginType, path: currentPath });
if (target.path) {
router.replace({ path: target.path, query: target.query });
} else {
router.replace({ name: target.name, query: target.query });
}
} catch (err: any) {
if (err?.errors || err?.errorFields) return;
Message.error(err?.message || '修改密码失败');
} finally {
changePasswordLoading.value = false;
}
}
const onMenuClick = async (event: { key: string }) => {
const { key } = event;
if (key === 'logout') {
const loginType = rbacUserStore.resolveLoginTypeForRedirect();
const currentPath = router.currentRoute.value.fullPath;
rbacUserStore.logout();
userStore.logout();
const target = resolvePostLogoutLogin({ loginType, path: currentPath });
if (target.path) {
router.push({ path: target.path, query: target.query });
} else {
router.push({ name: target.name, query: target.query });
}
} else if (key === 'userinfo') {
router.push({ name: 'UserProfile' });
} else if (key === 'changePassword') {
openChangePasswordModal();
} else if (key === 'settings') {
appStore.updateSettings({ globalSettings: true });
}
};
</script>
<style scoped>
.user-dropdown-trigger {
display: flex;
align-items: center;
white-space: nowrap;
cursor: pointer;
padding: 0 8px;
flex-shrink: 0;
color: inherit;
text-decoration: none;
transition: all 0.3s;
}
.user-dropdown-trigger:hover {
opacity: 0.8;
}
.user-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
margin-right: 4px;
}
.user-icon {
font-size: 16px;
flex-shrink: 0;
}
.user-menu-item {
display: flex;
align-items: center;
width: 132px;
box-sizing: border-box;
}
.user-menu-item__icon {
width: 18px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 10px;
font-size: 14px;
}
.user-menu-item__label {
flex: 1;
white-space: nowrap;
font-weight: 500;
text-align: left;
}
.user-menu-item.is-danger .user-menu-item__label {
color: rgb(var(--red-6));
}
</style>
<style lang="less">
.user-account-dropdown {
.arco-dropdown {
min-width: 156px;
}
.arco-dropdown-option-content {
padding: 8px 12px;
}
}
</style>