feat(layout): 优化侧边栏菜单生成逻辑并增强类型安全
All checks were successful
CI/CD Pipeline / playwright (push) Successful in 2m11s
CI/CD Pipeline / build-and-deploy (push) Successful in 4m15s

This commit is contained in:
严浩
2025-10-22 13:22:04 +08:00
parent 4949e1c957
commit e95d883c23
4 changed files with 42 additions and 54 deletions

View File

@@ -16,5 +16,8 @@
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.autoImportFileExcludePatterns": ["vue-router/auto$"]
} }

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="tsx">
import type { MenuOption } from 'naive-ui';
import type { RouteRecordNormalized } from 'vue-router';
import { createGetRoutes, router } from '@/plugins/router-plugin'; import { createGetRoutes, router } from '@/plugins/router-plugin';
import type { MenuOption } from 'naive-ui';
import { RouterLink, type RouteRecordRaw } from 'vue-router';
// 路由转换为菜单树的辅助函数 // 路由转换为菜单树的辅助函数
function convertRoutesToMenuOptions(routes: RouteRecordNormalized[]): MenuOption[] { function convertRoutesToMenuOptions(routes: Readonly<RouteRecordRaw[]>): MenuOption[] {
const menuMap = new Map<string, MenuOption>(); const menuMap = new Map<string, MenuOption>();
const rootMenus: MenuOption[] = []; const rootMenus: MenuOption[] = [];
@@ -12,7 +12,7 @@ function convertRoutesToMenuOptions(routes: RouteRecordNormalized[]): MenuOption
const validRoutes = routes const validRoutes = routes
.filter((route) => { .filter((route) => {
// 过滤掉不需要显示的路由 // 过滤掉不需要显示的路由
if (!route.name || route.meta?.hidden === true || route.meta?.layout === false) { if (route.meta?.hidden === true || route.meta?.layout === false) {
return false; return false;
} }
// 过滤掉通配符路径 // 过滤掉通配符路径
@@ -27,7 +27,9 @@ function convertRoutesToMenuOptions(routes: RouteRecordNormalized[]): MenuOption
for (const route of validRoutes) { for (const route of validRoutes) {
const pathSegments = route.path.split('/').filter(Boolean); const pathSegments = route.path.split('/').filter(Boolean);
const menuOption: MenuOption = { const menuOption: MenuOption = {
label: route.meta?.title || (route.name as string), label: () => (
<RouterLink to={route}>{route.meta?.title || (route.name as string)}</RouterLink>
),
key: route.path, key: route.path,
}; };
@@ -40,32 +42,6 @@ function convertRoutesToMenuOptions(routes: RouteRecordNormalized[]): MenuOption
let currentPath = ''; let currentPath = '';
for (let i = 0; i < pathSegments.length - 1; i++) { for (let i = 0; i < pathSegments.length - 1; i++) {
currentPath += `/${pathSegments[i]}`; currentPath += `/${pathSegments[i]}`;
if (!menuMap.has(currentPath)) {
// 创建父菜单节点
const parentMenu: MenuOption = {
label: pathSegments[i],
key: currentPath,
children: [],
};
if (i === 0) {
// 顶级父节点
rootMenus.push(parentMenu);
} else {
// 找到祖父节点并添加
const grandParentPath = currentPath.substring(0, currentPath.lastIndexOf('/'));
const grandParent = menuMap.get(grandParentPath);
if (grandParent) {
if (!grandParent.children) {
grandParent.children = [];
}
grandParent.children.push(parentMenu);
}
}
menuMap.set(currentPath, parentMenu);
}
} }
// 将当前菜单项添加到父菜单 // 将当前菜单项添加到父菜单
@@ -85,22 +61,33 @@ function convertRoutesToMenuOptions(routes: RouteRecordNormalized[]): MenuOption
return rootMenus; return rootMenus;
} }
// 获取路由表但是不包含布局路由
const routes = createGetRoutes(router)(); const routes = createGetRoutes(router)();
const menuOptions = computed(() => convertRoutesToMenuOptions(routes)); const menuOptions = computed(() => convertRoutesToMenuOptions(routes));
console.debug('原始路由:', JSON.stringify(routes, null, 2)); console.debug('原始路由:', JSON.stringify(routes, null, 0));
console.debug('转换后的菜单:', JSON.stringify(menuOptions.value, null, 2)); console.debug('转换后的菜单:', JSON.stringify(menuOptions.value, null, 0));
// 处理菜单点击,导航到对应路由 const menuInstRef = useTemplateRef('menuInstRef');
const handleMenuUpdate = (key: string) => { const selectedKey = ref('');
// 只有当 key 对应一个实际路由时才导航
const route = routes.find((r) => r.path === key); watch(
if (route && !route.children?.length) { () => router.currentRoute.value.path,
router.push(key); (newPath) => {
} menuInstRef.value?.showOption(newPath);
}; selectedKey.value = newPath;
},
{ immediate: true },
);
</script> </script>
<template> <template>
<NMenu :options="menuOptions" @update:value="handleMenuUpdate" /> <!-- @update:value="handleMenuUpdate" -->
<NMenu
v-model:value="selectedKey"
ref="menuInstRef"
:options="menuOptions"
:root-indent="32"
:indent="32"
/>
</template> </template>

View File

@@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
definePage({ meta: { title: '示例演示' } }); definePage({ meta: { title: '示例演示' } });
</script> </script>
<template>这个文件只是为了给菜单渲染标题</template> <template><div>此页面文件仅用于在侧边栏菜单中显示示例演示分组标题</div></template>

View File

@@ -2,7 +2,7 @@ import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders';
import { setupLayouts } from 'virtual:meta-layouts'; import { setupLayouts } from 'virtual:meta-layouts';
// import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts'; // import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts';
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import { handleHotUpdate, routes } from 'vue-router/auto-routes'; import { routes, handleHotUpdate } from 'vue-router/auto-routes';
const setupLayoutsResult = setupLayouts(routes); const setupLayoutsResult = setupLayouts(routes);
const router = createRouter({ const router = createRouter({
@@ -14,12 +14,10 @@ const router = createRouter({
strict: true, strict: true,
}); });
if (__DEV__) Object.assign(globalThis, { router });
router.onError((error) => { router.onError((error) => {
console.debug('🚨 [router error]:', error); console.debug('🚨 [router error]:', error);
}); });
export { router, setupLayoutsResult };
export function install({ app }: { app: import('vue').App<Element> }) { export function install({ app }: { app: import('vue').App<Element> }) {
app app
// 在路由之前注册插件 // 在路由之前注册插件
@@ -37,12 +35,8 @@ export function install({ app }: { app: import('vue').App<Element> }) {
Object.assign(globalThis, { stack: createStackGuard(router) }); Object.assign(globalThis, { stack: createStackGuard(router) });
} }
/*
definePage({
meta: { },
});
*/
declare module 'vue-router' { declare module 'vue-router' {
/* definePage({ meta: { title: '示例演示' } }); */
interface RouteMeta { interface RouteMeta {
/** /**
* @description 是否在菜单中隐藏 * @description 是否在菜单中隐藏
@@ -60,7 +54,11 @@ declare module 'vue-router' {
} }
} }
export { router, setupLayoutsResult };
export { createGetRoutes } from 'virtual:meta-layouts'; export { createGetRoutes } from 'virtual:meta-layouts';
if (__DEV__) Object.assign(globalThis, { router });
// This will update routes at runtime without reloading the page // This will update routes at runtime without reloading the page
if (import.meta.hot) handleHotUpdate(router); if (import.meta.hot) {
handleHotUpdate(router);
}