front/src/components/top-nav/index.vue
zzh b87301e89d fix: 按轨迹新手指引模式补齐订单/渠道指引菜单与路由映射
generate-menu-excel、vite dev SPA 白名单、top-nav path 映射与 locale 区分同名「新手指引」。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 15:34:48 +08:00

969 lines
28 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 id="top-nav" class="top-nav">
<div class="top-nav-header">
<!-- Logo 区域 -->
<div class="top-nav-logo">
<img src="/src/assets/logo.png" alt="logo" />
</div>
<!-- 顶部水平菜单 -->
<div class="top-nav-menu" ref="topMenuCon">
<template v-if="topNavEnable">
<template v-for="(item, key) in menuData" :key="key">
<a-popover
v-if="hasChildren(item) && menuDataReady"
placement="bottomLeft"
:mouseEnterDelay="0.3"
>
<template #content>
<div class="child-window">
<div
v-for="(row, rowIndex) in getMenuRows(filterPage(item.children))"
:key="rowIndex"
class="child-window-item"
>
<div
v-for="(menuItem, childItem) in row"
:key="childItem"
class="child-menu-item"
@click="() => goto(menuItem)"
>
<span class="child-menu-item__icon">
<component :is="getSubMenuIcon(menuItem, item, menuNameToLocaleKeyMap)" />
</span>
<span class="child-menu-item__text">
{{ formatMenuTitle(menuItem.name || menuItem.meta?.title, menuItem.path || menuItem.route) }}
</span>
</div>
</div>
</div>
</template>
<template #title>
<span>{{ formatMenuTitle(item.name || item.meta?.title) }}</span>
</template>
<span
:key="key"
v-if="!item.meta?.isHide"
:class="{'active': belongTopMenu === (item.path || item.route) || (item.path && belongTopMenu.startsWith(item.path)) }"
class="top-nav-menu-item"
@click="(e) => {
e.preventDefault();
if (item.children && item.children.length > 0) {
// 有子菜单时不跳转,只显示弹出层
return;
} else {
// 如果没有子菜单,执行跳转
goto(item);
}
}"
>
{{ formatMenuTitle(item.name || item.meta?.title) }}
</span>
</a-popover>
<span
v-else
:key="key"
v-if="!item.meta?.isHide"
:class="{'active': belongTopMenu === (item.path || item.route) || (item.path && belongTopMenu.startsWith(item.path)) }"
class="top-nav-menu-item"
@click="(e) => {
e.preventDefault();
goto(item);
}"
>
{{ formatMenuTitle(item.name || item.meta?.title) }}
</span>
</template>
</template>
</div>
<!-- 右侧操作区 -->
<div class="top-nav-right">
<div class="top-nav-right-item">
<a class="print-connection-trigger" @click="e => e.preventDefault()">
<icon-printer class="print-connection-icon" />
<a-switch
size="small"
:model-value="printConnectionState.enabled"
:loading="printConnectionState.connecting"
@change="onTogglePrintConnection"
/>
<span
class="print-connection-state"
:class="{ 'print-connection-state--enabled': printConnectionState.enabled }"
>
{{ printConnectionState.enabled ? '已连接' : '未连接' }}
</span>
</a>
</div>
<!-- 联系客服 -->
<div class="top-nav-right-item">
<ContactService variant="navbar" />
</div>
<!-- 余额(仅客户门户显示) -->
<div v-if="showBalanceNav" class="top-nav-right-item">
<a-dropdown trigger="hover" placement="bottomRight">
<div class="balance-dropdown-trigger">
<span class="balance-label">{{ t('nav.balance.label') }}:</span>
<span class="balance-value">{{ balanceDisplay }}</span>
</div>
<template #content>
<a-doption @click="() => onBalanceMenuClick({ key: 'recharge' })">
{{ t('nav.balance.recharge') }}
</a-doption>
<a-doption @click="() => onBalanceMenuClick({ key: 'rechargeRecord' })">
{{ t('nav.balance.rechargeRecord') }}
</a-doption>
<a-doption @click="() => onBalanceMenuClick({ key: 'accountFlow' })">
{{ t('nav.balance.accountFlow') }}
</a-doption>
</template>
</a-dropdown>
</div>
<!-- 语言 -->
<div class="top-nav-right-item">
<SelectLang />
</div>
<!-- 消息 -->
<div class="top-nav-right-item">
<RightTopMessage />
</div>
<!-- 用户 -->
<div class="top-nav-right-item user-menu-item">
<RightTopUser />
</div>
</div>
</div>
<!-- Tab 导航栏 -->
<div v-if="tabNavEnable" class="top-nav-tabs">
<TabBar />
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAppStore } from '@/store';
import { useRbacUserStore } from '@/store/modules/rbac-user';
import TabBar from '@/components/tab-bar/index.vue';
import RightTopMessage from './RightTopMessage.vue';
import RightTopUser from './RightTopUser.vue';
import SelectLang from './SelectLang.vue';
import ContactService from '@/components/ContactService/index.vue';
import { IconPrinter } from '@arco-design/web-vue/es/icon';
import { Message } from '@arco-design/web-vue';
import { openWindow, regexUrl } from '@/utils';
import { MenuService, AuthService } from '@/api/rbac';
import type { MenuTreeDto } from '@/api/rbac';
import {
initHiprintClientState,
setHiprintClientEnabled,
useHiprintClientState,
} from '@/utils/hiprint/hiprintClientState';
import { getMapIdFromAccessToken } from '@/utils/jwt';
import { getSubMenuIcon, PATH_TO_LOCALE_KEY } from '@/utils/menu-icon';
import { canAccessCustomerPortalRoute } from '@/utils/login-portal';
import { resolveCurrentCustomer } from '@/api/customer/resolve';
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const rbacUserStore = useRbacUserStore();
const topMenuCon = ref<HTMLElement | null>(null);
const topNavEnable = computed(() => true); // 启用顶部导航
const tabNavEnable = computed(() => appStore.tabBar);
const printConnectionState = useHiprintClientState();
async function onTogglePrintConnection(value: string | number | boolean) {
try {
await setHiprintClientEnabled(!!value);
} catch (e: any) {
Message.warning(`连接失败:${e?.message || '请先启动 hiprint 客户端'}`);
}
}
// 菜单数据 - 全量菜单树和用户菜单权限
const allMenuTreeData = ref<MenuTreeDto[]>([]);
const menuLoading = ref(false);
const menuDataReady = ref(false);
// 使用 store 中的用户菜单权限(computed,自动响应式)
const userMenuTreeData = computed(() => rbacUserStore.menus || []);
// 根据用户菜单权限过滤菜单树(按菜单 Id + 权限码双重匹配,避免子菜单丢失)
function filterMenuByUserPermission(allMenus: MenuTreeDto[], userMenus: MenuTreeDto[]): MenuTreeDto[] {
const userMenuIdSet = new Set<string>();
const userPermissionSet = new Set(
(rbacUserStore.permissions || []).map((p) => String(p).toLowerCase()),
);
function collectMenuIds(menus: MenuTreeDto[]) {
menus.forEach(menu => {
if (menu.id) {
userMenuIdSet.add(String(menu.id));
}
if (menu.children && menu.children.length > 0) {
collectMenuIds(menu.children);
}
});
}
collectMenuIds(userMenus);
function hasMenuPermission(menu: MenuTreeDto): boolean {
const menuId = menu.id ? String(menu.id) : '';
if (menuId && userMenuIdSet.has(menuId)) {
return true;
}
const code = menu.permissionCode?.trim();
if (code && userPermissionSet.has(code.toLowerCase())) {
return true;
}
return false;
}
function filterMenus(menus: MenuTreeDto[]): MenuTreeDto[] {
const result: MenuTreeDto[] = [];
for (const menu of menus) {
let filteredChildren: MenuTreeDto[] | null = null;
if (menu.children && menu.children.length > 0) {
filteredChildren = filterMenus(menu.children);
}
const allowed = hasMenuPermission(menu)
|| (filteredChildren != null && filteredChildren.length > 0);
if (allowed) {
result.push({
...menu,
children: filteredChildren && filteredChildren.length > 0 ? filteredChildren : null,
});
}
}
return result;
}
return filterMenus(allMenus);
}
// 将 MenuTreeDto 转换为导航栏菜单格式
function convertMenuTreeToNavMenu(menuTree: MenuTreeDto[]): any[] {
return menuTree
.filter(menu => {
// 只显示一级菜单(type === 1 或没有 type)
if (menu.type === 2) return false; // 过滤按钮
if (menu.isHidden) return false; // 过滤隐藏菜单
return true;
})
.map(menu => {
const navMenu: any = {
id: menu.id,
name: menu.name,
path: menu.route,
route: menu.route,
type: menu.type,
meta: {
title: menu.name,
icon: menu.icon,
order: menu.order,
isHide: menu.isHidden || false,
},
};
// 只保留二级菜单,移除三级及更深层级
if (menu.children && menu.children.length > 0) {
navMenu.children = menu.children
.filter((child: MenuTreeDto) => {
if (child.type === 2) return false; // 过滤按钮
if (child.isHidden) return false; // 过滤隐藏菜单
return true;
})
.map((child: MenuTreeDto) => {
// 移除二级菜单的 children
return {
id: child.id,
name: child.name,
path: child.route,
route: child.route,
type: child.type,
meta: {
title: child.name,
icon: child.icon,
order: child.order,
isHide: child.isHidden || false,
},
};
});
}
return navMenu;
});
}
// 菜单数据 - 根据用户权限过滤后的菜单列表
const menuData = computed(() => {
const userMenus = userMenuTreeData.value;
// 如果全量菜单树为空,返回空数组
if (allMenuTreeData.value.length === 0) {
return [];
}
// 如果用户菜单权限为空,返回空数组
if (userMenus.length === 0) {
return [];
}
// 根据用户菜单权限过滤全量菜单树
const filteredMenus = filterMenuByUserPermission(
allMenuTreeData.value,
userMenus
);
// 转换为导航栏格式
return convertMenuTreeToNavMenu(filteredMenus);
});
// 加载菜单树和用户菜单权限
async function loadMenuTree() {
menuLoading.value = true;
menuDataReady.value = false;
try {
// 先检查用户是否已登录
if (!rbacUserStore.isLoggedIn) {
allMenuTreeData.value = [];
menuLoading.value = false;
menuDataReady.value = true;
return;
}
// 确保用户信息已加载(如果菜单为空,重新获取)
const currentUserMenus = rbacUserStore.menus;
if (!currentUserMenus || currentUserMenus.length === 0) {
try {
await rbacUserStore.getCurrentUser();
} catch (userError) {
console.error('获取用户信息失败:', userError);
}
}
// 加载全量菜单树
try {
const allMenus = await MenuService.getRbacMenuGetMenuTree() || [];
allMenuTreeData.value = allMenus;
// 等待渲染完成后再标记数据就绪
await nextTick();
menuDataReady.value = true;
} catch (menuError) {
console.error('加载全量菜单树失败:', menuError);
allMenuTreeData.value = [];
await nextTick();
menuDataReady.value = true;
}
} catch (error) {
console.error('加载菜单树或用户信息失败:', error);
allMenuTreeData.value = [];
await nextTick();
menuDataReady.value = true;
} finally {
menuLoading.value = false;
}
}
// 当前路由所属的顶部菜单
const belongTopMenu = computed(() => {
const currentPath = route.path;
// 查找当前路由所属的顶级菜单
for (const menu of menuData.value) {
const menuPath = menu.path || menu.route;
if (menuPath && currentPath.startsWith(menuPath)) {
return menuPath;
}
// 检查子菜单
if (menu.children) {
for (const child of menu.children) {
const childPath = child.path || child.route;
if (childPath && currentPath.startsWith(childPath)) {
return menuPath || '';
}
}
}
}
return '';
});
// 路径 → locale(同名菜单如「新手指引」须按 path 区分)
const pathToLocaleKeyMap = PATH_TO_LOCALE_KEY;
// 中文菜单名称到 locale key 的映射表
const menuNameToLocaleKeyMap: Record<string, string> = {
'首页': 'menu.home',
'客户': 'menu.user',
'客户管理': 'menu.user.customer',
'个人中心': 'menu.user.profile',
'资金充值': 'menu.user.recharge',
'财务': 'menu.finance',
'充值记录': 'menu.finance.rechargeRecord',
'资金流水': 'menu.finance.accountFlow',
'应收应付': 'menu.finance.receivablePayable',
'订单': 'menu.order',
'我要查价': 'menu.order.priceCheck',
'我要下单': 'menu.order.placeOrder',
'我的订单': 'menu.order.myOrders',
'渠道': 'menu.channel',
'销售产品': 'menu.channel.salesProducts',
'服务渠道': 'menu.channel.serviceChannels',
'价格分区': 'menu.channel.priceArea',
'黑名单区': 'menu.channel.blacklist',
'偏远分区': 'menu.channel.remoteZones',
'计重规则': 'menu.channel.weightRules',
'计重进位': 'menu.channel.weightRounding',
'自动触发': 'menu.channel.autoTrigger',
'问题模板': 'menu.channel.issueTemplate',
'轨迹': 'menu.track',
'新手指引': 'menu.track.trackGuide',
'轨迹订单': 'menu.track.trackOrder',
'预警订单': 'menu.track.warningOrder',
'轨迹状态': 'menu.track.trackState',
'轨迹标记': 'menu.track.trackFlag',
'轨迹内容': 'menu.track.trackContent',
'轨迹拉取': 'menu.track.trackPull',
'轨迹推送': 'menu.track.trackPush',
'预警分区': 'menu.track.kpiRegion',
'预警配置': 'menu.track.kpiEarlyWarningConfig',
'操作': 'menu.operation',
'入库扫描': 'menu.operation.inboundScan',
'出库扫描': 'menu.operation.outboundScan',
'打印标签': 'menu.operation.printLabel',
'API': 'menu.api',
'上游接口': 'menu.api.upstreamInterface',
'下游接口': 'menu.api.downstreamInterface',
'创建接口': 'menu.api.createInterface',
'修改接口': 'menu.api.editInterface',
'App列表': 'menu.api.appList',
'App管理': 'menu.api.appList',
'应用管理': 'menu.api.appList',
'请求日志': 'menu.api.requestLog',
'调用统计': 'menu.api.callStatistics',
'创建应用': 'menu.api.createApplication',
'API调试': 'menu.api.apiDebugger',
'API LIKE': 'menu.api.apiLike',
'API测试': 'menu.api.apiTest',
'系统': 'menu.system',
'组织管理': 'menu.system.organization',
'用户管理': 'menu.system.user',
'职位管理': 'menu.system.position',
'角色管理': 'menu.system.role',
'模块管理': 'menu.system.module',
'菜单管理': 'menu.system.menu',
'仓库地址': 'menu.system.warehouseAddress',
'其他配置': 'menu.system.otherConfig',
'小红书': 'menu.xhs',
'启动服务': 'menu.xhs.startService',
'内容分析': 'menu.xhs.analysis',
'爆款创作': 'menu.xhs.creation',
'发布记录': 'menu.xhs.publishHistory',
'技能卡片': 'menu.xhs.skills',
'拟人设置': 'menu.xhs.humanSettings',
'AI 助手': 'menu.system.xhsAiAssistant',
'数据权限': 'menu.system.dataPerm',
'用户数据权限': 'menu.system.dataPermUsers',
'权限模板': 'menu.system.dataPermTemplates',
'权限审计': 'menu.system.dataPermAudit',
};
// 格式化菜单标题
const formatMenuTitle = (title?: string, path?: string) => {
if (!title) return '';
const routePath = path?.trim();
if (routePath && pathToLocaleKeyMap[routePath]) {
return t(pathToLocaleKeyMap[routePath]) || title;
}
// 如果是 locale key,直接翻译
if (title.startsWith('menu.')) {
return t(title) || title;
}
// 如果是中文菜单名称,查找对应的 locale key
if (menuNameToLocaleKeyMap[title]) {
return t(menuNameToLocaleKeyMap[title]) || title;
}
// 否则直接返回原文本
return title;
};
// 过滤页面 - 只显示2级菜单,忽略3级菜单
const filterPage = (pages?: any[]) => {
if (!pages) return [];
return pages
.filter(item => {
// 过滤掉隐藏的菜单
if (item.meta?.isHide || item.meta?.hideInMenu) return false;
// 过滤掉按钮类型的菜单
if (item.type === 2) return false;
const itemPath = String(item.path || item.route || '');
if (
!canAccessCustomerPortalRoute(rbacUserStore.loginPortalType, {
path: itemPath,
customerPortalOnly: item.meta?.customerPortalOnly,
})
) {
return false;
}
if (
!rbacUserStore.isCustomerPortal() &&
(item.name === 'UserProfile' || item.title === '个人中心')
) {
return false;
}
return true;
})
.map(item => {
// 确保二级菜单不包含 children(三级菜单)
const { children, ...itemWithoutChildren } = item;
return itemWithoutChildren;
});
};
// 检查是否有子菜单
const hasChildren = (item: any) => {
if (!item.children || item.children.length === 0) return false;
// 检查是否有可见的子菜单
return item.children.some((child: any) => {
if (child.meta?.isHide || child.meta?.hideInMenu) return false;
if (child.type === 2) return false; // 过滤按钮
const childPath = String(child.path || child.route || '');
if (
!canAccessCustomerPortalRoute(rbacUserStore.loginPortalType, {
path: childPath,
customerPortalOnly: child.meta?.customerPortalOnly,
})
) {
return false;
}
if (
!rbacUserStore.isCustomerPortal() &&
(child.name === 'UserProfile' || child.title === '个人中心')
) {
return false;
}
return true;
});
};
// 将菜单项按每行4个分组
const getMenuRows = (items: any[]) => {
const rows: any[][] = [];
for (let i = 0; i < items.length; i += 4) {
rows.push(items.slice(i, i + 4));
}
return rows;
};
// 路径到路由名称的映射(用于回退,避免使用中文显示名称)
const pathToRouteNameMap: Record<string, string> = {
'/channel/salesProducts': 'SalesProducts',
'/channel/serviceChannels': 'ServiceChannels',
'/channel/priceArea': 'PriceArea',
'/channel/blacklist': 'Blacklist',
'/channel/remoteZones': 'RemoteZones',
'/channel/weightRules': 'WeightRules',
'/channel/weightRounding': 'WeightRounding',
'/channel/autoTrigger': 'AutoTrigger',
'/channel/issueTemplate': 'IssueTemplate',
'/channel/guide': 'ChannelGuide',
'/order/guide': 'OrderGuide',
'/order/priceCheck': 'PriceCheck',
'/order/placeOrder': 'PlaceOrder',
'/order/myOrders': 'MyOrders',
'/order/batchJobs': 'OrderBatchJobs',
// Track 相关路由
'/track/guide': 'TrackGuide',
'/track/order': 'TrackOrder',
'/track/warningOrder': 'TrackWarningOrder',
'/track/state': 'TrackState',
'/track/flag': 'TrackFlag',
'/track/content': 'TrackContent',
'/track/pull': 'TrackPull',
'/track/push': 'TrackPush',
'/track/kpiRegion': 'KpiRegion',
'/track/kpiEarlyWarningConfig': 'KpiEarlyWarningConfig',
'/xhs/startService': 'XhsStartService',
'/xhs/analysis': 'XhsAnalysis',
'/xhs/creation': 'XhsCreation',
'/xhs/publishHistory': 'XhsPublishHistory',
'/xhs/skills': 'XhsSkills',
'/xhs/humanSettings': 'XhsHumanSettings',
};
// 跳转
const goto = (item: any) => {
if (!item) {
return;
}
// 获取路径,优先使用 path,其次使用 route
let targetPath = item.path || item.route;
if (!targetPath) {
return;
}
// 确保路径以 / 开头
if (!targetPath.startsWith('/') && !regexUrl.test(targetPath)) {
targetPath = '/' + targetPath;
}
// 如果是外部链接,使用 openWindow
if (regexUrl.test(targetPath)) {
openWindow(targetPath);
return;
}
// 使用路由 path 跳转
router.push(targetPath).catch((error) => {
console.error(`路由跳转失败: path=${targetPath}`, error);
// 尝试使用路由名称跳转(从映射中获取,而不是使用 item.name,因为 item.name 是中文显示名称)
const routeName = pathToRouteNameMap[targetPath];
if (routeName) {
router.push({ name: routeName }).catch((nameError) => {
console.error(`使用路由名称跳转也失败: name=${routeName}`, nameError);
});
} else {
// 如果没有映射,尝试使用 item.name(可能是路由名称,也可能是显示名称)
// 但这种情况应该尽量避免,因为 item.name 通常是显示名称
if (item.name && /^[A-Z]/.test(item.name)) {
// 如果 item.name 以大写字母开头,可能是路由名称
router.push({ name: item.name }).catch((nameError) => {
console.error(`使用路由名称跳转也失败: name=${item.name}`, nameError);
});
}
}
});
};
// 当前路由所属的顶部菜单
const showBalanceNav = computed(() => rbacUserStore.isCustomerPortal());
const customerBalance = ref<number | null>(null);
const balanceDisplay = computed(() => {
const raw = customerBalance.value;
if (raw == null) {
return '0.00';
}
const parsed = Number(raw);
return Number.isFinite(parsed) ? parsed.toFixed(2) : '0.00';
});
async function loadCustomerBalance() {
if (!rbacUserStore.isCustomerPortal() || !rbacUserStore.isLoggedIn) {
customerBalance.value = null;
return;
}
try {
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
const account = rbacUserStore.userInfo?.account?.trim() || '';
const data = await resolveCurrentCustomer({ mapId, account });
const raw = data?.accountBalance ?? data?.balance;
if (raw == null || raw === '') {
customerBalance.value = 0;
return;
}
const parsed = Number(raw);
customerBalance.value = Number.isFinite(parsed) ? parsed : 0;
} catch (error) {
console.error('Failed to load customer balance', error);
customerBalance.value = 0;
}
}
// 点击余额菜单项
const onBalanceMenuClick = (event: any) => {
const { key } = event;
if (key === 'recharge') {
router.push({ name: 'VipRechargeDeposit' });
} else if (key === 'rechargeRecord') {
router.push({ name: 'FinanceRechargeRecord' });
} else if (key === 'accountFlow') {
router.push({ name: 'FinanceAccountFlow' });
}
};
watch(
() => [rbacUserStore.isLoggedIn, rbacUserStore.loginPortalType, rbacUserStore.accessToken] as const,
() => {
void loadCustomerBalance();
},
{ immediate: true }
);
function handleCustomerBalanceChanged() {
void loadCustomerBalance();
}
onMounted(async () => {
window.addEventListener(CUSTOMER_BALANCE_CHANGED_EVENT, handleCustomerBalanceChanged);
initHiprintClientState().catch(() => undefined);
// 加载菜单树
await loadMenuTree();
});
onUnmounted(() => {
window.removeEventListener(CUSTOMER_BALANCE_CHANGED_EVENT, handleCustomerBalanceChanged);
});
</script>
<style lang="less" scoped>
@headerHeight: 50px;
@headerTabNavHeight: 36px;
.top-nav {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: (@headerHeight + @headerTabNavHeight);
box-shadow: 0 1px 4px rgba(114, 115, 116, 0.08);
z-index: 100;
background: var(--app-navbar-bg);
.top-nav-header {
display: flex;
width: 100%;
height: @headerHeight;
background: var(--app-navbar-bg) !important;
background-color: var(--app-navbar-bg) !important;
color: #fff;
align-items: center;
.top-nav-logo {
display: flex;
align-items: center;
padding: 0 16px;
flex-shrink: 0;
img {
height: 36px;
width: auto;
display: block;
}
}
.top-nav-menu {
height: @headerHeight;
line-height: @headerHeight;
flex: 1;
overflow: hidden;
overflow-x: auto;
display: flex;
align-items: center;
.top-nav-menu-item {
display: inline-block;
padding: 6px 20px;
height: auto;
line-height: 1.5;
text-decoration: none;
color: #fff;
font-size: 16px;
border-radius: 20px;
white-space: nowrap;
transition: all 0.3s ease;
margin: 0 16px;
cursor: pointer;
&:hover,
&.active {
background-color: rgba(255, 255, 255, 0.2);
color: #fff;
}
}
}
.top-nav-right {
display: flex;
align-items: center;
white-space: nowrap;
height: @headerHeight;
vertical-align: middle;
.top-nav-right-item {
display: flex;
align-items: center;
height: 100%;
padding: 0 16px;
position: relative;
&:not(:first-child)::before {
content: '';
position: absolute;
left: 0;
top: 25%;
width: 1px;
height: 50%;
background-color: rgba(255, 255, 255, 0.2);
}
}
.print-connection-trigger {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
cursor: pointer;
color: inherit;
text-decoration: none;
transition: all 0.3s;
&:hover {
opacity: 0.8;
}
}
.print-connection-icon {
font-size: 16px;
}
.print-connection-state {
font-size: 12px;
color: rgba(255, 255, 255, 0.75);
}
.print-connection-state--enabled {
color: #22c55e;
}
.balance-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;
font-weight: 500;
font-size: 16px;
&:hover {
opacity: 0.8;
}
.balance-label {
margin-right: 4px;
}
.balance-value {
color: #fff;
font-size: 16px;
}
}
.user-menu-item {
margin-left: auto;
}
}
}
.top-nav-tabs {
height: @headerTabNavHeight;
background: #fff;
border-bottom: 1px solid #e5e6eb;
}
&.tabNavEnable {
height: (@headerHeight + @headerTabNavHeight);
}
&:not(.tabNavEnable) {
height: @headerHeight;
}
}
.child-window {
width: 600px;
}
.child-window-item {
border-bottom: 1px dashed #eee;
padding-bottom: 1rem;
margin-bottom: 1rem;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem 1rem;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
}
.child-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
color: var(--color-text-1);
transition: color 0.2s ease, background-color 0.2s ease;
&:hover {
color: rgb(var(--primary-6));
background-color: var(--color-fill-2);
.child-menu-item__icon {
color: rgb(var(--primary-6));
}
}
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 16px;
height: 16px;
font-size: 16px;
color: var(--color-text-3);
transition: color 0.2s ease;
}
&__text {
line-height: 1.4;
font-size: 14px;
word-break: break-all;
}
}
// 滚动条样式
.top-nav-menu {
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.5);
}
}
</style>