diff --git a/auto-imports.d.ts b/auto-imports.d.ts index fa480c2..4ad6055 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -224,6 +224,7 @@ declare global { const useMemoize: typeof import('@vueuse/core')['useMemoize'] const useMemory: typeof import('@vueuse/core')['useMemory'] const useMessage: typeof import('naive-ui')['useMessage'] + const useMetaLayoutsNMenuOptions: typeof import('./src/composables/useMetaLayoutsMenuOptions')['useMetaLayoutsNMenuOptions'] const useModal: typeof import('naive-ui')['useModal'] const useModel: typeof import('vue')['useModel'] const useMounted: typeof import('@vueuse/core')['useMounted'] @@ -560,6 +561,7 @@ declare module 'vue' { readonly useMemoize: UnwrapRef readonly useMemory: UnwrapRef readonly useMessage: UnwrapRef + readonly useMetaLayoutsNMenuOptions: UnwrapRef readonly useModal: UnwrapRef readonly useModel: UnwrapRef readonly useMounted: UnwrapRef diff --git a/src/composables/useMetaLayoutsMenuOptions.tsx b/src/composables/useMetaLayoutsMenuOptions.tsx new file mode 100644 index 0000000..ec4bf62 --- /dev/null +++ b/src/composables/useMetaLayoutsMenuOptions.tsx @@ -0,0 +1,150 @@ +import enUS from '@/pages/_page-title-locales/en-US'; +import zhCN from '@/pages/_page-title-locales/zh-CN'; + +import type { Ref } from 'vue'; +import type { MenuInst, MenuOption } from 'naive-ui'; +import { createGetRoutes } from 'virtual:meta-layouts'; +import { RouterLink, type RouteRecordRaw } from 'vue-router'; +import IconMenuRounded from '~icons/material-symbols/menu-rounded'; + +export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref }) { + // 路由转换为菜单树的辅助函数 + function convertRoutesToNMenuOptions(routes: Readonly): MenuOption[] { + const menuMap = new Map(); + const rootMenus: MenuOption[] = []; + + // 过滤和排序路由 + const validRoutes = routes + .filter((route) => { + // 过滤掉不需要显示的路由 + if (route.meta?.hideInMenu === true || route.meta?.layout === false) { + return false; + } + // 过滤掉通配符路径 + if (route.path.includes('*')) { + return false; + } + // 根据环境变量判断是否显示 /demos 开头的路由 + if (route.path.startsWith('/demos') && import.meta.env.VITE_MENU_SHOW_DEMOS !== 'true') { + return false; + } + return true; + }) + // 排序路由,确保父路由总是在子路由之前,同级路由则根据 `meta.order` 排序 + .sort((a: RouteRecordRaw, b: RouteRecordRaw) => { + const pathA = a.path; + const pathB = b.path; + const segmentsA = pathA.split('/').filter(Boolean); + const segmentsB = pathB.split('/').filter(Boolean); + const parentAPath = `/${segmentsA.slice(0, -1).join('/')}`; + const parentBPath = `/${segmentsB.slice(0, -1).join('/')}`; + + // 如果不是同级路由,则按路径排序,确保父路由在前 + if (parentAPath !== parentBPath) { + return pathA.localeCompare(pathB); + } + + // 同级路由,处理 `meta.order` + const orderA = a.meta?.order; + const orderB = b.meta?.order; + const hasOrderA = orderA !== undefined; + const hasOrderB = orderB !== undefined; + + // 当一个有 order 而另一个没有时,有 order 的排在前面 + if (hasOrderA !== hasOrderB) { + return hasOrderA ? -1 : 1; + } + + // 当两个都有 order 时,按 order 值升序排序 + if (hasOrderA && hasOrderB) { + const orderDiff = orderA - orderB; + if (orderDiff !== 0) { + return orderDiff; + } + } + + // order 相同或都没有 order,按路径字母顺序排序 + return pathA.localeCompare(pathB); + }); + + // 构建菜单树 + for (const route of validRoutes) { + const pathSegments = route.path.split('/').filter(Boolean); + const routeName = route.name as string; + const text = te(routeName) ? t(routeName) : route.meta?.title || routeName; + + const menuOption: MenuOption = { + label: () => + route.meta?.link === false ? text : {text}, + key: route.path, + icon: () => , + }; + + // 如果是根路径或只有一级路径,直接添加到根菜单 + if (pathSegments.length === 0 || pathSegments.length === 1) { + rootMenus.push(menuOption); + menuMap.set(route.path, menuOption); + } else { + // 多级路径,需要创建或找到父菜单 + let currentPath = ''; + for (let i = 0; i < pathSegments.length - 1; i++) { + currentPath += `/${pathSegments[i]}`; + } + + // 将当前菜单项添加到父菜单 + const parentPath = currentPath; + const parent = menuMap.get(parentPath); + if (parent) { + if (!parent.children) { + parent.children = []; + } + parent.children.push(menuOption); + } else { + consola.warn(`未找到父菜单项: ${parentPath},无法将子菜单项添加到其下。`); + } + + menuMap.set(route.path, menuOption); + } + } + + return rootMenus; + } + + const router = useRouter(); + + const { t, te } = useI18n({ + inheritLocale: true, + useScope: 'local', + missing: (locale, key) => { + consola.warn(`菜单翻译缺失: locale=${locale}, key=${key}`); + return key; + }, + fallbackRoot: true, + messages: { + 'zh-CN': zhCN, + 'en-US': enUS, + }, + }); + + // 获取路由表但是不包含布局路由 + const routes = createGetRoutes(router)(); + + const options = computed(() => convertRoutesToNMenuOptions(routes)); + const selectedKey = ref(''); + + watch( + () => router.currentRoute.value.path, + (newPath) => { + menuInstRef.value?.showOption(newPath); + selectedKey.value = newPath; + }, + { immediate: true }, + ); + + // console.debug('原始路由:', JSON.stringify(routes, null, 0)); + // console.debug('转换后的菜单:', JSON.stringify(menuOptions.value, null, 0)); + return { + options, + selectedKey, + }; +} diff --git a/src/layouts/base-layout/base-layout-sider.vue b/src/layouts/base-layout/base-layout-sider.vue index 516a2c8..ce9a72b 100644 --- a/src/layouts/base-layout/base-layout-sider.vue +++ b/src/layouts/base-layout/base-layout-sider.vue @@ -1,146 +1,10 @@ @@ -148,14 +12,14 @@ const appStore = useAppStore();