|
@@ -2,18 +2,25 @@ import type { RouteRecordRaw } from 'vue-router'
|
|
|
import type { MenuItem } from '@/types/menu'
|
|
import type { MenuItem } from '@/types/menu'
|
|
|
|
|
|
|
|
// Vite glob 懒加载所有视图组件
|
|
// Vite glob 懒加载所有视图组件
|
|
|
-// 注意:此文件位于 src/utils/,../views 指向 src/views
|
|
|
|
|
|
|
+// 此文件位于 src/utils/,所以 ../views 指向 src/views
|
|
|
const viewModules = import.meta.glob('../views/**/*.vue')
|
|
const viewModules = import.meta.glob('../views/**/*.vue')
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 根据字符串路径懒加载 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`
|
|
const key = `../views/${componentPath}.vue`
|
|
|
|
|
+
|
|
|
if (viewModules[key]) {
|
|
if (viewModules[key]) {
|
|
|
return viewModules[key] as () => Promise<unknown>
|
|
return viewModules[key] as () => Promise<unknown>
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
console.warn(`[路由] 未找到组件: ${key}`)
|
|
console.warn(`[路由] 未找到组件: ${key}`)
|
|
|
return () => import('../views/error/404.vue')
|
|
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 {
|
|
export function resolvePath(basePath: string, routePath: string): string {
|
|
|
|
|
+ if (!routePath) return basePath || '/'
|
|
|
if (routePath.startsWith('/')) return routePath
|
|
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[] = []
|
|
const routes: RouteRecordRaw[] = []
|
|
|
|
|
|
|
|
- menuItems.forEach(item => {
|
|
|
|
|
|
|
+ for (const item of menuItems) {
|
|
|
const fullPath = resolvePath(parentPath, item.path)
|
|
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({
|
|
routes.push({
|
|
|
path: fullPath,
|
|
path: fullPath,
|
|
|
name: item.name,
|
|
name: item.name,
|
|
|
- component: loadComponent(item.component!),
|
|
|
|
|
|
|
+ component: loadComponent(item.component),
|
|
|
meta: item.meta
|
|
meta: item.meta
|
|
|
} as RouteRecordRaw)
|
|
} 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({
|
|
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)
|
|
} as RouteRecordRaw)
|
|
|
}
|
|
}
|
|
|
- })
|
|
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
return routes
|
|
return routes
|
|
|
-}
|
|
|
|
|
|
|
+}
|