TopMenu.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <template>
  2. <div class="top-menu-container">
  3. <el-menu
  4. :default-active="activeMenu"
  5. mode="horizontal"
  6. :ellipsis="false"
  7. background-color="#1ca851"
  8. text-color="#ffffff"
  9. active-text-color="#ffffff"
  10. @select="handleSelect"
  11. >
  12. <el-menu-item v-for="(item, index) in topMenus" :key="index" :index="isExternalLink(item) ? `external_${index}` : item.path">
  13. <template v-if="isExternalLink(item)">
  14. <a :href="getExternalUrl(item)" target="_blank" rel="noopener noreferrer" class="external-link" @click.stop>
  15. <svg-icon v-if="item.meta && item.meta.icon" :icon-class="item.meta.icon" />
  16. <span>{{ item.meta?.title || item.name }}</span>
  17. </a>
  18. </template>
  19. <template v-else>
  20. <svg-icon v-if="item.meta && item.meta.icon" :icon-class="item.meta.icon" />
  21. <span>{{ item.meta?.title || item.name }}</span>
  22. </template>
  23. </el-menu-item>
  24. </el-menu>
  25. </div>
  26. </template>
  27. <script setup lang="ts">
  28. import { ref, computed, watch, onMounted } from 'vue';
  29. import { usePermissionStore } from '@/store/modules/permission';
  30. import { useRouter, useRoute } from 'vue-router';
  31. import { RouteRecordRaw } from 'vue-router';
  32. const router = useRouter();
  33. const route = useRoute();
  34. const permissionStore = usePermissionStore();
  35. // 顶部菜单列表
  36. const topMenus = ref<RouteRecordRaw[]>([]);
  37. // 当前激活的一级菜单
  38. const activeMenu = computed(() => {
  39. const path = route.path;
  40. let activePath = path;
  41. // 查找当前路径对应的一级菜单
  42. if (topMenus.value.length > 0) {
  43. const currentTopMenu = topMenus.value.find((item) => {
  44. // 清理双斜杠
  45. const menuPath = item.path.replace(/\/+/g, '/');
  46. return path.startsWith(menuPath);
  47. });
  48. if (currentTopMenu && currentTopMenu.path) {
  49. // 清理双斜杠
  50. activePath = currentTopMenu.path.replace(/\/+/g, '/');
  51. }
  52. }
  53. return activePath;
  54. });
  55. // 设置顶部菜单
  56. const setTopMenus = () => {
  57. const routers = permissionStore.getTopbarRoutes();
  58. console.log('TopMenu - 顶部路由:', routers);
  59. // 过滤出一级菜单(visible为true,且不是隐藏的菜单,且不是按钮类型)
  60. topMenus.value = routers.filter((item: any) => {
  61. return !item.hidden && item.meta?.visible !== false && item.menuType !== 'F';
  62. });
  63. console.log('TopMenu - 顶部菜单列表:', topMenus.value);
  64. };
  65. // 判断是否为外链
  66. const isExternalLink = (item: any): boolean => {
  67. // isFrame 为 "0" 表示是外链
  68. return item.isFrame === '0';
  69. };
  70. // 获取外链完整URL
  71. const getExternalUrl = (item: any): string => {
  72. // 对于 isFrame='0' 的外链菜单,path 字段就是完整的外链地址
  73. // 对于内链(isFrame='1' 且 path 是 http 开头),地址存储在 meta.link 中
  74. const url = item.path || item.meta?.link || '';
  75. console.log('TopMenu - 外链地址:', url, '(isFrame:', item.isFrame, ')');
  76. return url;
  77. };
  78. // 菜单选择事件
  79. const handleSelect = (index: string) => {
  80. console.log('TopMenu - 选择菜单:', index);
  81. // 如果是外链菜单的 index(格式:external_数字),直接返回
  82. if (index.startsWith('external_')) {
  83. console.log('TopMenu - 外链菜单,由 <a> 标签处理打开');
  84. return;
  85. }
  86. // 查找对应的菜单项
  87. const menuItem = topMenus.value.find((item) => {
  88. const cleanPath = item.path.replace(/\/+/g, '/');
  89. const cleanIndex = index.replace(/\/+/g, '/');
  90. return cleanPath === cleanIndex || item.path === index;
  91. });
  92. console.log('TopMenu - 找到的菜单项:', menuItem);
  93. // 清理双斜杠
  94. const cleanIndex = index.replace(/\/+/g, '/');
  95. console.log('TopMenu - 清理后路径:', cleanIndex);
  96. if (menuItem) {
  97. // 判断是否有子菜单(过滤掉隐藏的和按钮类型的)
  98. const hasChildren = menuItem.children && menuItem.children.length > 0;
  99. const visibleChildren = hasChildren ? menuItem.children.filter((child: any) => !child.hidden && child.menuType !== 'F') : [];
  100. // 如果有子菜单(说明是目录菜单)
  101. if (visibleChildren.length > 0) {
  102. // 判断当前路由是否已经在该菜单下
  103. const currentPath = route.path;
  104. const isUnderThisMenu = currentPath.startsWith(cleanIndex);
  105. console.log('TopMenu - 当前路径:', currentPath, '是否在该菜单下:', isUnderThisMenu);
  106. // 如果当前路由不在该菜单下,则跳转到第一个子菜单
  107. if (!isUnderThisMenu) {
  108. const firstChild = visibleChildren[0];
  109. console.log('TopMenu - 跳转到第一个子菜单:', firstChild);
  110. if (firstChild) {
  111. // 构建完整路径
  112. let targetPath = firstChild.path;
  113. // 如果子菜单的path不是以/开头,说明是相对路径,需要拼接父路径
  114. if (!targetPath.startsWith('/')) {
  115. targetPath = `${cleanIndex}/${targetPath}`;
  116. }
  117. // 清理可能的双斜杠
  118. targetPath = targetPath.replace(/\/+/g, '/');
  119. console.log('TopMenu - 最终跳转路径:', targetPath);
  120. router.push(targetPath);
  121. }
  122. }
  123. // 如果已经在该菜单下,不做跳转,保持当前路由,让左侧菜单树显示
  124. else {
  125. console.log('TopMenu - 已在该菜单下,不跳转,保持当前页面');
  126. }
  127. }
  128. // 如果没有子菜单,说明是直接页面,直接跳转
  129. else {
  130. console.log('TopMenu - 直接跳转到:', cleanIndex);
  131. router.push(cleanIndex);
  132. }
  133. }
  134. };
  135. // 初始化
  136. onMounted(() => {
  137. setTopMenus();
  138. });
  139. // 监听路由变化,更新菜单
  140. watch(
  141. () => permissionStore.topbarRouters,
  142. () => {
  143. setTopMenus();
  144. },
  145. { deep: true }
  146. );
  147. </script>
  148. <style lang="scss" scoped>
  149. .top-menu-container {
  150. height: 100%;
  151. :deep(.el-menu) {
  152. border: none;
  153. height: 100%;
  154. &.el-menu--horizontal {
  155. border-bottom: none;
  156. }
  157. }
  158. :deep(.el-menu-item) {
  159. height: 100%;
  160. line-height: 50px;
  161. border-bottom: 3px solid transparent;
  162. padding: 0 20px;
  163. font-size: 14px;
  164. font-weight: 500;
  165. transition: all 0.3s;
  166. &:hover {
  167. background-color: rgba(255, 255, 255, 0.15) !important;
  168. color: #ffffff !important;
  169. }
  170. &.is-active {
  171. border-bottom-color: #ffffff;
  172. background-color: rgba(255, 255, 255, 0.2) !important;
  173. color: #ffffff !important;
  174. font-weight: 600;
  175. }
  176. .svg-icon {
  177. margin-right: 8px;
  178. font-size: 16px;
  179. vertical-align: middle;
  180. }
  181. span {
  182. vertical-align: middle;
  183. }
  184. .external-link {
  185. display: flex;
  186. align-items: center;
  187. color: inherit;
  188. text-decoration: none;
  189. width: 100%;
  190. height: 100%;
  191. &:hover {
  192. color: #ffffff;
  193. }
  194. .svg-icon {
  195. margin-right: 8px;
  196. font-size: 16px;
  197. }
  198. }
  199. }
  200. }
  201. </style>