Kaynağa Gözat

refactor(路由): 优化路由生成逻辑并添加面包屑功能

重构路由生成工具函数,改进隐藏路由处理逻辑
添加面包屑导航支持显示父级路由信息
为管理详情按钮添加点击事件处理
piks 5 gün önce
ebeveyn
işleme
0a9c70a251

+ 24 - 5
src/components/Breadcrumb/index.vue

@@ -14,12 +14,31 @@ interface BreadcrumbItem {
 
 const breadcrumbs = computed<BreadcrumbItem[]>(() => {
   const matched = route.matched.filter(item => item.meta?.title)
+  const items: BreadcrumbItem[] = []
+
+  matched.forEach((item, index) => {
+    // 如果有父路由信息,先添加父路由
+    if (item.meta?.parentPath && item.meta?.parentTitle) {
+      // 检查是否已经添加过父路由
+      const parentExists = items.some(b => b.path === item.meta?.parentPath)
+      if (!parentExists) {
+        items.push({
+          title: item.meta.parentTitle as string,
+          path: item.meta.parentPath as string,
+          isCurrent: false
+        })
+      }
+    }
+
+    // 添加当前路由
+    items.push({
+      title: item.meta?.title as string,
+      path: item.path,
+      isCurrent: index === matched.length - 1
+    })
+  })
 
-  return matched.map((item, index) => ({
-    title: item.meta?.title as string,
-    path: item.path,
-    isCurrent: index === matched.length - 1
-  }))
+  return items
 })
 
 const canGoBack = computed(() => {

+ 1 - 1
src/mock/index.ts

@@ -31,7 +31,7 @@ const allAsyncRoutes: MenuItem[] = [
         path: "detail",
         name: "SdkDetail",
         component: 'sdk/detail',
-        meta: { title: "SDK管理详情", icon: "Box", roles: ["admin"], hidden: true },
+        meta: { title: "管理详情", icon: "Box", roles: ["admin"], hidden: true },
       }
     ]
   },

+ 71 - 30
src/utils/routeHelper.ts

@@ -2,18 +2,25 @@ import type { RouteRecordRaw } from 'vue-router'
 import type { MenuItem } from '@/types/menu'
 
 // Vite glob 懒加载所有视图组件
-// 注意:此文件位于 src/utils/,../views 指向 src/views
+// 此文件位于 src/utils/,所以 ../views 指向 src/views
 const viewModules = import.meta.glob('../views/**/*.vue')
 
 /**
  * 根据字符串路径懒加载 Vue 组件
- * @param componentPath - 如 'dashboard/index' 或 'system/user/index'
+ * @param componentPath - 如 'dashboard/index' 或 'sdk/detail'
  */
-export function loadComponent(componentPath: string): () => Promise<unknown> {
+export function loadComponent(componentPath?: string): () => Promise<unknown> {
+  if (!componentPath) {
+    console.warn('[路由] componentPath 为空')
+    return () => import('../views/error/404.vue')
+  }
+
   const key = `../views/${componentPath}.vue`
+
   if (viewModules[key]) {
     return viewModules[key] as () => Promise<unknown>
   }
+
   console.warn(`[路由] 未找到组件: ${key}`)
   return () => import('../views/error/404.vue')
 }
@@ -22,50 +29,84 @@ export function loadComponent(componentPath: string): () => Promise<unknown> {
  * 路径拼接(兼容绝对路径与相对路径)
  */
 export function resolvePath(basePath: string, routePath: string): string {
+  if (!routePath) return basePath || '/'
   if (routePath.startsWith('/')) return routePath
-  return `${basePath}/${routePath}`.replace(/\/+/g, '/')
+  const full = `${basePath}/${routePath}`.replace(/\/+/g, '/')
+  return full === '' ? '/' : full
+}
+
+/**
+ * 判断菜单项是否隐藏
+ */
+function isHidden(item: MenuItem): boolean {
+  return !!item.meta?.hidden
 }
 
 /**
- * 将嵌套菜单数据扁平化为 Vue Router 可用的路由配置
- * 所有动态路由最终挂载在 Root(Layout)下
+ * 将菜单树转换为 Vue Router 路由
+ * 规则:
+ * 1. 顶层菜单作为父路由
+ * 2. hidden 子菜单仍然生成路由,但不显示在侧边栏
+ * 3. 父路由如果有 visible 子路由,则可配置 redirect
+ * 4. 路由默认都挂载在根下(适合 Layout + router-view)
  */
-export function flattenMenuToRoutes(menuItems: MenuItem[], parentPath = ''): RouteRecordRaw[] {
+export function flattenMenuToRoutes(
+  menuItems: MenuItem[],
+  parentPath = ''
+): RouteRecordRaw[] {
   const routes: RouteRecordRaw[] = []
 
-  menuItems.forEach(item => {
+  for (const item of menuItems) {
     const fullPath = resolvePath(parentPath, item.path)
+    const children = item.children || []
 
-    // 过滤掉 hidden 的子菜单
-    const visibleChildren = item.children?.filter(child => !child.meta?.hidden) || []
+    const visibleChildren = children.filter(child => !isHidden(child))
+    const hiddenChildren = children.filter(child => isHidden(child))
 
-    if (visibleChildren.length > 0) {
-      // 父级菜单:注册 redirect 路由,再递归处理子项
-      routes.push({
-        path: fullPath,
-        redirect: item.redirect || resolvePath(fullPath, visibleChildren[0].path),
-        meta: item.meta
-      } as RouteRecordRaw)
-      routes.push(...flattenMenuToRoutes(visibleChildren, fullPath))
-    } else if (item.children && item.children.length > 0) {
-      // 有子菜单但都是隐藏的:注册父级路由 + 递归处理隐藏子项
+    // 1) 当前菜单本身生成路由
+    // 如果有 component,说明它是一个可访问页面
+    if (item.component) {
       routes.push({
         path: fullPath,
         name: item.name,
-        component: loadComponent(item.component!),
+        component: loadComponent(item.component),
         meta: item.meta
       } as RouteRecordRaw)
-      routes.push(...flattenMenuToRoutes(item.children, fullPath))
-    } else {
-      // 叶子路由:直接对应一个页面组件
+    }
+
+    // 2) 处理子菜单
+    // visible 子菜单:正常递归
+    if (visibleChildren.length > 0) {
+      // 如果父级没有 component,但有可见子路由,可以给父级加 redirect
+      // 例如 /sdk -> /sdk/list
+      if (!item.component) {
+        routes.push({
+          path: fullPath,
+          redirect: item.redirect || resolvePath(fullPath, visibleChildren[0].path),
+          meta: item.meta
+        } as RouteRecordRaw)
+      }
+
+      routes.push(...flattenMenuToRoutes(visibleChildren, fullPath))
+    }
+
+    // 3) hidden 子菜单:也要生成路由,但不递归进菜单树
+    // 因为它们不显示在侧边栏,但需要能直接访问
+    for (const child of hiddenChildren) {
+      const childFullPath = resolvePath(fullPath, child.path)
+
       routes.push({
-        path: fullPath,
-        name: item.name,
-        component: loadComponent(item.component!),
-        meta: item.meta
+        path: childFullPath,
+        name: child.name,
+        component: loadComponent(child.component),
+        meta: {
+          ...child.meta,
+          parentPath: fullPath,
+          parentTitle: item.meta?.title
+        }
       } as RouteRecordRaw)
     }
-  })
+  }
 
   return routes
-}
+}

+ 15 - 1
src/views/sdk/index.vue

@@ -34,7 +34,7 @@
       </template>
 
       <template #column-operation="{ row }">
-        <el-button type="primary" link>管理详情</el-button>
+        <el-button @click="btnClickHandler('detail', row)" type="primary" link>管理详情</el-button>
         <el-button type="primary" link>升级</el-button>
         <el-button type="primary" link>编辑</el-button>
         <el-button type="warning" link>续费</el-button>
@@ -55,6 +55,8 @@ const activeTab = ref('all')
 const searchKeyword = ref('')
 const selectedRows = ref<any[]>([])
 
+const router = useRouter()
+
 const state = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -108,6 +110,18 @@ const tableData = ref([
   }
 ])
 
+const btnClickHandler = (type: string, row: any) => {
+  const typeMap: Record<string, () => void> = {
+    detail() {
+      router.push({
+        path: "/sdk/detail",
+        query: row
+      })
+    },
+  }
+  typeMap[type]?.()
+}
+
 function handleTabChange(value: string | number) {
   console.log('切换到:', value)
 }