- 前端
- AI 技能
【免费下载链接】basic
⭐⭐⭐⭐⭐ 面向 AI 编程的管理系统框架,兼容PC、移动端。AI-oriented management system framework, compatible with PC and mobile device.
本篇技术指南以 Fantastic-admin 框架的路由生成器技能(skills/fa-route-generator/SKILL.md)为骨架,系统讲解如何在该框架中创建、修改路由配置,并通过meta元信息驱动导航菜单、权限控制、页面保活与面包屑展示。读完本文,你将掌握路由文件的标准写法、RouteMetaRaw全部可用属性、列表-详情页保活等高频实战方案,以及这些配置在框架源码中的真实生效链路。
一、路由生成器是什么:一条路由配置驱动整站导航
Fantastic-admin 是一个面向 AI 编程的管理系统框架,兼容 PC 与移动端,仓库采用 monorepo 架构,apps/目录下存放各应用(如core、example、core-ant-design-vue等)。框架的核心设计之一是导航菜单由路由配置自动生成——你不需要单独维护菜单数据,只要按照约定编写路由文件并配置meta属性,侧边栏菜单、标签页、面包屑、权限过滤与页面保活都会随之联动。
这意味着路由配置是整个应用导航体系的"单一数据源",而路由生成器(fa-route-generator)就是围绕这一机制提供的操作规范:它规定了创建新路由、修改现有路由的完整流程,并明确了meta中每个属性的含义与取值。
二、前置准备:确认目标应用与路由驱动模式
第一步:确认工作区(必须等待用户明确回复)
由于是 monorepo 架构,在执行任何文件读写之前,必须先确认要在哪个应用中操作路由。操作规范要求:
- 执行
ls apps/列出所有可用应用; - 向用户提问,明确询问在哪个应用中操作路由,并停止等待回复;
- 收到明确回复后才能继续。
严格规则:如果用户没有在请求中明确说明目标应用(例如"在 example 应用中"、"apps/core"),则必须提问,不得自行猜测或默认选择任何应用。
确认后,后续所有文件路径均以该应用目录为根,例如apps/<app>/src/router/。
第二步:检查路由驱动模式(routeBaseOn)
读取apps/<app>/src/settings.ts,检查app.routeBaseOn的值:
'frontend'(默认):可以继续,手动创建的路由文件生效;'backend':路由由后端驱动,手动创建的路由文件会被忽略,此时应告知用户该模式不支持前端路由文件生成。
以apps/core为例,其 settings.ts 内容为空(未显式配置),因此采用默认值。从源码看,默认值定义于 packages/settings/src/default.ts:
routeMode: 'hash', routeBaseOn: 'frontend',而 packages/settings/src/types.ts 中定义的完整取值是'frontend' | 'backend' | 'filesystem',其中历史遗留的'filesystem'会在配置解析时被统一映射为'frontend'(见 packages/settings/src/utils.ts)。
该模式在运行时如何生效?查看路由守卫 apps/core/src/router/guards.ts 可知:
switch (appSettingsStore.settings.app.routeBaseOn) { case 'frontend': appRouteStore.generateRoutesAtFront(asyncRoutes) break case 'backend': await appRouteStore.generateRoutesAtBack() break }前端模式直接使用routes.ts中导出的asyncRoutes生成并注册动态路由;后端模式则调用 API 获取路由数据后再格式化注册(见 apps/core/src/store/modules/app/route.ts)。
三、框架约定:路由文件必须遵守的规则
- 一级路由
path必须以/开头,component必须是Layout(即@/layouts/index.vue); - 子路由
path不要以/开头; - 多级路由的中间层级无需设置
component; - 所有路由的
name必须全局唯一。
其中"多级路由中间层级无需设置 component"不仅是约定,更是框架的强制处理逻辑。在 apps/core/src/store/modules/app/route.ts 中,deleteMiddleRouteComponent会递归删除所有含children的中间层级路由的component:
function deleteMiddleRouteComponent(routes: RouteRecordRaw[]) { const res: RouteRecordRaw[] = [] routes.forEach((route) => { if (route.children?.length) { delete route.component route.children = deleteMiddleRouteComponent(route.children) } else { delete route.children } res.push(route) }) return res }这一处理在generateRoutesAtFront与systemRoutes计算属性中都会被调用,因此即使你在路由文件中给中间层级写了component,最终注册时也会被清理。
四、场景 A:创建新路由
1. 收集路由信息
向用户询问或确认:路由路径、页面标题、图标(可选)、是否多级路由、所属主导航分组。
2. 创建路由文件
在apps/<app>/src/router/modules/下创建<模块名>.ts,例如example.ts:
import type { RouteRecordRaw } from 'vue-router' function Layout() { return import('@/layouts/index.vue') } const routes: RouteRecordRaw = { path: '/example', component: Layout, name: 'example', meta: { title: '示例', icon: 'i-ep:menu', }, children: [ { path: '', name: 'exampleIndex', component: () => import('@/views/example/index.vue'), meta: { title: '示例页面', }, }, ], } export default routes仓库中真实的路由模块可参考 apps/core/src/router/modules/multilevel.menu.example.ts,它演示了三级导航的标准写法:顶层component: Layout,中间层级(level2)只写meta与children不写component,叶子节点才声明页面组件。
3. 更新 routes.ts 并注册到主导航分组
在apps/<app>/src/router/routes.ts中添加 import,并把路由模块注册到对应主导航分组的children数组中。以apps/core的 routes.ts 为例:
import type { RouteRecordMainRaw } from '@fantastic-admin/types' import type { RouteRecordRaw } from 'vue-router' import pinia from '@/store' import MultilevelMenuExample from './modules/multilevel.menu.example' // 动态路由(异步路由、导航菜单路由) const asyncRoutes: RouteRecordMainRaw[] = [ { meta: { title: '演示', icon: 'i-ri:function-ai-line', }, children: [ MultilevelMenuExample, ], }, ] export { asyncRoutes, constantRoutes, systemRoutes, }可以看到主导航分组的类型是RouteRecordMainRaw,它本身没有path与component,只承载meta(auth、title、icon、sort)与children(packages/types/types.ts),真正的路由路径由 children 中的路由模块提供。constantRoutes(固定路由)与systemRoutes(系统路由)与此并列导出,最终constantRoutes在 apps/core/src/router/index.ts 中交给createRouter,asyncRoutes由守卫动态注册。
五、场景 B:修改现有路由
定位路由文件(在apps/<app>/src/router/modules/下搜索),读取后按需修改meta属性。
权限控制
meta: { auth: 'user:view' } // 或数组 ['user:view', 'user:edit']auth为字符串时表示需要该权限;为数组时表示满足其中一个即可。菜单生成时会据此过滤无权限的导航项(见下文"权限过滤")。
页面保活(从详情返回列表时保留列表状态)
// 列表页 meta: { keepAlive: ['productDetail'] } // 详情页 meta: { menu: false, activeMenu: '/product', noKeepAlive: 'productList' }- 列表页声明
keepAlive: ['productDetail']:只有从名为productDetail的页面返回时才保留列表状态; - 详情页声明
menu: false不出现在导航中,activeMenu: '/product'让详情页仍高亮"产品管理"菜单,noKeepAlive: 'productList'表示从列表进入详情时不保活(保证详情始终加载最新数据)。
隐藏菜单项
meta: { menu: false, activeMenu: '/parent/path' }某些页面(如详情页、个人设置)不需要出现在导航菜单中,可用menu: false隐藏,同时用activeMenu指定进入该页面时应该高亮的菜单地址。
六、RouteMeta 属性全解析
框架在 packages/types/types.ts 中定义了RouteMetaRaw接口,并通过 packages/types/global.d.ts 将其合并进 vue-router 的RouteMeta,因此所有路由meta都能获得完整的类型提示。详细说明见 skills/fa-route-generator/references/route-meta.md。
权限相关
auth
- 类型:
string | string[] - 默认值:
undefined - 说明:路由访问权限,配置为数组时,只需满足一个即可进入
- 示例:
auth: 'news:view' // 需要具备 news:view 权限 auth: ['news:view', 'news:edit'] // 需要具备其中一个权限导航显示
title(必需)
- 类型:
string | (() => string) - 默认值:
undefined - 说明:标题会在导航、标签页、面包屑等需要的展示位置显示
- 示例:
title: '新闻管理' title: () => '动态标题' // 支持函数式动态标题icon
- 类型:
string - 默认值:
undefined - 说明:导航菜单、标签页等展示区域使用的图标(Iconify 图标格式)
- 示例:
icon: 'i-ep:lock' // 默认显示 i-ep:lock 图标menu
- 类型:
boolean - 默认值:
true - 说明:是否在导航中显示;当子导航里没有可展示的导航时,会直接显示父导航
activeMenu
- 类型:
string - 默认值:
undefined - 说明:高亮导航,需要设置完整路由地址
- 示例:
activeMenu: '/news/list'expand
- 类型:
boolean - 默认值:
undefined(实际表现为不默认展开) - 说明:是否默认展开
- 示例:
expand: true // 默认展开breadcrumb
- 类型:
boolean - 默认值:
true - 说明:是否在面包屑中显示
页面行为
keepAlive
- 类型:
boolean | string | string[] - 默认值:
undefined - 说明:保活,根据规则保活当前路由页面
- 示例:
keepAlive: true // 始终保活 keepAlive: 'news' // 访问路由name为news的页面时保活 keepAlive: ['news', 'user'] // 访问路由name为news或user的页面时保活noKeepAlive
- 类型:
string | string[] - 默认值:
undefined - 说明:不保活,根据规则不保活当前路由页面
- 示例:
noKeepAlive: 'news' // 访问路由name为news的页面时不保活 noKeepAlive: ['news', 'user'] // 访问路由name为news或user的页面时不保活link
- 类型:
string - 默认值:
undefined - 说明:外部链接,会在浏览器新窗口访问该链接
- 示例:
link: 'https://example.com' // 在浏览器新窗口打开该外部地址其他(源码补充)
RouteMetaRaw中还包含两个未在技能文档中展开的属性:
- sort:导航排序,数字越大越靠前,默认值
0。它由 apps/core/src/store/modules/app/route.ts 的sortAsyncRoutes递归应用到每层路由; - copyright:是否显示版权,不设置时使用全局配置。
技能文档开篇强调的"常用属性"即:title(必需)、icon、menu、auth、keepAlive、activeMenu、breadcrumb,其余属性按需使用即可。
七、高频场景配置示例
以下示例均整理自 skills/fa-route-generator/references/examples.md,可直接复制到apps/<app>/src/router/modules/下按需修改。
单页面路由
最简单的单页面路由配置:
import type { RouteRecordRaw } from 'vue-router' function Layout() { return import('@/layouts/index.vue') } const routes: RouteRecordRaw = { path: '/dashboard', component: Layout, name: 'dashboard', meta: { title: '仪表盘', icon: 'i-ep:data-line', }, children: [ { path: '', name: 'dashboardIndex', component: () => import('@/views/dashboard/index.vue'), meta: { title: '数据概览', }, }, ], } export default routes注意子路由path: ''表示该页面直接渲染在父级路径/dashboard上。
列表-详情路由
最常见的列表与详情页组合:
const routes: RouteRecordRaw = { path: '/article', component: Layout, name: 'article', meta: { title: '文章管理', icon: 'i-ep:document', }, children: [ { path: '', name: 'articleList', component: () => import('@/views/article/list.vue'), meta: { title: '文章列表', keepAlive: 'articleDetail', // 从详情返回时保持列表状态 }, }, { path: 'detail/:id?', name: 'articleDetail', component: () => import('@/views/article/detail.vue'), meta: { title: '文章详情', menu: false, // 不在导航中显示 activeMenu: '/article', // 高亮文章管理菜单 keepAlive: true, noKeepAlive: 'articleList', // 从列表进入时不保活 }, }, ], }其中path: 'detail/:id?'中?表示id参数可选,详情页无需重复注册为独立菜单。
二级菜单
const routes: RouteRecordRaw = { path: '/system', component: Layout, name: 'system', meta: { title: '系统管理', icon: 'i-ep:setting', }, children: [ { path: 'user', name: 'systemUser', component: () => import('@/views/system/user/index.vue'), meta: { title: '用户管理', icon: 'i-ep:user', }, }, { path: 'role', name: 'systemRole', component: () => import('@/views/system/role/index.vue'), meta: { title: '角色管理', icon: 'i-ep:avatar', }, }, { path: 'permission', name: 'systemPermission', component: () => import('@/views/system/permission/index.vue'), meta: { title: '权限管理', icon: 'i-ep:lock', }, }, ], }三级菜单
const routes: RouteRecordRaw = { path: '/content', component: Layout, name: 'content', meta: { title: '内容管理', icon: 'i-ep:folder', }, children: [ { path: 'article', name: 'contentArticle', meta: { title: '文章管理', icon: 'i-ep:document', }, // 注意: 多级路由的中间层级不需要设置 component children: [ { path: 'list', name: 'contentArticleList', component: () => import('@/views/content/article/list.vue'), meta: { title: '文章列表', }, }, { path: 'category', name: 'contentArticleCategory', component: () => import('@/views/content/article/category.vue'), meta: { title: '文章分类', }, }, ], }, { path: 'media', name: 'contentMedia', meta: { title: '媒体管理', icon: 'i-ep:picture', }, children: [ { path: 'image', name: 'contentMediaImage', component: () => import('@/views/content/media/image.vue'), meta: { title: '图片管理', }, }, { path: 'video', name: 'contentMediaVideo', component: () => import('@/views/content/media/video.vue'), meta: { title: '视频管理', }, }, ], }, ], }带权限的路由
const routes: RouteRecordRaw = { path: '/admin', component: Layout, name: 'admin', meta: { title: '管理员', icon: 'i-ep:user-filled', auth: 'admin', // 需要 admin 权限 }, children: [ { path: 'users', name: 'adminUsers', component: () => import('@/views/admin/users.vue'), meta: { title: '用户列表', auth: ['admin:view', 'admin:edit'], // 需要其中一个权限 }, }, ], }外部链接
const routes: RouteRecordRaw = { path: '/external', component: Layout, name: 'external', meta: { title: '外部链接', icon: 'i-ep:link', }, children: [ { path: 'github', name: 'externalGithub', component: () => import('@/views/external/link.vue'), meta: { title: 'GitHub', link: 'https://example.com', // 在新窗口打开外部地址 }, }, ], }默认展开的路由
const routes: RouteRecordRaw = { path: '/menu', component: Layout, name: 'menu', meta: { title: '菜单示例', icon: 'i-ep:menu', expand: true, // 默认展开 }, children: [ { path: 'always', name: 'menuAlways', meta: { title: '默认展开', expand: true, }, children: [ { path: 'item1', name: 'menuAlwaysItem1', component: () => import('@/views/menu/item1.vue'), meta: { title: '菜单项 1', }, }, ], }, ], }八、路由配置调整:从改一行 meta 到完整落地
权限配置调整
场景 1:添加单个权限
// 修改前 meta: { title: '用户管理', } // 修改后 meta: { title: '用户管理', auth: 'user:view', // 需要 user:view 权限 }场景 2:添加多个权限(或关系)
meta: { title: '用户管理', auth: ['user:view', 'user:edit'], // 满足其中一个即可 }页面保活配置
场景 1:列表页保活——列表页需要在从详情页返回时保持状态:
{ path: '', name: 'orderList', component: () => import('@/views/order/list.vue'), meta: { title: '订单列表', keepAlive: ['orderDetail', 'orderEdit'], // 从这些页面返回时保活 }, }场景 2:详情页保活——详情页需要保活,但从列表页进入时不保活(确保显示最新数据):
{ path: 'detail/:id', name: 'orderDetail', component: () => import('@/views/order/detail.vue'), meta: { title: '订单详情', menu: false, activeMenu: '/order', keepAlive: true, // 始终保活 noKeepAlive: 'orderList', // 从列表进入时不保活 }, }场景 3:取消保活——某些页面不需要保活,每次进入都重新加载:
// 删除或注释掉 keepAlive 相关配置 meta: { title: '实时数据', // keepAlive: true, // 删除此行 }导航显示调整
隐藏导航项——某些页面(如个人设置)不需要出现在导航菜单中:
meta: { title: '个人设置', menu: false, // 不在导航中显示 activeMenu: '/user', // 但高亮用户菜单 }面包屑配置
场景 1:隐藏面包屑
meta: { title: '登录', breadcrumb: false, // 不显示面包屑 }场景 2:自定义面包屑高亮
meta: { title: '编辑文章', activeMenu: '/article/list', // 面包屑高亮到文章列表 }在 routes.ts 中的完整注册
import ArticleRoute from './modules/article' import ContentRoute from './modules/content' // 1. 导入路由模块 import DashboardRoute from './modules/dashboard' import SystemRoute from './modules/system' // 2. 添加到 asyncRoutes const asyncRoutes: Route.recordMainRaw[] = [ { meta: { title: '工作台', icon: 'i-ep:monitor', }, children: [ DashboardRoute, ], }, { meta: { title: '内容', icon: 'i-ep:document', }, children: [ ArticleRoute, ContentRoute, ], }, { meta: { title: '系统', icon: 'i-ep:setting', }, children: [ SystemRoute, ], }, ]九、底层原理:路由如何变成菜单、权限与保活如何生效
1. 菜单自动生成
菜单并非独立维护,而是由路由实时转换而来。apps/core/src/store/modules/app/menu.ts 中的convertRouteToMenu把主导航分组转换成MenuRecordMainRaw,convertRouteToMenuRecursive递归展开每层路由,并只挑选菜单需要的 meta 字段:auth、title、icon、menu、expand、link。这也解释了为什么menu: false、expand: true会直接影响导航展示,而keepAlive等页面行为属性则不会出现在菜单数据结构中。
2. 权限过滤
apps/core/src/store/modules/app/menu.ts 的filterAsyncMenus会基于auth.auth(menu.meta?.auth)递归过滤菜单:父级无权限则整组移除,子级全部无权限的父级也不会展示。同时路由守卫 apps/core/src/router/guards.ts 中的setupRedirectAuthChildrenRoute会在父级路由未配置重定向时,自动重定向到第一个有访问权限且menu !== false的子路由,保证无权限用户不会落到空白页。
3. 保活机制
保活由路由守卫 apps/core/src/router/guards.ts 的setupKeepAlive驱动,规则与文档完全一致:
to.meta.keepAlive为boolean时,不为true则清除保活;- 为
string时,与from.name不一致则清除保活; - 为
array时,不包含from.name则清除保活; to.meta.noKeepAlive为string且与from.name一致,或为array且包含from.name,则清除保活;- 从
reload页面刷新进入时清除保活。
清除或添加操作最终落到 apps/core/src/store/modules/app/keepAlive.ts 的appKeepAliveStore(add/remove/clean),由布局中的<KeepAlive>组件消费。另外需要注意:保活依赖组件name,若组件未命名,开发环境下会通过warnKeepAliveComponentNameMissing给出警告提示。
4. 路由注册与守卫
apps/core/src/router/guards.ts 的setupRoutes是核心守卫:登录后先(可选)拉取用户权限,再按routeBaseOn模式生成动态路由,并通过router.addRoute注册,同时记录removeRoutes回调供登出时清理。此外框架还扩展了 vue-router 的能力(apps/core/src/router/extensions.ts),为router增加了back(关闭当前标签页并回退/跳转)与close(关闭当前标签页并跳转)等扩展语法,与标签页功能联动。路由模式(hash / history)则由settings.app.routeMode控制(apps/core/src/router/index.ts)。
十、小结
Fantastic-admin 的路由体系以"约定优于配置"为原则:写好modules/下的路由文件、遵守路径与组件约定、按需配置meta,即可自动获得导航菜单、权限控制、页面保活、面包屑与标签页等完整能力。实操时可遵循以下 Checklist:
- 先确认目标应用(monorepo 下不得猜测)与
routeBaseOn是否为frontend; - 新建路由:在
apps/<app>/src/router/modules/创建模块文件 → 在routes.ts注册到主导航分组; - 修改路由:直接调整
meta,优先使用auth/menu/activeMenu/keepAlive/noKeepAlive/breadcrumb等高频属性; - 牢记框架约定:一级路由以
/开头且挂载Layout、子路由不带/、中间层级不写component、name全局唯一; - 遇到保活、权限、菜单展示问题,可回查 guards.ts 与 menu.ts 的判定逻辑,理解配置背后的真实执行规则。
- 前端
- AI 技能
【免费下载链接】basic
⭐⭐⭐⭐⭐ 面向 AI 编程的管理系统框架,兼容PC、移动端。AI-oriented management system framework, compatible with PC and mobile device.
相关推荐
Roc 模式匹配的基石:work-list 算法实现类型 inhabitedness 检查的原理与源码解析
Roc 模式匹配的基石:work list 算法实现类型 inhabitedness 检查的原理与源码解析 本篇技术文章围绕 Roc 编译器耗尽性检查(exha
前端AI 技能Fantastic-admin 路由配置示例详解:从单页面到多级菜单的完整实战指南
Fantastic admin 路由配置示例详解:从单页面到多级菜单的完整实战指南 导读 本文以 Fantastic admin( ba/basic 仓库)的路
前端AI 技能v3-admin-vite路由配置完全指南:动态路由与菜单生成
v3 admin vite路由配置完全指南:动态路由与菜单生成 引言:路由管理的痛点与解决方案 你是否在开发后台管理系统时遇到过以下问题?权限控制繁琐、菜单与路
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考