Browse Source

fix: 菜单渲染

laiqi 1 year ago
parent
commit
e834986e3d

+ 4 - 1
apps/web-ele/components.d.ts

@@ -2,7 +2,7 @@
 // @ts-nocheck
 // Generated by unplugin-vue-components
 // Read more: https://github.com/vuejs/core/pull/3399
-export {}
+export {};
 
 /* prettier-ignore */
 declare module 'vue' {
@@ -10,4 +10,7 @@ declare module 'vue' {
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
   }
+  export interface ComponentCustomProperties {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+  }
 }

+ 4 - 2
apps/web-ele/src/api/request.ts

@@ -58,7 +58,8 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
   }
 
   function formatToken(token: null | string) {
-    return token ? `Bearer ${token}` : null;
+    // return token ? `Bearer ${token}` : null;
+    return token ? `${token}` : null;
   }
 
   // 请求头处理
@@ -66,7 +67,8 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
     fulfilled: async (config) => {
       const accessStore = useAccessStore();
 
-      config.headers.Authorization = formatToken(accessStore.accessToken);
+      // config.headers.Authorization = formatToken(accessStore.accessToken);
+      config.headers.token = formatToken(accessStore.accessToken);
       config.headers['Accept-Language'] = preferences.app.locale;
 
       // .net 序列化请求数据

+ 72 - 57
apps/web-ele/src/layouts/basic.vue

@@ -4,9 +4,9 @@ import type { NotificationItem } from '@vben/layouts';
 import { computed, ref, watch } from 'vue';
 
 import { AuthenticationLoginExpiredModal } from '@vben/common-ui';
-import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
+// import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants';
 import { useWatermark } from '@vben/hooks';
-import { BookOpenText, CircleHelp, MdiGithub } from '@vben/icons';
+import { MdiOnepassword } from '@vben/icons';
 import {
   BasicLayout,
   LockScreen,
@@ -15,41 +15,41 @@ import {
 } from '@vben/layouts';
 import { preferences } from '@vben/preferences';
 import { useAccessStore, useUserStore } from '@vben/stores';
-import { openWindow } from '@vben/utils';
+// import { openWindow } from '@vben/utils';
 
-import { $t } from '#/locales';
+// import { $t } from '#/locales';
 import { useAuthStore } from '#/store';
 import LoginForm from '#/views/_core/authentication/login.vue';
 
 const notifications = ref<NotificationItem[]>([
-  {
-    avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
-    date: '3小时前',
-    isRead: true,
-    message: '描述信息描述信息描述信息',
-    title: '收到了 14 份新周报',
-  },
-  {
-    avatar: 'https://avatar.vercel.sh/1',
-    date: '刚刚',
-    isRead: false,
-    message: '描述信息描述信息描述信息',
-    title: '朱偏右 回复了你',
-  },
-  {
-    avatar: 'https://avatar.vercel.sh/1',
-    date: '2024-01-01',
-    isRead: false,
-    message: '描述信息描述信息描述信息',
-    title: '曲丽丽 评论了你',
-  },
-  {
-    avatar: 'https://avatar.vercel.sh/satori',
-    date: '1天前',
-    isRead: false,
-    message: '描述信息描述信息描述信息',
-    title: '代办提醒',
-  },
+  // {
+  //   avatar: 'https://avatar.vercel.sh/vercel.svg?text=VB',
+  //   date: '3小时前',
+  //   isRead: true,
+  //   message: '描述信息描述信息描述信息',
+  //   title: '收到了 14 份新周报',
+  // },
+  // {
+  //   avatar: 'https://avatar.vercel.sh/1',
+  //   date: '刚刚',
+  //   isRead: false,
+  //   message: '描述信息描述信息描述信息',
+  //   title: '朱偏右 回复了你',
+  // },
+  // {
+  //   avatar: 'https://avatar.vercel.sh/1',
+  //   date: '2024-01-01',
+  //   isRead: false,
+  //   message: '描述信息描述信息描述信息',
+  //   title: '曲丽丽 评论了你',
+  // },
+  // {
+  //   avatar: 'https://avatar.vercel.sh/satori',
+  //   date: '1天前',
+  //   isRead: false,
+  //   message: '描述信息描述信息描述信息',
+  //   title: '代办提醒',
+  // },
 ]);
 
 const userStore = useUserStore();
@@ -63,37 +63,52 @@ const showDot = computed(() =>
 const menus = computed(() => [
   {
     handler: () => {
-      openWindow(VBEN_DOC_URL, {
-        target: '_blank',
-      });
+      // ...
     },
-    icon: BookOpenText,
-    text: $t('ui.widgets.document'),
-  },
-  {
-    handler: () => {
-      openWindow(VBEN_GITHUB_URL, {
-        target: '_blank',
-      });
-    },
-    icon: MdiGithub,
-    text: 'GitHub',
-  },
-  {
-    handler: () => {
-      openWindow(`${VBEN_GITHUB_URL}/issues`, {
-        target: '_blank',
-      });
-    },
-    icon: CircleHelp,
-    text: $t('ui.widgets.qa'),
+    icon: MdiOnepassword,
+    text: '修改密码',
   },
+  // {
+  //   handler: () => {
+  //     openWindow(VBEN_DOC_URL, {
+  //       target: '_blank',
+  //     });
+  //   },
+  //   icon: BookOpenText,
+  //   text: $t('ui.widgets.document'),
+  // },
+  // {
+  //   handler: () => {
+  //     openWindow(VBEN_GITHUB_URL, {
+  //       target: '_blank',
+  //     });
+  //   },
+  //   icon: MdiGithub,
+  //   text: 'GitHub',
+  // },
+  // {
+  //   handler: () => {
+  //     openWindow(`${VBEN_GITHUB_URL}/issues`, {
+  //       target: '_blank',
+  //     });
+  //   },
+  //   icon: CircleHelp,
+  //   text: $t('ui.widgets.qa'),
+  // },
 ]);
 
 const avatar = computed(() => {
   return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
 });
 
+const gwName = computed(() => {
+  return userStore.userInfo?.gw ?? '';
+});
+
+const nickName = computed(() => {
+  return userStore.userInfo?.nickname ?? '';
+});
+
 async function handleLogout() {
   await authStore.logout(false);
 }
@@ -129,8 +144,8 @@ watch(
         :avatar
         :menus
         :text="userStore.userInfo?.realName"
-        description="ann.vben@gmail.com"
-        tag-text="Pro"
+        :description="nickName"
+        :tag-text="gwName"
         @logout="handleLogout"
       />
     </template>

+ 6 - 1
apps/web-ele/src/router/access.ts

@@ -29,8 +29,13 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
         duration: 1500,
         message: `${$t('common.loadingMenu')}...`,
       });
-      return await getAllMenusApi();
+
+      // 获取后端菜单列表
+      const { Data: dynamicRoutes } = (await getAllMenusApi()) as any;
+
+      return dynamicRoutes;
     },
+
     // 可以指定没有权限跳转403页面
     forbiddenComponent,
     // 如果 route.meta.menuVisibleWithForbidden = true

+ 3 - 1
apps/web-ele/src/router/routes/core.ts

@@ -6,6 +6,8 @@ import { AuthPageLayout, BasicLayout } from '#/layouts';
 import { $t } from '#/locales';
 import Login from '#/views/_core/authentication/login.vue';
 
+import { dynamicRoutes } from './dynamic';
+
 /** 全局404页面 */
 const fallbackNotFoundRoute: RouteRecordRaw = {
   component: () => import('#/views/_core/fallback/not-found.vue'),
@@ -35,7 +37,7 @@ const coreRoutes: RouteRecordRaw[] = [
     name: 'Root',
     path: '/',
     redirect: DEFAULT_HOME_PATH,
-    children: [],
+    children: dynamicRoutes,
   },
   {
     component: AuthPageLayout,

+ 12 - 0
apps/web-ele/src/router/routes/dynamic.ts

@@ -0,0 +1,12 @@
+import type { RouteRecordRaw } from 'vue-router';
+
+import type { AuthorityDataList } from '@vben/types';
+
+// 导出动态路由
+export const dynamicRoutes: RouteRecordRaw[] = [];
+
+// 异步设置动态路由
+export function setDynamicRoutes(routes: AuthorityDataList[]) {
+  dynamicRoutes.length = 0;
+  dynamicRoutes.push(...(routes as unknown as RouteRecordRaw[]));
+}

+ 2 - 1
apps/web-ele/src/router/routes/index.ts

@@ -34,4 +34,5 @@ const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
 
 /** 有权限校验的路由列表,包含动态路由和静态路由 */
 const accessRoutes = [...dynamicRoutes, ...staticRoutes];
-export { accessRoutes, coreRouteNames, routes };
+
+export { accessRoutes, coreRouteNames, dynamicRoutes, routes };

+ 0 - 36
apps/web-ele/src/router/routes/modules/demos.ts

@@ -1,36 +0,0 @@
-import type { RouteRecordRaw } from 'vue-router';
-
-import { $t } from '#/locales';
-
-const routes: RouteRecordRaw[] = [
-  {
-    meta: {
-      icon: 'ic:baseline-view-in-ar',
-      keepAlive: true,
-      order: 1000,
-      title: $t('demos.title'),
-    },
-    name: 'Demos',
-    path: '/demos',
-    children: [
-      {
-        meta: {
-          title: $t('demos.elementPlus'),
-        },
-        name: 'NaiveDemos',
-        path: '/demos/element',
-        component: () => import('#/views/demos/element/index.vue'),
-      },
-      {
-        meta: {
-          title: $t('demos.form'),
-        },
-        name: 'BasicForm',
-        path: '/demos/form',
-        component: () => import('#/views/demos/form/basic.vue'),
-      },
-    ],
-  },
-];
-
-export default routes;

+ 0 - 82
apps/web-ele/src/router/routes/modules/vben.ts

@@ -1,82 +0,0 @@
-import type { RouteRecordRaw } from 'vue-router';
-
-import {
-  VBEN_ANT_PREVIEW_URL,
-  VBEN_DOC_URL,
-  VBEN_GITHUB_URL,
-  VBEN_LOGO_URL,
-  VBEN_NAIVE_PREVIEW_URL,
-} from '@vben/constants';
-import { SvgAntdvLogoIcon } from '@vben/icons';
-
-import { IFrameView } from '#/layouts';
-import { $t } from '#/locales';
-
-const routes: RouteRecordRaw[] = [
-  {
-    meta: {
-      badgeType: 'dot',
-      icon: VBEN_LOGO_URL,
-      order: 9998,
-      title: $t('demos.vben.title'),
-    },
-    name: 'VbenProject',
-    path: '/vben-admin',
-    children: [
-      {
-        name: 'VbenDocument',
-        path: '/vben-admin/document',
-        component: IFrameView,
-        meta: {
-          icon: 'lucide:book-open-text',
-          link: VBEN_DOC_URL,
-          title: $t('demos.vben.document'),
-        },
-      },
-      {
-        name: 'VbenGithub',
-        path: '/vben-admin/github',
-        component: IFrameView,
-        meta: {
-          icon: 'mdi:github',
-          link: VBEN_GITHUB_URL,
-          title: 'Github',
-        },
-      },
-      {
-        name: 'VbenNaive',
-        path: '/vben-admin/naive',
-        component: IFrameView,
-        meta: {
-          badgeType: 'dot',
-          icon: 'logos:naiveui',
-          link: VBEN_NAIVE_PREVIEW_URL,
-          title: $t('demos.vben.naive-ui'),
-        },
-      },
-      {
-        name: 'VbenAntd',
-        path: '/vben-admin/antd',
-        component: IFrameView,
-        meta: {
-          badgeType: 'dot',
-          icon: SvgAntdvLogoIcon,
-          link: VBEN_ANT_PREVIEW_URL,
-          title: $t('demos.vben.antdv'),
-        },
-      },
-    ],
-  },
-  {
-    name: 'VbenAbout',
-    path: '/vben-admin/about',
-    component: () => import('#/views/_core/about/index.vue'),
-    meta: {
-      icon: 'lucide:copyright',
-      title: $t('demos.vben.about'),
-      order: 9999,
-    },
-  },
-];
-
-export default routes;

+ 1 - 1
apps/web-ele/src/views/dashboard/analytics/index.vue

@@ -64,7 +64,7 @@ const chartTabs: TabOption[] = [
 </script>
 
 <template>
-  <div class="p-5">
+  <div class="p-2">
     <AnalysisOverview :items="overviewItems" />
     <AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
       <template #trends>

+ 1 - 1
apps/web-ele/src/views/dashboard/workspace/index.vue

@@ -234,7 +234,7 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
 </script>
 
 <template>
-  <div class="p-5">
+  <div class="p-2">
     <WorkbenchHeader
       :avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
     >

+ 117 - 0
packages/@core/base/typings/src/vue-router.d.ts

@@ -140,7 +140,124 @@ interface GenerateMenuAndRoutesOptions {
   routes: RouteRecordRaw[];
 }
 
+/**
+ * 菜单类型枚举
+ */
+type MenuType = {
+  /** API接口 */
+  API: 4;
+  /** 按钮 */
+  BUTTON: 3;
+  /** 目录 */
+  DIRECTORY: 1;
+  /** 菜单 */
+  MENU: 2;
+}[keyof MenuType];
+
+// 数据库-权限数据
+interface AuthorityDataList {
+  /**
+   * 激活的菜单路径
+   */
+  active_menu: null | string;
+
+  /**
+   * API参数
+   */
+  api_parameter: null | string;
+
+  /**
+   * API地址
+   */
+  api_url: null | string;
+
+  /**
+   * 子菜单列表
+   */
+  children?: AuthorityDataList[];
+
+  /**
+   * 组件路径
+   */
+  component: string;
+
+  /**
+   * 是否隐藏头部(0显示 1隐藏)
+   */
+  hidden_header: number;
+
+  /**
+   * 菜单图标
+   */
+  icon: string;
+
+  /**
+   * 菜单ID
+   */
+  menu_id: number;
+
+  /**
+   * 菜单唯一标识
+   */
+  menu_key: string;
+
+  /**
+   * 菜单名称
+   */
+  menu_name: string;
+
+  /**
+   * 菜单类型
+   * - 1: 目录
+   * - 2: 菜单
+   * - 3: 按钮
+   * - 4: API接口
+   */
+  menu_type: MenuType;
+
+  /**
+   * 是否缓存(0缓存 1不缓存)
+   */
+  no_cache: number;
+
+  /**
+   * 显示顺序
+   */
+  order_num: number;
+
+  /**
+   * 父级ID
+   */
+  parent_id: number;
+
+  /**
+   * 路由路径
+   */
+  path: string;
+
+  /**
+   * 权限标识
+   */
+  perms: null | string;
+
+  /**
+   * 重定向地址
+   */
+  redirect: null | string;
+
+  /**
+   * 备注
+   */
+  remark: string;
+
+  /**
+   * 菜单显示状态(0显示 1隐藏)
+   */
+  visible: number;
+}
+
 export type {
+  AuthorityDataList,
   ComponentRecordType,
   GenerateMenuAndRoutesOptions,
   RouteMeta,

+ 1 - 1
packages/@core/preferences/src/config.ts

@@ -2,7 +2,7 @@ import type { Preferences } from './types';
 
 const defaultPreferences: Preferences = {
   app: {
-    accessMode: 'frontend',
+    accessMode: 'backend',
     authPageLayout: 'panel-right',
     checkUpdatesInterval: 1,
     colorGrayMode: false,

+ 9 - 2
packages/effects/access/src/accessible.ts

@@ -16,12 +16,13 @@ async function generateAccessible(
   mode: AccessModeType,
   options: GenerateMenuAndRoutesOptions,
 ) {
+  // console.log('generateAccessible options:', options);
+
   const { router } = options;
 
   options.routes = cloneDeep(options.routes);
-  // 生成路由
+  // 接口请求_生成路由
   const accessibleRoutes = await generateRoutes(mode, options);
-
   const root = router.getRoutes().find((item) => item.path === '/');
 
   // 动态添加到router实例内
@@ -66,6 +67,8 @@ async function generateRoutes(
   switch (mode) {
     case 'backend': {
       resultRoutes = await generateRoutesByBackend(options);
+      // eslint-disable-next-line no-console
+      console.log('backend routes:', resultRoutes);
       break;
     }
     case 'frontend': {
@@ -74,6 +77,8 @@ async function generateRoutes(
         roles || [],
         forbiddenComponent,
       );
+      // eslint-disable-next-line no-console
+      console.log('frontend routes:', resultRoutes);
       break;
     }
   }
@@ -98,6 +103,8 @@ async function generateRoutes(
     return route;
   });
 
+  // console.log('mapTree routes:', resultRoutes);
+
   return resultRoutes;
 }
 

+ 2 - 0
packages/icons/src/iconify/index.ts

@@ -11,3 +11,5 @@ export const MdiGithub = createIconifyIcon('mdi:github');
 export const MdiGoogle = createIconifyIcon('mdi:google');
 
 export const MdiQqchat = createIconifyIcon('mdi:qqchat');
+
+export const MdiOnepassword = createIconifyIcon('mdi:onepassword');

+ 251 - 3
packages/utils/src/helpers/generate-routes-backend.ts

@@ -1,8 +1,10 @@
 import type { RouteRecordRaw } from 'vue-router';
 
 import type {
+  AuthorityDataList,
   ComponentRecordType,
   GenerateMenuAndRoutesOptions,
+  RouteMeta,
   RouteRecordStringComponent,
 } from '@vben-core/typings';
 
@@ -14,10 +16,36 @@ import { mapTree } from '@vben-core/shared/utils';
 async function generateRoutesByBackend(
   options: GenerateMenuAndRoutesOptions,
 ): Promise<RouteRecordRaw[]> {
-  const { fetchMenuListAsync, layoutMap = {}, pageMap = {} } = options;
+  const {
+    fetchMenuListAsync,
+    layoutMap = {},
+    pageMap = {},
+    routes: dynamicRoutes,
+  } = options;
 
   try {
     const menuRoutes = await fetchMenuListAsync?.();
+    // console.log('menuRoutes:', menuRoutes);
+    // [{
+    //   "menu_id": 1,
+    //   "menu_name": "systemManage",
+    //   "menu_key": "_systemManage",
+    //   "component": "/layout",
+    //   "active_menu": null,
+    //   "parent_id": 0,
+    //   "order_num": 99,
+    //   "menu_type": 1,
+    //   "visible": 0,
+    //   "perms": null,
+    //   "icon": "ii-system",
+    //   "path": "/systemmanage",
+    //   "redirect": null,
+    //   "hidden_header": 0,
+    //   "remark": "系统管理",
+    //   "no_cache": 0,
+    //   "api_parameter": null,
+    //   "api_url": null
+    // },]
     if (!menuRoutes) {
       return [];
     }
@@ -28,9 +56,21 @@ async function generateRoutesByBackend(
       normalizePageMap[normalizeViewPath(key)] = value;
     }
 
-    const routes = convertRoutes(menuRoutes, layoutMap, normalizePageMap);
+    // 转换接口权限数据
+    const processMenuRoutes = processMenuData(
+      menuRoutes as AuthorityDataList[],
+    );
 
-    return routes;
+    const routes = convertRoutes(
+      processMenuRoutes as unknown as RouteRecordStringComponent[],
+      layoutMap,
+      normalizePageMap,
+    );
+
+    // 合并动态路由和后端路由
+    const finalRoutes = [...dynamicRoutes, ...routes];
+
+    return finalRoutes;
   } catch (error) {
     console.error(error);
     return [];
@@ -80,4 +120,212 @@ function normalizeViewPath(path: string): string {
   // 这里耦合了vben-admin的目录结构
   return viewPath.replace(/^\/views/, '');
 }
+
+/**
+ * 菜单类型枚举
+ */
+const MenuType = {
+  /** 目录 */
+  DIRECTORY: 1,
+  /** 菜单 */
+  MENU: 2,
+  /** 按钮 */
+  BUTTON: 3,
+  /** API接口 */
+  API: 4,
+} as const;
+
+/**
+ * 处理接口返回的权限数据
+ * 1.分离按钮和菜单权限;
+ * 2.将按钮生成权限组,存入store中
+ * 3.将数据库字段转为系统所用字段,并递归生成菜单树
+ */
+
+function processMenuData(routes: AuthorityDataList[]) {
+  // 使用语义化的枚举值过滤按钮类型
+  // const buttonTempArr = routes.filter(
+  //   (item) => item.menu_type === MenuType.BUTTON,
+  // );
+
+  const menuTempArr = routes.filter((item) =>
+    [MenuType.DIRECTORY, MenuType.MENU].includes(item.menu_type),
+  );
+
+  // const apiTempArr = routes.filter((item) => item.menu_type === MenuType.API);
+
+  const menuTree = convertToTree(menuTempArr);
+
+  // console.log('api list:', apiTempArr);
+  // console.log('button list:', buttonTempArr);
+  // console.log('menu list:', menuTempArr);
+  // console.log('menu tree:', menuTree);
+
+  return menuTree;
+}
+
+// 转换为菜单结构
+function convertToTree(routes: AuthorityDataList[]) {
+  const tree: AuthorityDataList[] = [];
+  const map = new Map();
+
+  routes.forEach((route) => {
+    map.set(route.menu_id, { ...route, children: [] });
+  });
+
+  // 根据parent_id 递归生成路由树
+  routes.forEach((route) => {
+    const node = map.get(route.menu_id);
+    if (route.parent_id === 0) {
+      // 顶级节点直接加入tree
+      tree.push(node);
+    } else {
+      // 非顶级节点加入到父节点的children中
+      const parent = map.get(route.parent_id);
+      if (parent) {
+        parent.children.push(node);
+      }
+    }
+  });
+
+  // 递归排序
+  function sortTree(nodes: AuthorityDataList[]) {
+    return nodes
+      .sort((a, b) => a.order_num - b.order_num)
+      .map((node) => {
+        if (node.children && node.children.length > 0) {
+          node.children = sortTree(node.children);
+        }
+        return node;
+      });
+  }
+
+  const sortedTree = sortTree(tree);
+
+  interface RouteItem {
+    children?: RouteItem[];
+    component?: string;
+    meta: RouteMeta;
+    parent?: string; // 当前菜单的直接父级路径
+    parents?: string[]; // 当前菜单的所有父级路径
+    name: string; // 菜单名称
+    path: string; // 菜单路径
+  }
+
+  // 递归生成路由树
+  function generateRoutes(nodes: AuthorityDataList[]): RouteItem[] {
+    return nodes.map((node) => {
+      const route: RouteItem = {
+        path: node.path,
+        name: node.menu_key,
+        component: node.component,
+        meta: {
+          title: node.menu_name,
+          icon: node.icon,
+          hideInMenu: node.visible === 1,
+          keepAlive: node.no_cache === 0,
+          hideInBreadcrumb: node.hidden_header === 1,
+          hideInTab: node.hidden_header === 1,
+          // affixTab: node.affix_tab === 1,
+          // affixTabOrder: node.affix_tab_order,
+          // maxNumOfOpenTab: node.max_num_of_open_tab,
+          // menuVisibleWithForbidden: node.menu_visible_with_forbidden === 1,
+          // noBasicLayout: node.no_basic_layout === 1,
+          // openInNewWindow: node.open_in_new_window === 1,
+          order: node.order_num,
+          query: node.api_parameter,
+          // redirect: node.redirect,
+          authority: node.perms ? node.perms.split(',') : [],
+          // hideChildrenInMenu: node.visible === 1,
+        },
+      };
+
+      // 处理直接父级路径
+      const parent =
+        node.parent_id === 0
+          ? null
+          : routes.find((n) => n.menu_id === node.parent_id)?.path || '';
+
+      if (parent) route.parent = parent;
+
+      // 递归处理子节点
+      if (node.children?.length) {
+        route.children = generateRoutes(node.children);
+      }
+
+      return route;
+    });
+  }
+  const treeRoutes = generateRoutes(sortedTree);
+
+  // 递归处理父级路径 parents
+  function handleParentPaths(routes: RouteItem[]) {
+    // 扁平化所有路由,包括子路由
+    function flattenRoutes(items: RouteItem[]): RouteItem[] {
+      const result: RouteItem[] = [];
+      for (const item of items) {
+        result.push(item);
+        if (item.children && item.children.length > 0) {
+          result.push(...flattenRoutes(item.children));
+        }
+      }
+      return result;
+    }
+
+    // 标准化路径格式
+    function normalizePath(path: string): string {
+      return path.startsWith('/') ? path : `/${path}`;
+    }
+
+    // 获取所有父级路径
+    function getAllParentPaths(
+      route: RouteItem,
+      allRoutes: RouteItem[],
+    ): string[] {
+      const parentPaths: string[] = [];
+      const visitedPaths = new Set<string>();
+      let currentPath = route.parent ? normalizePath(route.parent) : null;
+
+      while (currentPath && !visitedPaths.has(currentPath)) {
+        visitedPaths.add(currentPath);
+        parentPaths.unshift(currentPath);
+        const parentRoute = allRoutes.find(
+          (r) => normalizePath(r.path) === currentPath,
+        );
+        currentPath = parentRoute?.parent
+          ? normalizePath(parentRoute.parent)
+          : null;
+      }
+
+      return parentPaths;
+    }
+
+    // 一次性扁平化所有路由
+    const allFlattenedRoutes = flattenRoutes(routes);
+
+    function processRoutes(routesToProcess: RouteItem[]): RouteItem[] {
+      return routesToProcess.map((route) => {
+        const newRoute = { ...route };
+
+        if (newRoute.parent) {
+          newRoute.parent = normalizePath(newRoute.parent);
+          newRoute.parents = getAllParentPaths(newRoute, allFlattenedRoutes);
+        }
+        // 递归处理子路由,但使用同一个扁平化路由数组
+        if (newRoute.children && newRoute.children.length > 0) {
+          newRoute.children = processRoutes(newRoute.children);
+        }
+
+        return newRoute;
+      });
+    }
+
+    return processRoutes(routes);
+  }
+
+  const handledTreeRoutes = handleParentPaths(treeRoutes);
+
+  return handledTreeRoutes;
+}
+
 export { generateRoutesByBackend };