front/src/components/top-nav/RightTopUser.vue
zzh 3642d650db
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m59s
feat: 客户侧栏账户坞与运单发货后状态变更
侧栏改为 Cursor 式账户面板(仓库眼标预览、语言切换、假邮箱过滤),运单「更多」支持发货后四态变更且妥投成功仅可进入。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 11:46:32 +08:00

918 lines
25 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>
<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>