Explorar el Código

refactor(layouts): 抽离菜单展开逻辑到独立hook

- 将 Classic、Columns、Vertical 布局中的菜单展开逻辑提取为 useOpenedMenus hook
- 移除重复的 openedMenus 状态和相关计算方法
- 统一处理菜单展开状态的计算与监听逻辑
- 优化路径解析与祖先节点收集算法
- 支持手风琴模式下的菜单展开控制
- 提高代码复用性与可维护性
congxuesong hace 4 semanas
padre
commit
d2e2a28d33

+ 3 - 49
src/layouts/LayoutClassic/index.vue

@@ -39,7 +39,7 @@
 </template>
 
 <script setup lang="ts" name="layoutClassic">
-import { computed, ref, watch, nextTick } from "vue";
+import { computed, ref } from "vue";
 import { useRoute } from "vue-router";
 import { useAuthStore } from "@/stores/modules/auth";
 import { useGlobalStore } from "@/stores/modules/global";
@@ -48,6 +48,7 @@ import SubMenu from "@/layouts/components/Menu/SubMenu.vue";
 import ToolBarLeft from "@/layouts/components/Header/ToolBarLeft.vue";
 import ToolBarRight from "@/layouts/components/Header/ToolBarRight.vue";
 import type { ElMenu } from "element-plus";
+import { useOpenedMenus } from "@/layouts/hooks/useOpenedMenus";
 
 const title = import.meta.env.VITE_GLOB_APP_TITLE;
 
@@ -60,54 +61,7 @@ const menuList = computed(() => authStore.showMenuListGet);
 const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
 
 const menuRef = ref<InstanceType<typeof ElMenu>>();
-const openedMenus = ref<string[]>([]);
-
-// 计算应该展开的父菜单路径
-const getParentMenuPath = (path: string) => {
-  const pathSegments = path.split("/").filter(Boolean);
-  // 只有当路径有两段或以上时才返回父菜单路径
-  // 例如: /licenseManagement/businessLicense -> /licenseManagement
-  if (pathSegments.length > 1) {
-    return `/${pathSegments[0]}`;
-  }
-  return "";
-};
-
-// 根据当前路由计算应该展开的父菜单
-const calculateOpenedMenus = () => {
-  const currentPath = (route.meta.activeMenu as string) || route.path;
-  const parentPath = getParentMenuPath(currentPath);
-
-  if (parentPath) {
-    // 如果手风琴模式开启,只保留当前父菜单
-    if (accordion.value) {
-      return [parentPath];
-    } else {
-      // 如果手风琴模式关闭,保留已有的展开菜单,并添加当前父菜单
-      const newOpenedMenus = [...openedMenus.value];
-      if (!newOpenedMenus.includes(parentPath)) {
-        newOpenedMenus.push(parentPath);
-      }
-      return newOpenedMenus;
-    }
-  }
-  return openedMenus.value;
-};
-
-// 监听路由变化,保持父菜单展开
-watch(
-  () => [route.path, route.meta.activeMenu],
-  () => {
-    nextTick(() => {
-      const newOpenedMenus = calculateOpenedMenus();
-      // 只有当展开菜单发生变化时才更新
-      if (JSON.stringify(newOpenedMenus.sort()) !== JSON.stringify(openedMenus.value.sort())) {
-        openedMenus.value = newOpenedMenus;
-      }
-    });
-  },
-  { immediate: true }
-);
+const { openedMenus } = useOpenedMenus({ route, accordion });
 </script>
 
 <style scoped lang="scss">

+ 3 - 0
src/layouts/LayoutColumns/index.vue

@@ -30,6 +30,7 @@
         <el-menu
           :router="false"
           :default-active="activeMenu"
+          :default-openeds="openedMenus"
           :collapse="isCollapse"
           :unique-opened="accordion"
           :collapse-transition="false"
@@ -57,6 +58,7 @@ import Main from "@/layouts/components/Main/index.vue";
 import ToolBarLeft from "@/layouts/components/Header/ToolBarLeft.vue";
 import ToolBarRight from "@/layouts/components/Header/ToolBarRight.vue";
 import SubMenu from "@/layouts/components/Menu/SubMenu.vue";
+import { useOpenedMenus } from "@/layouts/hooks/useOpenedMenus";
 
 const title = import.meta.env.VITE_GLOB_APP_TITLE;
 
@@ -71,6 +73,7 @@ const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu
 
 const subMenuList = ref<Menu.MenuOptions[]>([]);
 const splitActive = ref("");
+const { openedMenus } = useOpenedMenus({ route, accordion });
 watch(
   () => [menuList, route],
   () => {

+ 3 - 49
src/layouts/LayoutVertical/index.vue

@@ -33,7 +33,7 @@
 </template>
 
 <script setup lang="ts" name="layoutVertical">
-import { computed, ref, watch, nextTick } from "vue";
+import { computed, ref } from "vue";
 import { useRoute } from "vue-router";
 import { useAuthStore } from "@/stores/modules/auth";
 import { useGlobalStore } from "@/stores/modules/global";
@@ -42,6 +42,7 @@ import ToolBarLeft from "@/layouts/components/Header/ToolBarLeft.vue";
 import ToolBarRight from "@/layouts/components/Header/ToolBarRight.vue";
 import SubMenu from "@/layouts/components/Menu/SubMenu.vue";
 import type { ElMenu } from "element-plus";
+import { useOpenedMenus } from "@/layouts/hooks/useOpenedMenus";
 
 const title = import.meta.env.VITE_GLOB_APP_TITLE;
 
@@ -54,54 +55,7 @@ const menuList = computed(() => authStore.showMenuListGet);
 const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
 
 const menuRef = ref<InstanceType<typeof ElMenu>>();
-const openedMenus = ref<string[]>([]);
-
-// 计算应该展开的父菜单路径
-const getParentMenuPath = (path: string) => {
-  const pathSegments = path.split("/").filter(Boolean);
-  // 只有当路径有两段或以上时才返回父菜单路径
-  // 例如: /licenseManagement/businessLicense -> /licenseManagement
-  if (pathSegments.length > 1) {
-    return `/${pathSegments[0]}`;
-  }
-  return "";
-};
-
-// 根据当前路由计算应该展开的父菜单
-const calculateOpenedMenus = () => {
-  const currentPath = (route.meta.activeMenu as string) || route.path;
-  const parentPath = getParentMenuPath(currentPath);
-
-  if (parentPath) {
-    // 如果手风琴模式开启,只保留当前父菜单
-    if (accordion.value) {
-      return [parentPath];
-    } else {
-      // 如果手风琴模式关闭,保留已有的展开菜单,并添加当前父菜单
-      const newOpenedMenus = [...openedMenus.value];
-      if (!newOpenedMenus.includes(parentPath)) {
-        newOpenedMenus.push(parentPath);
-      }
-      return newOpenedMenus;
-    }
-  }
-  return openedMenus.value;
-};
-
-// 监听路由变化,保持父菜单展开
-watch(
-  () => [route.path, route.meta.activeMenu],
-  () => {
-    nextTick(() => {
-      const newOpenedMenus = calculateOpenedMenus();
-      // 只有当展开菜单发生变化时才更新
-      if (JSON.stringify(newOpenedMenus.sort()) !== JSON.stringify(openedMenus.value.sort())) {
-        openedMenus.value = newOpenedMenus;
-      }
-    });
-  },
-  { immediate: true }
-);
+const { openedMenus } = useOpenedMenus({ route, accordion });
 </script>
 
 <style scoped lang="scss">

+ 65 - 0
src/layouts/hooks/useOpenedMenus.ts

@@ -0,0 +1,65 @@
+import { ref, watch } from "vue";
+import type { Ref } from "vue";
+import type { RouteLocationNormalizedLoaded } from "vue-router";
+
+interface UseOpenedMenusOptions {
+  route: RouteLocationNormalizedLoaded;
+  accordion: Ref<boolean>;
+}
+
+export const useOpenedMenus = ({ route, accordion }: UseOpenedMenusOptions) => {
+  const openedMenus = ref<string[]>([]);
+
+  const stringify = (arr: string[]) => JSON.stringify([...arr].sort());
+
+  const normalizePath = (path: string) => (path.startsWith("/") ? path : `/${path}`);
+
+  const collectAncestorPaths = (path?: string) => {
+    if (!path) return [];
+    const normalized = normalizePath(path);
+    const segments = normalized.split("/").filter(Boolean);
+    return segments.reduce<string[]>((acc, _, index) => {
+      if (index === segments.length - 1) return acc;
+      acc.push(`/${segments.slice(0, index + 1).join("/")}`);
+      return acc;
+    }, []);
+  };
+
+  const buildTargetOpenedPaths = () => {
+    const targetSet = new Set<string>();
+    collectAncestorPaths(route.path).forEach(path => targetSet.add(path));
+    const activeMenuPath = route.meta.activeMenu as string | undefined;
+    if (activeMenuPath) {
+      const normalizedActive = normalizePath(activeMenuPath);
+      collectAncestorPaths(normalizedActive).forEach(path => targetSet.add(path));
+      targetSet.add(normalizedActive);
+    }
+    return Array.from(targetSet);
+  };
+
+  const calculateOpenedMenus = () => {
+    const targetPaths = buildTargetOpenedPaths();
+    if (!targetPaths.length) {
+      return accordion.value ? [] : [...openedMenus.value];
+    }
+    if (accordion.value) return targetPaths;
+    const menuSet = new Set(openedMenus.value);
+    targetPaths.forEach(path => menuSet.add(path));
+    return Array.from(menuSet);
+  };
+
+  watch(
+    () => [route.path, route.meta.activeMenu, accordion.value],
+    () => {
+      const newOpenedMenus = calculateOpenedMenus();
+      if (stringify(newOpenedMenus) !== stringify(openedMenus.value)) {
+        openedMenus.value = newOpenedMenus;
+      }
+    },
+    { immediate: true }
+  );
+
+  return {
+    openedMenus
+  };
+};