feat: 优化路由 搭建赛事管理页面基础 处理部分样式问题 完成赛事列表页面搭建
This commit is contained in:
+107
-73
@@ -4,6 +4,7 @@ import type { MenuNode, MenuItemRaw } from '@/types';
|
||||
import router, { resetRouter } from '@/router';
|
||||
import { fetchMenuTree } from '@/api/menu';
|
||||
import { pageModules } from '@/router/glob';
|
||||
import { FALLBACK_MENU_NODES } from '@/config/fallbackRoutes';
|
||||
|
||||
/**
|
||||
* 使用 import.meta.glob 动态收集 src/pages/ 下所有页面组件
|
||||
@@ -24,15 +25,11 @@ import { pageModules } from '@/router/glob';
|
||||
function resolveComponent(component?: string): RouteRecordRaw['component'] | undefined {
|
||||
if (!component) return undefined;
|
||||
|
||||
// 构建 glob key
|
||||
const globKey = `/src/pages/${component}/index.tsx`;
|
||||
|
||||
// 直接匹配
|
||||
if (pageModules[globKey]) {
|
||||
return pageModules[globKey];
|
||||
}
|
||||
|
||||
// 兜底:尝试带 @ 前缀匹配(Vite alias)
|
||||
const aliasKey = `@/pages/${component}/index.tsx`;
|
||||
if (pageModules[aliasKey]) {
|
||||
return pageModules[aliasKey];
|
||||
@@ -60,43 +57,94 @@ const state = reactive<MenuState>({
|
||||
homePath: '/dashboard',
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 辅助函数
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 获取节点的可用子节点(排除 disabled 和 externalLink)
|
||||
*/
|
||||
function getEnabledChildren(node: MenuNode): MenuNode[] {
|
||||
if (!node.children?.length) return [];
|
||||
return node.children.filter((child) => !child.disabled && !child.externalLink);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端 MenuNode 递归转为 antd Menu 的 items 格式
|
||||
*
|
||||
* 规则:
|
||||
* - 过滤 disabled / hideInMenu / externalLink 节点
|
||||
* - 父节点有 enabledChildren 时递归转换;若所有子节点都被过滤且父节点无 component,则父节点也不显示
|
||||
* - key 统一加 '/' 前缀,与 route.path 匹配(如 '/dashboard'、'/events/list')
|
||||
*/
|
||||
function transformMenuNode(nodes: MenuNode[]): MenuItemRaw[] {
|
||||
return nodes
|
||||
.filter((node) => !node.hideInMenu && !node.externalLink)
|
||||
.sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||
.map((node) => {
|
||||
const item: MenuItemRaw = {
|
||||
key: node.path,
|
||||
label: node.name,
|
||||
iconName: node.icon,
|
||||
};
|
||||
const result: MenuItemRaw[] = [];
|
||||
|
||||
if (node.children?.length) {
|
||||
item.children = transformMenuNode(node.children);
|
||||
for (const node of nodes) {
|
||||
if (node.disabled || node.hideInMenu || node.externalLink) continue;
|
||||
|
||||
const enabledChildren = getEnabledChildren(node);
|
||||
const item: MenuItemRaw = {
|
||||
key: '/' + node.path,
|
||||
label: node.name,
|
||||
iconName: node.icon,
|
||||
};
|
||||
|
||||
if (enabledChildren.length > 0) {
|
||||
const childItems = transformMenuNode(enabledChildren);
|
||||
if (childItems.length > 0) {
|
||||
item.children = childItems;
|
||||
}
|
||||
// 子节点全部被过滤 → 父节点也不显示
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
result.push(item);
|
||||
}
|
||||
|
||||
// 排序
|
||||
result.sort((a, b) => {
|
||||
const aNode = nodes.find((n) => '/' + n.path === a.key);
|
||||
const bNode = nodes.find((n) => '/' + n.path === b.key);
|
||||
return (aNode?.sort ?? 0) - (bNode?.sort ?? 0);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端 MenuNode 递归转为 vue-router RouteRecordRaw
|
||||
* component 字段通过 resolveComponent() 动态解析
|
||||
*
|
||||
* node.path 存储完整路径(如 'events/list'),
|
||||
* 但 Vue Router 嵌套路由下子节点的 path 必须相对于父节点(如 'list')。
|
||||
* 通过 parentPath 参数实现自动剥离前缀。
|
||||
*
|
||||
* 规则:
|
||||
* - 跳过 disabled / externalLink 节点
|
||||
* - 父节点无 component 且所有子节点被过滤 → 跳过
|
||||
* - 父节点无 component 时,redirect 自动指向第一个可用子路由(相对路径)
|
||||
*/
|
||||
function transformMenuToRoutes(nodes: MenuNode[]): RouteRecordRaw[] {
|
||||
function transformMenuToRoutes(nodes: MenuNode[], parentPath = ''): RouteRecordRaw[] {
|
||||
const routes: RouteRecordRaw[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
// 外链节点不注册路由
|
||||
if (node.externalLink) continue;
|
||||
if (node.disabled || node.externalLink) continue;
|
||||
|
||||
const enabledChildren = getEnabledChildren(node);
|
||||
const componentLoader = resolveComponent(node.component);
|
||||
|
||||
// 无组件且无可用子节点 → 跳过
|
||||
if (!componentLoader && enabledChildren.length === 0) continue;
|
||||
|
||||
// 计算 Vue Router 路由 path: 根节点用完整路径,子节点剥离父级前缀
|
||||
const routePath = parentPath
|
||||
? node.path.slice(parentPath.length + 1) // 'events/list' → 'list'
|
||||
: node.path;
|
||||
|
||||
const route: RouteRecordRaw = {
|
||||
path: node.path,
|
||||
path: routePath,
|
||||
name: node.name,
|
||||
meta: {
|
||||
title: node.name,
|
||||
@@ -111,11 +159,13 @@ function transformMenuToRoutes(nodes: MenuNode[]): RouteRecordRaw[] {
|
||||
route.component = componentLoader;
|
||||
}
|
||||
|
||||
if (node.children?.length) {
|
||||
route.children = transformMenuToRoutes(node.children);
|
||||
// 没有组件的父节点自动重定向到第一个子节点
|
||||
if (!componentLoader && node.children[0]) {
|
||||
route.redirect = node.children[0].path;
|
||||
if (enabledChildren.length > 0) {
|
||||
// 递归时传入当前节点的完整路径作为父级前缀
|
||||
route.children = transformMenuToRoutes(enabledChildren, node.path);
|
||||
// 无组件的父节点 → redirect 指向第一个可用子路由(相对路径)
|
||||
if (!componentLoader) {
|
||||
const firstChildRelativePath = enabledChildren[0].path.slice(node.path.length + 1);
|
||||
route.redirect = firstChildRelativePath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,14 +177,19 @@ function transformMenuToRoutes(nodes: MenuNode[]): RouteRecordRaw[] {
|
||||
|
||||
/**
|
||||
* 获取第一个可见菜单的路径(用作默认首页)
|
||||
* 跳过 disabled / hideInMenu / externalLink 节点
|
||||
* 返回带 '/' 前缀的完整路径
|
||||
*/
|
||||
function getFirstMenuPath(nodes: MenuNode[]): string {
|
||||
for (const node of nodes) {
|
||||
if (!node.hideInMenu && !node.externalLink && !node.children?.length) {
|
||||
return node.path;
|
||||
if (node.disabled || node.externalLink) continue;
|
||||
|
||||
const enabledChildren = getEnabledChildren(node);
|
||||
if (!node.hideInMenu && enabledChildren.length === 0) {
|
||||
return '/' + node.path;
|
||||
}
|
||||
if (node.children?.length) {
|
||||
const childPath = getFirstMenuPath(node.children);
|
||||
if (enabledChildren.length > 0) {
|
||||
const childPath = getFirstMenuPath(enabledChildren);
|
||||
if (childPath) return childPath;
|
||||
}
|
||||
}
|
||||
@@ -146,8 +201,7 @@ function getFirstMenuPath(nodes: MenuNode[]): string {
|
||||
* 动态路由作为其子路由挂载
|
||||
*/
|
||||
function ensureLayoutRoute() {
|
||||
const existing = router.hasRoute('BasicLayout');
|
||||
if (!existing) {
|
||||
if (!router.hasRoute('BasicLayout')) {
|
||||
router.addRoute({
|
||||
path: '/',
|
||||
name: 'BasicLayout',
|
||||
@@ -159,61 +213,41 @@ function ensureLayoutRoute() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册兜底菜单和路由(后端接口不可用时使用)
|
||||
* 通用"注入路由 + 菜单"流程
|
||||
* 无论是后端数据还是兜底数据,都走同一套 logic
|
||||
*/
|
||||
function registerFallbackRoutes() {
|
||||
function applyMenuTree(menuTree: MenuNode[]) {
|
||||
state.menuTree = menuTree;
|
||||
state.menuItems = transformMenuNode(menuTree);
|
||||
state.homePath = getFirstMenuPath(menuTree);
|
||||
|
||||
ensureLayoutRoute();
|
||||
|
||||
router.addRoute('BasicLayout', {
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/pages/dashboard'),
|
||||
meta: { title: '工作台', icon: 'DashboardOutlined' },
|
||||
const dynamicRoutes = transformMenuToRoutes(menuTree);
|
||||
dynamicRoutes.forEach((route) => {
|
||||
router.addRoute('BasicLayout', route);
|
||||
});
|
||||
router.addRoute('BasicLayout', {
|
||||
path: 'about',
|
||||
name: 'About',
|
||||
component: () => import('@/pages/about'),
|
||||
meta: { title: '关于', icon: 'InfoCircleOutlined' },
|
||||
});
|
||||
router.addRoute('BasicLayout', { path: '', redirect: '/dashboard' });
|
||||
|
||||
// 默认重定向到首页
|
||||
router.addRoute('BasicLayout', { path: '', redirect: state.homePath });
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 暴露给组件使用的 composable
|
||||
// ============================================================
|
||||
|
||||
export function useMenuStore() {
|
||||
const loadMenu = async () => {
|
||||
if (state.loaded) return;
|
||||
|
||||
try {
|
||||
const menuTree = await fetchMenuTree();
|
||||
state.menuTree = menuTree;
|
||||
state.menuItems = transformMenuNode(menuTree);
|
||||
state.homePath = getFirstMenuPath(menuTree);
|
||||
|
||||
// 确保 BasicLayout 布局路由已注册
|
||||
ensureLayoutRoute();
|
||||
|
||||
// 动态注册路由:将后端路由配置注入到 BasicLayout 布局下
|
||||
const dynamicRoutes = transformMenuToRoutes(menuTree);
|
||||
dynamicRoutes.forEach((route) => {
|
||||
router.addRoute('BasicLayout', route);
|
||||
});
|
||||
|
||||
// 默认重定向到首页(BasicLayout 的根路径)
|
||||
router.addRoute('BasicLayout', { path: '', redirect: state.homePath });
|
||||
|
||||
applyMenuTree(menuTree);
|
||||
state.loaded = true;
|
||||
} catch (err) {
|
||||
console.error('加载菜单失败:', err);
|
||||
// 降级:使用兜底菜单
|
||||
state.menuItems = [
|
||||
{ key: '/dashboard', label: '工作台', iconName: 'DashboardOutlined' },
|
||||
{ key: '/about', label: '关于', iconName: 'InfoCircleOutlined' },
|
||||
];
|
||||
state.homePath = '/dashboard';
|
||||
|
||||
// 兜底路由
|
||||
registerFallbackRoutes();
|
||||
|
||||
// 降级:使用本地兜底菜单
|
||||
applyMenuTree(FALLBACK_MENU_NODES);
|
||||
state.loaded = true;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user