All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m59s
侧栏改为 Cursor 式账户面板(仓库眼标预览、语言切换、假邮箱过滤),运单「更多」支持发货后四态变更且妥投成功仅可进入。 Co-authored-by: Cursor <cursoragent@cursor.com>
918 lines
25 KiB
Vue
918 lines
25 KiB
Vue
<template>
|
||
<a-dropdown
|
||
v-model:popup-visible="menuVisible"
|
||
:trigger="dropdownTrigger"
|
||
:position="dropdownPosition"
|
||
:content-class="contentClass"
|
||
:popup-translate="popupTranslate"
|
||
@select="onDropdownSelect"
|
||
>
|
||
<div class="user-dropdown-trigger" :class="{ 'is-compact': compact }">
|
||
<img
|
||
v-if="showCustomerAvatar"
|
||
:src="customerAvatarUrl"
|
||
alt=""
|
||
class="user-avatar"
|
||
/>
|
||
<span v-else class="user-avatar user-avatar--fallback">{{ displayNameInitial }}</span>
|
||
<span v-if="!compact" class="user-name">{{ displayName }}</span>
|
||
</div>
|
||
|
||
<!-- Cursor 风格:侧栏账户面板(头信息 / 菜单 / 登出固定底) -->
|
||
<template v-if="isSiderVariant" #content>
|
||
<div class="csr-user-panel" @click.stop>
|
||
<div class="csr-user-panel__header">
|
||
<div class="csr-user-panel__top">
|
||
<div class="csr-user-panel__identity">
|
||
<img
|
||
v-if="showCustomerAvatar"
|
||
:src="customerAvatarUrl"
|
||
alt=""
|
||
class="csr-user-panel__avatar"
|
||
/>
|
||
<span v-else class="csr-user-panel__avatar csr-user-panel__avatar--fallback">
|
||
{{ displayNameInitial }}
|
||
</span>
|
||
<div class="csr-user-panel__meta">
|
||
<div class="csr-user-panel__name">{{ displayName }}</div>
|
||
<div v-if="accountSubtitle" class="csr-user-panel__sub">{{ accountSubtitle }}</div>
|
||
</div>
|
||
</div>
|
||
<button
|
||
v-if="showLanguageMenu"
|
||
type="button"
|
||
class="csr-user-panel__lang-toggle"
|
||
:title="t('nav.userMenu.switchLanguage', { lang: nextLanguageOption.label })"
|
||
:aria-label="t('nav.userMenu.switchLanguage', { lang: nextLanguageOption.label })"
|
||
@click="onPanelAction(`lang:${nextLanguageOption.value}`)"
|
||
>
|
||
{{ nextLanguageOption.short }}
|
||
</button>
|
||
</div>
|
||
<button
|
||
v-if="showRechargeCta"
|
||
type="button"
|
||
class="csr-user-panel__cta"
|
||
@click="onPanelAction('recharge')"
|
||
>
|
||
{{ t('nav.balance.recharge') }}
|
||
<span v-if="balanceText" class="csr-user-panel__cta-balance">¥{{ balanceText }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="csr-user-panel__body">
|
||
<button
|
||
v-for="item in accountMenus"
|
||
:key="accountMenuKey(item)"
|
||
type="button"
|
||
class="csr-user-panel__item"
|
||
@click="onPanelAction(accountMenuKey(item))"
|
||
>
|
||
<span class="csr-user-panel__item-icon">
|
||
<component :is="getSubMenuIcon(item, undefined, MENU_NAME_TO_LOCALE_KEY)" />
|
||
</span>
|
||
<span class="csr-user-panel__item-label">{{ formatAccountTitle(item) }}</span>
|
||
</button>
|
||
<button
|
||
v-if="isCustomerPortal && !hasAccountProfileItem"
|
||
type="button"
|
||
class="csr-user-panel__item"
|
||
@click="onPanelAction('userinfo')"
|
||
>
|
||
<span class="csr-user-panel__item-icon"><i class="fa fa-user" /></span>
|
||
<span class="csr-user-panel__item-label">{{ t('menu.user.profile') }}</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="csr-user-panel__item"
|
||
@click="onPanelAction('changePassword')"
|
||
>
|
||
<span class="csr-user-panel__item-icon"><i class="fa fa-lock" /></span>
|
||
<span class="csr-user-panel__item-label">{{ t('profile.changePassword') }}</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="csr-user-panel__item"
|
||
@click="onPanelAction('settings')"
|
||
>
|
||
<span class="csr-user-panel__item-icon"><i class="fa fa-cog" /></span>
|
||
<span class="csr-user-panel__item-label">{{ t('nav.userMenu.pageSettings') }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="csr-user-panel__footer">
|
||
<button
|
||
type="button"
|
||
class="csr-user-panel__item is-danger"
|
||
@click="onPanelAction('logout')"
|
||
>
|
||
<span class="csr-user-panel__item-icon"><i class="fa fa-sign-out" /></span>
|
||
<span class="csr-user-panel__item-label">{{ t('nav.userMenu.logout') }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else #content>
|
||
<a-doption
|
||
v-for="item in accountMenus"
|
||
:key="accountMenuKey(item)"
|
||
:value="accountMenuKey(item)"
|
||
>
|
||
<span class="user-menu-item">
|
||
<span class="user-menu-item__icon">
|
||
<component :is="getSubMenuIcon(item, undefined, MENU_NAME_TO_LOCALE_KEY)" />
|
||
</span>
|
||
<span class="user-menu-item__label">{{ formatAccountTitle(item) }}</span>
|
||
</span>
|
||
</a-doption>
|
||
<a-doption v-if="isCustomerPortal && !hasAccountProfileItem" value="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" value="settings">
|
||
<span class="user-menu-item">
|
||
<span class="user-menu-item__icon"><i class="fa fa-cog" /></span>
|
||
<span class="user-menu-item__label">{{ t('nav.userMenu.pageSettings') }}</span>
|
||
</span>
|
||
</a-doption>
|
||
<a-doption value="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" value="settings">
|
||
<span class="user-menu-item">
|
||
<span class="user-menu-item__icon"><i class="fa fa-cog" /></span>
|
||
<span class="user-menu-item__label">{{ t('nav.userMenu.pageSettings') }}</span>
|
||
</span>
|
||
</a-doption>
|
||
<template v-if="showLanguageMenu">
|
||
<a-doption
|
||
v-for="item in languageOptions"
|
||
:key="item.value"
|
||
:value="`lang:${item.value}`"
|
||
>
|
||
<span class="user-menu-item">
|
||
<span class="user-menu-item__icon">{{ item.icon }}</span>
|
||
<span class="user-menu-item__label">
|
||
{{ item.label }}
|
||
<span v-if="locale === item.value" class="user-menu-item__hint">✓</span>
|
||
</span>
|
||
</span>
|
||
</a-doption>
|
||
</template>
|
||
<a-doption value="logout">
|
||
<span class="user-menu-item is-danger">
|
||
<span class="user-menu-item__icon"><i class="fa fa-sign-out" /></span>
|
||
<span class="user-menu-item__label">{{ t('nav.userMenu.logout') }}</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, onMounted, onUnmounted, reactive, ref, watch } 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 {
|
||
peekCachedCurrentCustomer,
|
||
resolveCurrentCustomer,
|
||
} from '@/api/customer/resolve';
|
||
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||
import { isValidAvatarUrl, resolveAvatarUrl } from '@/utils/avatar';
|
||
import useLocale from '@/hooks/locale';
|
||
import { resolvePostLogoutLogin } from '@/utils/login-portal';
|
||
import { openPortalSideDrawer } from '@/views/user/portal-drawer/usePortalSideDrawer';
|
||
import { resolvePortalDrawerPanel } from '@/views/user/portal-drawer/types';
|
||
import { getSubMenuIcon } from '@/utils/menu-icon';
|
||
import { MENU_NAME_TO_LOCALE_KEY, translateMenuTitle } from '@/utils/menu-i18n';
|
||
import {
|
||
getCustomerNavPath,
|
||
type CustomerNavItem,
|
||
} from '@/utils/customer-portal-nav';
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
/** 侧栏收起等场景只显示头像 */
|
||
compact?: boolean
|
||
/** 下拉相对触发器位置;侧栏 Cursor 风格默认 tl(向上) */
|
||
dropdownPosition?: 'br' | 'bl' | 'tr' | 'tl' | 'rt' | 'rb'
|
||
/** 客户侧栏:资产与账户菜单,收进用户下拉 */
|
||
accountMenus?: CustomerNavItem[]
|
||
/** 客户侧栏:语言收进用户菜单,不再单独占底栏 */
|
||
showLanguageMenu?: boolean
|
||
/** navbar 保持原下拉;sider 用 Cursor 式面板 */
|
||
variant?: 'navbar' | 'sider'
|
||
trigger?: 'hover' | 'click'
|
||
/** 侧栏面板展示余额并提供去充值 CTA */
|
||
balanceText?: string
|
||
showRechargeCta?: boolean
|
||
}>(),
|
||
{
|
||
compact: false,
|
||
dropdownPosition: undefined,
|
||
accountMenus: () => [],
|
||
showLanguageMenu: false,
|
||
variant: 'navbar',
|
||
trigger: undefined,
|
||
balanceText: '',
|
||
showRechargeCta: false,
|
||
},
|
||
);
|
||
|
||
const { t, locale } = useI18n();
|
||
const { changeLocale } = useLocale();
|
||
const router = useRouter();
|
||
const userStore = useUserStore();
|
||
const appStore = useAppStore();
|
||
const rbacUserStore = useRbacUserStore();
|
||
|
||
const menuVisible = ref(false);
|
||
|
||
const isSiderVariant = computed(() => props.variant === 'sider');
|
||
const showLanguageMenu = computed(() => props.showLanguageMenu);
|
||
const showRechargeCta = computed(() => props.showRechargeCta && isSiderVariant.value);
|
||
const balanceText = computed(() => String(props.balanceText || '').trim());
|
||
|
||
const dropdownTrigger = computed(() => {
|
||
if (props.trigger) return props.trigger;
|
||
return isSiderVariant.value ? 'click' : 'hover';
|
||
});
|
||
|
||
const dropdownPosition = computed(
|
||
() => props.dropdownPosition ?? (isSiderVariant.value ? 'tl' : 'br'),
|
||
);
|
||
|
||
const contentClass = computed(() =>
|
||
isSiderVariant.value ? 'csr-dock-dropdown csr-account-dropdown' : 'user-account-dropdown',
|
||
);
|
||
|
||
/** 向上弹出时略抬离触发器,贴近 Cursor */
|
||
const popupTranslate = computed<[number, number] | undefined>(() =>
|
||
isSiderVariant.value ? [0, -6] : undefined,
|
||
);
|
||
|
||
const languageOptions = [
|
||
{ value: 'zh-CN', label: '中文', short: '中', icon: '🇨🇳' },
|
||
{ value: 'en-US', label: 'English', short: 'EN', icon: '🇺🇸' },
|
||
] as const;
|
||
|
||
/** 点击切换到另一种语言(单按钮,非分段滑块) */
|
||
const nextLanguageOption = computed(() => {
|
||
const current = String(locale.value || 'zh-CN');
|
||
return languageOptions.find((item) => item.value !== current) || languageOptions[1];
|
||
});
|
||
|
||
const isCustomerPortal = computed(() => rbacUserStore.isCustomerPortal());
|
||
|
||
const displayName = computed(() => {
|
||
return userStore.name || '--';
|
||
});
|
||
|
||
const displayNameInitial = computed(() => {
|
||
const name = String(displayName.value || '').trim();
|
||
return name ? name.slice(0, 1).toUpperCase() : '?';
|
||
});
|
||
|
||
/** 后端占位邮箱,不当真实邮箱展示 */
|
||
function isPlaceholderEmail(value: string) {
|
||
const v = value.trim().toLowerCase();
|
||
if (!v) return true;
|
||
return v.endsWith('@noemail.local') || v.includes('@noemail.');
|
||
}
|
||
|
||
const accountSubtitle = computed(() => {
|
||
const info = rbacUserStore.userInfo as Record<string, unknown> | null | undefined;
|
||
const account = String(info?.account ?? '').trim();
|
||
const email = String(info?.email ?? '').trim();
|
||
if (email && !isPlaceholderEmail(email)) return email;
|
||
if (account && !isPlaceholderEmail(account)) return account;
|
||
return '';
|
||
});
|
||
|
||
const compact = computed(() => props.compact);
|
||
const accountMenus = computed(() => props.accountMenus || []);
|
||
|
||
const hasAccountProfileItem = computed(() =>
|
||
accountMenus.value.some((item) => {
|
||
const path = getCustomerNavPath(item);
|
||
return path === '/user/profile' || resolvePortalDrawerPanel(path) === 'profile';
|
||
}),
|
||
);
|
||
|
||
function accountMenuKey(item: CustomerNavItem) {
|
||
return `account:${getCustomerNavPath(item) || item.id || item.name}`;
|
||
}
|
||
|
||
function formatAccountTitle(item: CustomerNavItem) {
|
||
void locale.value;
|
||
return translateMenuTitle(t, {
|
||
name: item.name,
|
||
route: item.route || item.path,
|
||
path: item.path || item.route,
|
||
meta: item.meta,
|
||
});
|
||
}
|
||
|
||
function openAccountMenuItem(item: CustomerNavItem) {
|
||
let targetPath = getCustomerNavPath(item);
|
||
if (!targetPath) return;
|
||
if (!targetPath.startsWith('/')) targetPath = `/${targetPath}`;
|
||
const portalPanel = resolvePortalDrawerPanel(targetPath);
|
||
if (portalPanel) {
|
||
openPortalSideDrawer(portalPanel);
|
||
return;
|
||
}
|
||
if (router.currentRoute.value.path === targetPath) return;
|
||
router.push(targetPath).catch(() => undefined);
|
||
}
|
||
|
||
const customerAvatarRaw = ref('');
|
||
|
||
const customerAvatarUrl = computed(() =>
|
||
resolveAvatarUrl(customerAvatarRaw.value),
|
||
);
|
||
|
||
const showCustomerAvatar = computed(
|
||
() => isCustomerPortal.value && isValidAvatarUrl(customerAvatarRaw.value),
|
||
);
|
||
|
||
async function loadCustomerAvatar() {
|
||
if (!rbacUserStore.isCustomerPortal() || !rbacUserStore.isLoggedIn) {
|
||
customerAvatarRaw.value = '';
|
||
return;
|
||
}
|
||
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
||
const account = rbacUserStore.userInfo?.account?.trim() || '';
|
||
const cached = peekCachedCurrentCustomer({ mapId, account });
|
||
const fromCache =
|
||
cached?.wechatAvatarUrl || cached?.wechatAvatar || '';
|
||
if (isValidAvatarUrl(fromCache)) {
|
||
customerAvatarRaw.value = String(fromCache).trim();
|
||
}
|
||
try {
|
||
const data = await resolveCurrentCustomer({ mapId, account });
|
||
const next = data?.wechatAvatarUrl || data?.wechatAvatar || '';
|
||
customerAvatarRaw.value = isValidAvatarUrl(next) ? String(next).trim() : '';
|
||
} catch {
|
||
if (!isValidAvatarUrl(customerAvatarRaw.value)) {
|
||
customerAvatarRaw.value = '';
|
||
}
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() =>
|
||
[
|
||
rbacUserStore.isLoggedIn,
|
||
rbacUserStore.loginPortalType,
|
||
rbacUserStore.accessToken,
|
||
] as const,
|
||
() => {
|
||
void loadCustomerAvatar();
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
function onCustomerProfileChanged() {
|
||
void loadCustomerAvatar();
|
||
}
|
||
|
||
onMounted(() => {
|
||
window.addEventListener(CUSTOMER_BALANCE_CHANGED_EVENT, onCustomerProfileChanged);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener(
|
||
CUSTOMER_BALANCE_CHANGED_EVENT,
|
||
onCustomerProfileChanged,
|
||
);
|
||
});
|
||
|
||
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 || t('profile.changePasswordFailed'));
|
||
} finally {
|
||
changePasswordLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function closeMenu() {
|
||
menuVisible.value = false;
|
||
}
|
||
|
||
function onPanelAction(key: string) {
|
||
// 切语言留在面板内,其它操作关闭
|
||
if (!key.startsWith('lang:')) {
|
||
closeMenu();
|
||
}
|
||
void onMenuClick({ key });
|
||
}
|
||
|
||
const onDropdownSelect = (value: string | number | Record<string, unknown> | undefined) => {
|
||
if (value === undefined || value === null) return;
|
||
onMenuClick({ key: String(value) });
|
||
};
|
||
|
||
const onMenuClick = async (event: { key: string }) => {
|
||
const { key } = event;
|
||
|
||
if (key === 'recharge') {
|
||
router.push({ name: 'VipRechargeDeposit' }).catch(() => undefined);
|
||
return;
|
||
}
|
||
|
||
if (key.startsWith('account:')) {
|
||
const path = key.slice('account:'.length);
|
||
const item = accountMenus.value.find(
|
||
(m) => getCustomerNavPath(m) === path || String(m.id || m.name) === path,
|
||
);
|
||
if (item) openAccountMenuItem(item);
|
||
return;
|
||
}
|
||
|
||
if (key.startsWith('lang:')) {
|
||
changeLocale(key.slice('lang:'.length));
|
||
return;
|
||
}
|
||
|
||
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') {
|
||
openPortalSideDrawer('profile');
|
||
} else if (key === 'changePassword') {
|
||
openChangePasswordModal();
|
||
} else if (key === 'settings') {
|
||
void appStore.openGlobalSettings();
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.user-dropdown-trigger {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
max-width: 180px;
|
||
height: 28px;
|
||
padding: 0 8px;
|
||
border-radius: 6px;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
color: inherit;
|
||
text-decoration: none;
|
||
transition: background-color 0.15s ease, opacity 0.15s ease;
|
||
}
|
||
|
||
.user-dropdown-trigger.is-compact {
|
||
max-width: none;
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
justify-content: center;
|
||
}
|
||
|
||
.user-dropdown-trigger:hover {
|
||
background-color: rgba(255, 255, 255, 0.12);
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
flex-shrink: 0;
|
||
background: rgba(255, 255, 255, 0.16);
|
||
}
|
||
|
||
.user-avatar--fallback {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--color-text-1, #1d2129);
|
||
background: var(--color-fill-3, #e5e6eb);
|
||
}
|
||
|
||
.user-name {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
max-width: 140px;
|
||
font-size: 13px;
|
||
line-height: 28px;
|
||
}
|
||
|
||
.user-menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 148px;
|
||
max-width: 220px;
|
||
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;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
.user-menu-item__label {
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
font-weight: 500;
|
||
text-align: left;
|
||
}
|
||
|
||
.user-menu-item__hint {
|
||
margin-left: 8px;
|
||
color: var(--app-primary-color, rgb(var(--primary-6)));
|
||
font-weight: 600;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
/* Cursor 风格:侧栏 dock 账户面板 */
|
||
.csr-account-dropdown {
|
||
width: 200px !important;
|
||
min-width: 200px !important;
|
||
max-width: 200px !important;
|
||
padding: 0 !important;
|
||
border: 1px solid var(--color-border-2, rgba(0, 0, 0, 0.08)) !important;
|
||
border-radius: 10px !important;
|
||
box-shadow:
|
||
0 8px 28px rgba(15, 23, 42, 0.12),
|
||
0 2px 8px rgba(15, 23, 42, 0.06) !important;
|
||
overflow: hidden;
|
||
background: #fff !important;
|
||
|
||
.arco-dropdown {
|
||
width: 100%;
|
||
min-width: 0 !important;
|
||
padding: 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.arco-dropdown-list-wrapper,
|
||
.arco-scrollbar,
|
||
.arco-dropdown-list {
|
||
max-height: none !important;
|
||
padding: 0 !important;
|
||
margin: 0 !important;
|
||
}
|
||
}
|
||
|
||
.csr-user-panel {
|
||
/* 与客户侧栏展开宽对齐 */
|
||
width: 200px;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #fff;
|
||
color: var(--color-text-1);
|
||
|
||
&__header {
|
||
padding: 10px 10px 8px;
|
||
border-bottom: 1px solid var(--color-border-2, rgba(0, 0, 0, 0.06));
|
||
}
|
||
|
||
&__top {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__identity {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__lang-toggle {
|
||
flex-shrink: 0;
|
||
min-width: 28px;
|
||
height: 26px;
|
||
padding: 0 4px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
color: var(--color-text-2);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
transition: color 0.12s ease, background-color 0.12s ease;
|
||
|
||
&:hover {
|
||
color: var(--app-primary-color, #1d7281);
|
||
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 6%, #fff);
|
||
}
|
||
}
|
||
|
||
&__avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
flex-shrink: 0;
|
||
background: var(--color-fill-3, #e5e6eb);
|
||
|
||
&--fallback {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--color-text-1);
|
||
}
|
||
}
|
||
|
||
&__meta {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
&__name {
|
||
font-size: 14px;
|
||
font-weight: 650;
|
||
line-height: 1.3;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__sub {
|
||
margin-top: 2px;
|
||
font-size: 12px;
|
||
color: var(--color-text-3);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__cta {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 6px;
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
height: 30px;
|
||
padding: 0 10px;
|
||
border: 1px solid color-mix(in srgb, var(--app-primary-color, #1d7281) 28%, transparent);
|
||
border-radius: 6px;
|
||
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 8%, #fff);
|
||
color: var(--app-primary-color, #1d7281);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background-color 0.14s ease;
|
||
|
||
&:hover {
|
||
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 14%, #fff);
|
||
}
|
||
}
|
||
|
||
&__cta-balance {
|
||
font-variant-numeric: tabular-nums;
|
||
font-weight: 700;
|
||
}
|
||
|
||
&__body {
|
||
padding: 6px;
|
||
max-height: min(52vh, 360px);
|
||
overflow: auto;
|
||
}
|
||
|
||
&__footer {
|
||
padding: 6px;
|
||
border-top: 1px solid var(--color-border-2, rgba(0, 0, 0, 0.06));
|
||
background: color-mix(in srgb, var(--color-fill-2, #f2f3f5) 55%, #fff);
|
||
}
|
||
|
||
&__item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
width: 100%;
|
||
min-height: 36px;
|
||
padding: 0 10px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: var(--color-text-1);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: background-color 0.12s ease;
|
||
|
||
&:hover {
|
||
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 8%, #fff);
|
||
}
|
||
|
||
&.is-danger {
|
||
color: rgb(var(--red-6));
|
||
|
||
.csr-user-panel__item-icon {
|
||
color: rgb(var(--red-6));
|
||
}
|
||
}
|
||
}
|
||
|
||
&__item-icon {
|
||
width: 18px;
|
||
flex-shrink: 0;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
color: var(--color-text-3);
|
||
}
|
||
|
||
&__item-label {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__hint {
|
||
margin-left: 8px;
|
||
color: var(--app-primary-color, rgb(var(--primary-6)));
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.csr-dock-dropdown {
|
||
.arco-dropdown-option {
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.arco-dropdown-option-content {
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
}
|
||
}
|
||
</style>
|