完善标签设计器/销售产品模板绑定与打印面单 UX,统一入库出库到仓扫码页布局,并补充库存绑定与相关文档配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
994 lines
29 KiB
Vue
994 lines
29 KiB
Vue
<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>
|