851 lines
26 KiB
Vue
851 lines
26 KiB
Vue
<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="cursor-pointer hover:text-blue-600 text-gray-800"
|
||
@click="() => goto(menuItem)"
|
||
>
|
||
{{ formatMenuTitle(menuItem.name || menuItem.meta?.title) }}
|
||
</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-popover placement="bottomLeft" :mouseEnterDelay="0.3">
|
||
<template #content>
|
||
<div class="contact-content p-3" style="width: 280px;">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="text-lg font-semibold text-gray-800">{{ t('nav.contactService.title') }}</h3>
|
||
</div>
|
||
<div class="mb-4">
|
||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('nav.contactService.selectWarehouse') }}</label>
|
||
<a-select
|
||
v-model:value="serviceWarehouseSelect"
|
||
:options="serviceWarehouseSelectData"
|
||
style="width: 100%"
|
||
:getPopupContainer="trigger => trigger?.parentElement || document.body"
|
||
></a-select>
|
||
</div>
|
||
<div class="space-y-4">
|
||
<div class="warehouse-detail">
|
||
<div class="mb-3">
|
||
<h4 class="text-sm font-medium text-gray-500 mb-1">{{ t('nav.contactService.whseName') }}</h4>
|
||
<p class="text-gray-800 font-medium">{{ serviceIsWarehouse.whseName || '--' }}</p>
|
||
</div>
|
||
<div class="mb-3" v-if="serviceIsWarehouse.serviceWechat">
|
||
<h4 class="text-sm font-medium text-gray-500 mb-2">{{ t('nav.contactService.wechatQrcode') }}</h4>
|
||
<div class="border-2 border-dashed rounded-xl mx-auto p-2 flex items-center justify-center">
|
||
<img :src="MixinTransitionUrl(serviceIsWarehouse.serviceWechat)" width="128" height="128" alt="微信二维码" />
|
||
</div>
|
||
</div>
|
||
<div class="mb-3" v-if="serviceIsWarehouse.servicePhone">
|
||
<h4 class="text-sm font-medium text-gray-500 mb-1">{{ t('nav.contactService.phone') }}</h4>
|
||
<div class="flex items-center">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-500 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||
</svg>
|
||
<span class="text-gray-800 font-medium">{{ serviceIsWarehouse.servicePhone }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="text-xs text-gray-500 mt-4 pt-4 border-t border-gray-200">
|
||
<p>{{ t('nav.contactService.workingTime') }}</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<a class="service-button-trigger" @click="e => e.preventDefault()">
|
||
<i class="fa fa-weixin text-green-500" aria-hidden="true"></i>
|
||
<span>{{ t('nav.contactService.contactUs') }}</span>
|
||
</a>
|
||
</a-popover>
|
||
</div>
|
||
|
||
<!-- 余额 -->
|
||
<div 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>
|
||
</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, watch, nextTick } from 'vue';
|
||
import { useRoute, useRouter } from 'vue-router';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { useAppStore, useUserStore } 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 { openWindow, regexUrl } from '@/utils';
|
||
import { MenuService, AuthService } from '@/api/rbac';
|
||
import type { MenuTreeDto } from '@/api/rbac';
|
||
|
||
const { t } = useI18n();
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const appStore = useAppStore();
|
||
const userStore = useUserStore();
|
||
const rbacUserStore = useRbacUserStore();
|
||
|
||
const topMenuCon = ref<HTMLElement | null>(null);
|
||
const topNavEnable = computed(() => true); // 启用顶部导航
|
||
const tabNavEnable = computed(() => appStore.tabBar);
|
||
|
||
// 菜单数据 - 全量菜单树和用户菜单权限
|
||
const allMenuTreeData = ref<MenuTreeDto[]>([]);
|
||
const menuLoading = ref(false);
|
||
const menuDataReady = ref(false);
|
||
|
||
// 使用 store 中的用户菜单权限(computed,自动响应式)
|
||
const userMenuTreeData = computed(() => rbacUserStore.menus || []);
|
||
|
||
// 根据用户菜单权限过滤菜单树
|
||
function filterMenuByUserPermission(allMenus: MenuTreeDto[], userMenus: MenuTreeDto[]): MenuTreeDto[] {
|
||
// 创建用户菜单ID集合,用于快速查找
|
||
const userMenuIdSet = new Set<string>();
|
||
|
||
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 filterMenus(menus: MenuTreeDto[]): MenuTreeDto[] {
|
||
const result: MenuTreeDto[] = [];
|
||
|
||
for (const menu of menus) {
|
||
const menuId = menu.id ? String(menu.id) : null;
|
||
const hasPermission = menuId && userMenuIdSet.has(menuId);
|
||
|
||
// 先处理子菜单
|
||
let filteredChildren: MenuTreeDto[] | null = null;
|
||
if (menu.children && menu.children.length > 0) {
|
||
filteredChildren = filterMenus(menu.children);
|
||
}
|
||
|
||
// 如果菜单本身有权限,或者子菜单有权限,则保留
|
||
if (hasPermission || (filteredChildren && filteredChildren.length > 0)) {
|
||
const filteredMenu: MenuTreeDto = {
|
||
...menu,
|
||
children: filteredChildren && filteredChildren.length > 0 ? filteredChildren : null,
|
||
};
|
||
result.push(filteredMenu);
|
||
}
|
||
}
|
||
|
||
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 key 的映射表
|
||
const menuNameToLocaleKeyMap: Record<string, string> = {
|
||
'首页': 'menu.home',
|
||
'客户': 'menu.user',
|
||
'客户管理': 'menu.user.customer',
|
||
'个人中心': 'menu.user.profile',
|
||
'资金充值': 'menu.user.recharge',
|
||
'充值记录': 'menu.user.rechargeRecord',
|
||
'订单': 'menu.order',
|
||
'我要查价': 'menu.order.priceCheck',
|
||
'我要下单': 'menu.order.placeOrder',
|
||
'我的订单': 'menu.order.myOrders',
|
||
'渠道': 'menu.channel',
|
||
'销售产品': 'menu.channel.salesProducts',
|
||
'服务渠道': 'menu.channel.serviceChannels',
|
||
'黑名单区': 'menu.channel.blacklist',
|
||
'偏远分区': 'menu.channel.remoteZones',
|
||
'计重规则': 'menu.channel.weightRules',
|
||
'计重进位': 'menu.channel.weightRounding',
|
||
'自动触发': 'menu.channel.autoTrigger',
|
||
'轨迹': 'menu.track',
|
||
'轨迹订单': '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',
|
||
};
|
||
|
||
// 格式化菜单标题
|
||
const formatMenuTitle = (title?: string) => {
|
||
if (!title) return '';
|
||
// 如果是 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;
|
||
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; // 过滤按钮
|
||
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/blacklist': 'Blacklist',
|
||
'/channel/remoteZones': 'RemoteZones',
|
||
'/channel/weightRules': 'WeightRules',
|
||
'/channel/weightRounding': 'WeightRounding',
|
||
'/channel/autoTrigger': 'AutoTrigger',
|
||
// Track 相关路由
|
||
'/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',
|
||
};
|
||
|
||
// 跳转
|
||
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);
|
||
});
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 联系客服相关数据
|
||
interface ServiceWarehouseItem {
|
||
whseName: string;
|
||
serviceWechat: string;
|
||
servicePhone: string;
|
||
}
|
||
|
||
const serviceAddressData = ref<ServiceWarehouseItem[]>([]);
|
||
const serviceWarehouseSelectData = ref<Array<{label: string; value: string}>>([]);
|
||
const serviceWarehouseSelect = ref<string | null>(null);
|
||
|
||
// 加载联系客服仓库列表
|
||
const loadServiceDataList = async () => {
|
||
try {
|
||
// TODO: 替换为实际的 API 调用
|
||
// const res = await ApiWhseAddress.serviceList();
|
||
// if (res.code == 0) {
|
||
// serviceAddressData.value = res.data;
|
||
// serviceWarehouseSelectData.value = serviceAddressData.value.map(item => ({
|
||
// label: item.whseName,
|
||
// value: item.whseName
|
||
// }));
|
||
// serviceWarehouseSelect.value = serviceWarehouseSelectData.value[0]?.value || null;
|
||
// }
|
||
} catch (error) {
|
||
console.error('Failed to load service data', error);
|
||
}
|
||
};
|
||
|
||
// 当前选中的联系客服仓库
|
||
const serviceIsWarehouse = computed(() => {
|
||
if (!serviceWarehouseSelect.value) return {} as ServiceWarehouseItem;
|
||
return serviceAddressData.value.find(item => item.whseName == serviceWarehouseSelect.value) || {} as ServiceWarehouseItem;
|
||
});
|
||
|
||
// 图片URL转换方法
|
||
const MixinTransitionUrl = (url: string) => {
|
||
if (!url) return '';
|
||
if (url.indexOf("//") == -1) {
|
||
if (url.indexOf("QiNiuYunFile/") == -1) {
|
||
return `/api/upload/${url}`;
|
||
}
|
||
return `/${url}`;
|
||
}
|
||
return url;
|
||
};
|
||
|
||
// 余额显示
|
||
const balanceDisplay = computed(() => {
|
||
// TODO: 从 userStore 或 API 获取余额
|
||
const balance = (userStore as any).balance;
|
||
if (balance === null || balance === undefined) {
|
||
return '--';
|
||
}
|
||
const parsed = Number(balance);
|
||
return Number.isFinite(parsed) ? parsed.toFixed(2) : String(balance);
|
||
});
|
||
|
||
// 点击余额菜单项
|
||
const onBalanceMenuClick = (event: any) => {
|
||
const { key } = event;
|
||
if (key === 'recharge') {
|
||
router.push({ name: 'VipRechargeDeposit' });
|
||
} else if (key === 'rechargeRecord') {
|
||
router.push({ name: 'VipRechargeRechargeRecord' });
|
||
}
|
||
};
|
||
|
||
onMounted(async () => {
|
||
loadServiceDataList();
|
||
// 加载菜单树
|
||
await loadMenuTree();
|
||
});
|
||
</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: 15px;
|
||
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);
|
||
}
|
||
}
|
||
|
||
.service-button-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;
|
||
|
||
&:hover {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
i {
|
||
font-size: 18px;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
span {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.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: var(--app-primary-color);
|
||
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: 1rem;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
// 滚动条样式
|
||
.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>
|
||
|