feat(composables): 提取菜单选项生成逻辑到独立 composables
This commit is contained in:
2
auto-imports.d.ts
vendored
2
auto-imports.d.ts
vendored
@@ -224,6 +224,7 @@ declare global {
|
|||||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||||
const useMessage: typeof import('naive-ui')['useMessage']
|
const useMessage: typeof import('naive-ui')['useMessage']
|
||||||
|
const useMetaLayoutsNMenuOptions: typeof import('./src/composables/useMetaLayoutsMenuOptions')['useMetaLayoutsNMenuOptions']
|
||||||
const useModal: typeof import('naive-ui')['useModal']
|
const useModal: typeof import('naive-ui')['useModal']
|
||||||
const useModel: typeof import('vue')['useModel']
|
const useModel: typeof import('vue')['useModel']
|
||||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||||
@@ -560,6 +561,7 @@ declare module 'vue' {
|
|||||||
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
||||||
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
||||||
readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']>
|
readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']>
|
||||||
|
readonly useMetaLayoutsNMenuOptions: UnwrapRef<typeof import('./src/composables/useMetaLayoutsMenuOptions')['useMetaLayoutsNMenuOptions']>
|
||||||
readonly useModal: UnwrapRef<typeof import('naive-ui')['useModal']>
|
readonly useModal: UnwrapRef<typeof import('naive-ui')['useModal']>
|
||||||
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
||||||
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
||||||
|
|||||||
150
src/composables/useMetaLayoutsMenuOptions.tsx
Normal file
150
src/composables/useMetaLayoutsMenuOptions.tsx
Normal file
@@ -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<MenuInst | null> }) {
|
||||||
|
// 路由转换为菜单树的辅助函数
|
||||||
|
function convertRoutesToNMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOption[] {
|
||||||
|
const menuMap = new Map<string, MenuOption>();
|
||||||
|
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 : <RouterLink to={route}>{text}</RouterLink>,
|
||||||
|
key: route.path,
|
||||||
|
icon: () => <IconMenuRounded style="width: 1em; height: 1em;" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果是根路径或只有一级路径,直接添加到根菜单
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,146 +1,10 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { createGetRoutes, router } from '@/plugins/00.router-plugin';
|
|
||||||
import type { MenuOption } from 'naive-ui';
|
|
||||||
import { RouterLink, type RouteRecordRaw } from 'vue-router';
|
|
||||||
import IconMenuRounded from '~icons/material-symbols/menu-rounded';
|
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
|
|
||||||
import zhCN from '@/pages/_page-title-locales/zh-CN';
|
|
||||||
import enUS from '@/pages/_page-title-locales/en-US';
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 路由转换为菜单树的辅助函数
|
|
||||||
function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOption[] {
|
|
||||||
const menuMap = new Map<string, MenuOption>();
|
|
||||||
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 : <RouterLink to={route}>{text}</RouterLink>),
|
|
||||||
key: route.path,
|
|
||||||
icon: () => <IconMenuRounded style="width: 1em; height: 1em;" />,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果是根路径或只有一级路径,直接添加到根菜单
|
|
||||||
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 {
|
|
||||||
console.warn(`未找到父菜单项: ${parentPath},无法将子菜单项添加到其下。`);
|
|
||||||
}
|
|
||||||
|
|
||||||
menuMap.set(route.path, menuOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rootMenus;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取路由表但是不包含布局路由
|
|
||||||
const routes = createGetRoutes(router)();
|
|
||||||
const menuOptions = computed(() => convertRoutesToMenuOptions(routes));
|
|
||||||
|
|
||||||
// console.debug('原始路由:', JSON.stringify(routes, null, 0));
|
|
||||||
// console.debug('转换后的菜单:', JSON.stringify(menuOptions.value, null, 0));
|
|
||||||
|
|
||||||
const menuInstRef = useTemplateRef('menuInstRef');
|
const menuInstRef = useTemplateRef('menuInstRef');
|
||||||
const selectedKey = ref('');
|
const { options, selectedKey } = useMetaLayoutsNMenuOptions({
|
||||||
|
menuInstRef,
|
||||||
watch(
|
});
|
||||||
() => router.currentRoute.value.path,
|
|
||||||
(newPath) => {
|
|
||||||
menuInstRef.value?.showOption(newPath);
|
|
||||||
selectedKey.value = newPath;
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
</script>
|
</script>
|
||||||
@@ -148,14 +12,14 @@ const appStore = useAppStore();
|
|||||||
<template>
|
<template>
|
||||||
<!-- @update:value="handleMenuUpdate" -->
|
<!-- @update:value="handleMenuUpdate" -->
|
||||||
<NMenu
|
<NMenu
|
||||||
ref="menuInstRef"
|
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
|
ref="menuInstRef"
|
||||||
:collapsed="appStore.sidebarCollapsed"
|
:collapsed="appStore.sidebarCollapsed"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
:icon-size="20"
|
:icon-size="20"
|
||||||
:collapsed-icon-size="24"
|
:collapsed-icon-size="24"
|
||||||
v-model:value="selectedKey"
|
v-model:value="selectedKey"
|
||||||
:options="menuOptions"
|
:options="options"
|
||||||
:inverted="false"
|
:inverted="false"
|
||||||
:root-indent="32"
|
:root-indent="32"
|
||||||
:indent="32"
|
:indent="32"
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ declare module 'vue-router' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { router, setupLayoutsResult };
|
export { router, setupLayoutsResult };
|
||||||
export { createGetRoutes } from 'virtual:meta-layouts';
|
|
||||||
declare global {
|
declare global {
|
||||||
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
|
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default [
|
|||||||
// https://github.com/antfu/unplugin-auto-import
|
// https://github.com/antfu/unplugin-auto-import
|
||||||
AutoImport({
|
AutoImport({
|
||||||
dirs: [
|
dirs: [
|
||||||
// 'src/composables',
|
'src/composables',
|
||||||
// 'src/utils',
|
// 'src/utils',
|
||||||
'src/stores',
|
'src/stores',
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user