front/src/components/top-nav/index.vue
zzh c4979a1fb4 feat: 面单标签设计与仓内操作页优化,接通模板打印链路
完善标签设计器/销售产品模板绑定与打印面单 UX,统一入库出库到仓扫码页布局,并补充库存绑定与相关文档配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 17:49:18 +08:00

994 lines
29 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="navLogo" alt="ID logo" />
</div>
<!-- 顶部水平菜单 -->
<div class="top-nav-menu" ref="topMenuCon">
<template v-if="topNavEnable">
<template v-for="(item, key) in menuData" :key="key">
<a-trigger
v-if="hasChildren(item) && menuDataReady"
:key="`nav-sub-${key}`"
trigger="hover"
position="bl"
:popup-offset="0"
:mouse-enter-delay="80"
:mouse-leave-delay="120"
unmount-on-close
>
<span
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: MouseEvent) => {
e.preventDefault();
if (!(item.children && item.children.length > 0)) {
goto(item);
}
}"
>
{{ formatMenuTitle(item.name || item.meta?.title) }}
</span>
<template #content>
<div class="nav-submenu-panel">
<div class="nav-submenu-panel__bridge" aria-hidden="true" />
<div class="nav-submenu-panel__title">
{{ formatMenuTitle(item.name || item.meta?.title) }}
</div>
<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="() => gotoFromSubmenu(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) }}
</span>
</div>
</div>
</div>
</div>
</template>
</a-trigger>
<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 { useTabBarStore } from '@/store';
import { useRbacUserStore } from '@/store/modules/rbac-user';
import TabBar from '@/components/tab-bar/index.vue';
import RightTopUser from './RightTopUser.vue';
import RightTopMessage from './RightTopMessage.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 navLogo from '@/assets/500x500.png';
import {
initHiprintClientState,
setHiprintClientEnabled,
useHiprintClientState,
} from '@/utils/hiprint/hiprintClientState';
import { getMapIdFromAccessToken } from '@/utils/jwt';
import { getSubMenuIcon } 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 tabBarStore = useTabBarStore();
const rbacUserStore = useRbacUserStore();
const topMenuCon = ref<HTMLElement | null>(null);
const topNavEnable = computed(() => true); // 启用顶部导航
const tabNavEnable = computed(() => appStore.tabBar);
const printConnectionState = useHiprintClientState();
// Tab 切换时不再 remount 顶栏菜单(此前 submenuCloseEpoch 是顿挫主因之一)
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;
});
}
// 菜单数据 - 优先用用户 menus 立即渲染;全量树到位后按权限过滤
const menuData = computed(() => {
const userMenus = userMenuTreeData.value;
if (userMenus.length === 0) {
return [];
}
if (allMenuTreeData.value.length > 0) {
const filteredMenus = filterMenuByUserPermission(
allMenuTreeData.value,
userMenus,
);
return convertMenuTreeToNavMenu(filteredMenus);
}
return convertMenuTreeToNavMenu(userMenus);
});
// 加载菜单树和用户菜单权限
async function loadMenuTree() {
menuLoading.value = true;
try {
if (!rbacUserStore.isLoggedIn) {
allMenuTreeData.value = [];
menuDataReady.value = true;
return;
}
const currentUserMenus = rbacUserStore.menus;
if (currentUserMenus && currentUserMenus.length > 0) {
menuDataReady.value = true;
}
if (!currentUserMenus || currentUserMenus.length === 0) {
try {
await rbacUserStore.getCurrentUser();
if (rbacUserStore.menus?.length) {
menuDataReady.value = true;
}
} catch (userError) {
console.error('获取用户信息失败:', userError);
}
}
try {
const allMenus = await MenuService.getRbacMenuGetMenuTree() || [];
allMenuTreeData.value = allMenus;
await nextTick();
menuDataReady.value = true;
tabBarStore.refreshTagTitles();
} catch (menuError) {
console.error('加载全量菜单树失败:', menuError);
allMenuTreeData.value = [];
await nextTick();
menuDataReady.value = true;
tabBarStore.refreshTagTitles();
}
} 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 '';
});
// 顶部菜单标题:直接使用 RBAC 菜单返回的名称
const formatMenuTitle = (title?: string) => title?.trim() || '';
// 中文菜单名称到 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.order.waybillManage',
'渠道': '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',
'快递到仓': 'menu.operation.parcelArrival',
'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',
};
// 过滤页面 - 只显示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,
adminPortalOnly: item.meta?.adminPortalOnly,
})
) {
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,
adminPortalOnly: child.meta?.adminPortalOnly,
})
) {
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',
// 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 gotoFromSubmenu = (item: any) => {
goto(item);
};
// 当前路由所属的顶部菜单
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: 36px;
object-fit: contain;
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;
}
}
.nav-submenu-panel {
position: relative;
min-width: 600px;
max-width: 720px;
padding: 12px 16px 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
/* 填补触发条与面板之间的空隙,鼠标移入二级时不会断 hover */
.nav-submenu-panel__bridge {
position: absolute;
top: -12px;
left: 0;
right: 0;
height: 12px;
}
.nav-submenu-panel__title {
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--color-border-2, #e5e6eb);
font-size: 14px;
font-weight: 600;
color: var(--color-text-1, #1d2129);
}
.child-window {
width: 100%;
}
.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>