菜单布局等初步完成
This commit is contained in:
@@ -7,13 +7,16 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import type { SysMenu } from '@/types';
|
||||
import { MenuType } from '@/types/enums';
|
||||
import { routes } from '@/router';
|
||||
|
||||
/**
|
||||
* 布局组件映射
|
||||
*/
|
||||
const LAYOUT_MAP: Record<string, () => Promise<any>> = {
|
||||
// 基础布局
|
||||
// 基础布局(旧版,带侧边栏)
|
||||
'BasicLayout': () => import('@/layouts/BasicLayout.vue'),
|
||||
// 导航布局(新版,顶部导航+动态侧边栏)
|
||||
'NavigationLayout': () => import('@/layouts/NavigationLayout.vue'),
|
||||
// 空白布局
|
||||
'BlankLayout': () => import('@/layouts/BlankLayout.vue'),
|
||||
// 页面布局
|
||||
@@ -49,14 +52,20 @@ export function generateRoutes(menus: SysMenu[]): RouteRecordRaw[] {
|
||||
/**
|
||||
* 根据单个菜单生成路由
|
||||
* @param menu 菜单对象
|
||||
* @param isTopLevel 是否是顶层菜单
|
||||
* @returns 路由配置
|
||||
*/
|
||||
function generateRouteFromMenu(menu: SysMenu): RouteRecordRaw | null {
|
||||
// 只处理目录和菜单类型,忽略按钮类型
|
||||
function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw | null {
|
||||
// 跳过按钮类型
|
||||
if (menu.type === MenuType.BUTTON) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 跳过静态路由(已经在 router 中定义,不需要再次添加)
|
||||
if (menu.component === '__STATIC_ROUTE__') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const route: any = {
|
||||
path: menu.url || `/${menu.menuID}`,
|
||||
name: menu.menuID,
|
||||
@@ -72,37 +81,71 @@ function generateRouteFromMenu(menu: SysMenu): RouteRecordRaw | null {
|
||||
}
|
||||
};
|
||||
|
||||
// 根据菜单类型处理组件
|
||||
if (menu.type === MenuType.DIRECTORY) {
|
||||
// 目录类型 - 使用布局组件
|
||||
route.component = getComponent(menu.component || 'BasicLayout');
|
||||
|
||||
// 处理子菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
route.children = [];
|
||||
menu.children.forEach(child => {
|
||||
const childRoute = generateRouteFromMenu(child);
|
||||
if (childRoute) {
|
||||
route.children!.push(childRoute);
|
||||
}
|
||||
});
|
||||
// 如果有子菜单,使用布局组件
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
// 如果是顶层的NAVIGATION类型菜单,使用NavigationLayout
|
||||
if (isTopLevel && menu.type === MenuType.NAVIGATION) {
|
||||
route.component = getComponent(menu.component || 'NavigationLayout');
|
||||
} else if (menu.type === MenuType.SIDEBAR) {
|
||||
// SIDEBAR类型的菜单使用BlankLayout,避免嵌套布局
|
||||
// BlankLayout 只是一个纯容器,不会添加额外的导航栏或面包屑
|
||||
route.component = getComponent(menu.component || 'BlankLayout');
|
||||
} else {
|
||||
// 如果是目录但没有子菜单,设置重定向
|
||||
route.redirect = route.path + '/index';
|
||||
// 其他情况使用BasicLayout
|
||||
route.component = getComponent(menu.component || 'BasicLayout');
|
||||
}
|
||||
|
||||
} else if (menu.type === MenuType.MENU) {
|
||||
// 菜单类型 - 使用页面组件
|
||||
if (!menu.component) {
|
||||
console.warn(`菜单 ${menu.name} 缺少component字段`);
|
||||
return null;
|
||||
} else {
|
||||
// 没有子菜单,使用具体的页面组件
|
||||
if (menu.component) {
|
||||
route.component = getComponent(menu.component);
|
||||
} else {
|
||||
// 如果没有指定组件,使用BlankLayout作为默认
|
||||
route.component = getComponent('BlankLayout');
|
||||
}
|
||||
route.component = getComponent(menu.component);
|
||||
}
|
||||
|
||||
// 处理子菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
route.children = [];
|
||||
menu.children.forEach(child => {
|
||||
const childRoute = generateRouteFromMenu(child, false);
|
||||
if (childRoute) {
|
||||
route.children!.push(childRoute);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有设置重定向,自动重定向到第一个有URL的子菜单
|
||||
if (!route.redirect && route.children.length > 0) {
|
||||
const firstChildWithUrl = findFirstMenuWithUrl(menu.children);
|
||||
if (firstChildWithUrl?.url) {
|
||||
route.redirect = firstChildWithUrl.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个有URL的菜单
|
||||
* @param menus 菜单列表
|
||||
* @returns 第一个有URL的菜单
|
||||
*/
|
||||
function findFirstMenuWithUrl(menus: SysMenu[]): SysMenu | null {
|
||||
for (const menu of menus) {
|
||||
if (menu.type !== MenuType.BUTTON) {
|
||||
if (menu.url) {
|
||||
return menu;
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findFirstMenuWithUrl(menu.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据组件名称获取组件
|
||||
* @param componentName 组件名称/路径
|
||||
@@ -119,41 +162,101 @@ function getComponent(componentName: string) {
|
||||
|
||||
// 如果不是以@/开头的完整路径,则添加@/views/前缀
|
||||
if (!componentPath.startsWith('@/')) {
|
||||
// 如果不是以/开头,添加/
|
||||
// 确保路径以/开头
|
||||
if (!componentPath.startsWith('/')) {
|
||||
componentPath = '/' + componentPath;
|
||||
}
|
||||
// 添加@/views前缀
|
||||
componentPath = '@/views' + componentPath;
|
||||
}
|
||||
|
||||
// 将@/别名转换为相对路径,因为Vite动态导入可能无法正确解析别名
|
||||
if (componentPath.startsWith('@/')) {
|
||||
componentPath = componentPath.replace('@/', '../');
|
||||
}
|
||||
|
||||
// 如果没有.vue扩展名,添加它
|
||||
if (!componentPath.endsWith('.vue')) {
|
||||
componentPath += '.vue';
|
||||
}
|
||||
|
||||
// 动态导入组件
|
||||
return () => import(/* @vite-ignore */ componentPath).catch((error) => {
|
||||
console.warn(`组件加载失败: ${componentPath}`, error);
|
||||
// 返回404组件或空组件
|
||||
return import('@/views/error/404.vue').catch(() =>
|
||||
Promise.resolve({
|
||||
template: `<div class="component-error">
|
||||
<h3>组件加载失败</h3>
|
||||
<p>无法加载组件: ${componentPath}</p>
|
||||
<p>错误: ${error.message}</p>
|
||||
</div>`,
|
||||
style: `
|
||||
.component-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #f56565;
|
||||
background: #fed7d7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`
|
||||
})
|
||||
);
|
||||
return () => {
|
||||
try {
|
||||
// 使用动态导入,Vite 会自动处理路径解析
|
||||
return import(/* @vite-ignore */ componentPath);
|
||||
} catch (error) {
|
||||
console.warn(`组件加载失败: ${componentPath}`, error);
|
||||
// 返回404组件
|
||||
return import('@/views/error/404.vue').catch(() =>
|
||||
Promise.resolve({
|
||||
template: `<div class="component-error">
|
||||
<h3>组件加载失败</h3>
|
||||
<p>无法加载组件: ${componentPath}</p>
|
||||
<p>错误: ${error instanceof Error ? error.message : String(error)}</p>
|
||||
</div>`,
|
||||
style: `
|
||||
.component-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #f56565;
|
||||
background: #fed7d7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将静态路由转换为菜单项
|
||||
* @param routes 静态路由数组
|
||||
* @returns 菜单项数组
|
||||
*/
|
||||
function convertRoutesToMenus(routes: RouteRecordRaw[]): SysMenu[] {
|
||||
const menus: SysMenu[] = [];
|
||||
|
||||
routes.forEach(route => {
|
||||
// 处理有子路由的情况(现在静态路由都有布局组件)
|
||||
if (route.children && route.children.length > 0) {
|
||||
route.children.forEach(child => {
|
||||
// 只处理有 meta.menuType 的子路由
|
||||
if (child.meta?.menuType !== undefined) {
|
||||
const menu: SysMenu = {
|
||||
menuID: child.name as string || child.path.replace(/\//g, '-'),
|
||||
parentID: '0',
|
||||
name: child.meta.title as string || child.name as string,
|
||||
url: route.path, // 使用父路由的路径
|
||||
type: child.meta.menuType as MenuType,
|
||||
orderNum: (child.meta.orderNum as number) || -1,
|
||||
// 标记为静态路由,避免重复生成路由
|
||||
component: '__STATIC_ROUTE__', // 特殊标记
|
||||
};
|
||||
|
||||
menus.push(menu);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 处理没有子路由的情况(兼容性保留)
|
||||
else if (route.meta?.menuType !== undefined) {
|
||||
const menu: SysMenu = {
|
||||
menuID: route.name as string || route.path.replace(/\//g, '-'),
|
||||
parentID: '0',
|
||||
name: route.meta.title as string || route.name as string,
|
||||
url: route.path,
|
||||
type: route.meta.menuType as MenuType,
|
||||
orderNum: (route.meta.orderNum as number) || -1,
|
||||
// 标记为静态路由,避免重复生成路由
|
||||
component: '__STATIC_ROUTE__', // 特殊标记
|
||||
};
|
||||
|
||||
menus.push(menu);
|
||||
}
|
||||
});
|
||||
|
||||
return menus;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,8 +264,14 @@ function getComponent(componentName: string) {
|
||||
* @param menus 菜单列表
|
||||
* @returns 菜单树
|
||||
*/
|
||||
function buildMenuTree(menus: SysMenu[]): SysMenu[] {
|
||||
if (!menus || menus.length === 0) {
|
||||
export function buildMenuTree(menus: SysMenu[]): SysMenu[] {
|
||||
// 将静态路由转换为菜单项
|
||||
const staticMenus = convertRoutesToMenus(routes);
|
||||
|
||||
// 合并动态菜单和静态菜单
|
||||
const allMenus = [...staticMenus, ...menus];
|
||||
|
||||
if (allMenus.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -170,14 +279,14 @@ function buildMenuTree(menus: SysMenu[]): SysMenu[] {
|
||||
const rootMenus: SysMenu[] = [];
|
||||
|
||||
// 创建菜单映射
|
||||
menus.forEach(menu => {
|
||||
allMenus.forEach(menu => {
|
||||
if (menu.menuID) {
|
||||
menuMap.set(menu.menuID, { ...menu, children: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// 构建树结构
|
||||
menus.forEach(menu => {
|
||||
allMenus.forEach(menu => {
|
||||
const menuNode = menuMap.get(menu.menuID!);
|
||||
if (!menuNode) return;
|
||||
|
||||
@@ -293,3 +402,16 @@ export function getMenuPath(menus: SysMenu[], targetMenuId: string): SysMenu[] {
|
||||
findPath(menus);
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取第一个可访问的菜单URL(用于登录后跳转)
|
||||
* @param menus 菜单树
|
||||
* @returns 第一个可访问的菜单URL,如果没有则返回 null
|
||||
*/
|
||||
export function getFirstAccessibleMenuUrl(menus: SysMenu[]): string | null {
|
||||
if (!menus || menus.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return "/home";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user