使用AI一步一步实现若依前端(6)
功能6:动态添加路由记录
功能5:侧边栏菜单动态显示
功能4:首页使用Layout布局
功能3:点击登录按钮实现页面跳转
功能2:静态登录界面
功能1:创建前端项目
前言
昨天点击菜单后,没有得到预期结果的页面展示效果。是因为Router里没有对应的路由记录。
从开发工具可以看到当前的路由记录。就只有在src/router/index.js文件里配置的/login
和/index
一.操作步骤
1.addRoute方法
是 Vue Router 4.x 中动态添加路由的核心方法,主要用于实现权限路由、按需加载等场景。
方法的入参格式,可以参考路由配置文件里面的首页配置。需要重点关注两个component属性,值要对应到具体的资源对象。
{
path: '/',
component: Layout,
redirect: '/index',
children: [
{
path: 'index',
component: () => import('@/views/index.vue'),
}
]
}
2.解析getRouters接口的返回结果
- “component”: “Layout”
一级折叠菜单,点击会展开。要将Layout字符串,替换成Layout对象
- “component”: “ParentView”
二级折叠菜单,点击会展开。说明该对象下有children属性。需要特殊处理。修改children里每一个元素的path值。只保留children元素。
例如
“path”: "log"是父对象里的。
“path”: “operlog"是children元素里的。
就将children元素里的path修改为"path”: “log/operlog”
- “component”: “system/user/index”
菜单项,点击会在内容区显示对应的界面。要将字符串,替换成对应路径下的资源对象,src/views/system/user/index.vue
3.修改layout/index.vue
根据以上分析,写JS方法处理返回结果。
<script setup>
import Layout from '@/layout/index.vue'
import { ElContainer, ElAside } from 'element-plus'
import Sidebar from './components/Sidebar.vue'
import Navbar from './components/Navbar.vue'
import AppMain from './components/AppMain.vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const data1 = {"name": "System","path": "/system","hidden": false,"redirect": "noRedirect","component": "Layout","alwaysShow": true,"meta": {"title": "系统管理","icon": "system","noCache": false,"link": null},"children": [{"name": "User","path": "user","hidden": false,"component": "system/user/index","meta": {"title": "用户管理","icon": "user","noCache": false,"link": null}},{"name": "Role","path": "role","hidden": false,"component": "system/role/index","meta": {"title": "角色管理","icon": "peoples","noCache": false,"link": null}},{"name": "Menu","path": "menu","hidden": false,"component": "system/menu/index","meta": {"title": "菜单管理","icon": "tree-table","noCache": false,"link": null}},{"name": "Dept","path": "dept","hidden": false,"component": "system/dept/index","meta": {"title": "部门管理","icon": "tree","noCache": false,"link": null}},{"name": "Post","path": "post","hidden": false,"component": "system/post/index","meta": {"title": "岗位管理","icon": "post","noCache": false,"link": null}},{"name": "Dict","path": "dict","hidden": false,"component": "system/dict/index","meta": {"title": "字典管理","icon": "dict","noCache": false,"link": null}},{"name": "Config","path": "config","hidden": false,"component": "system/config/index","meta": {"title": "参数设置","icon": "edit","noCache": false,"link": null}},{"name": "Notice","path": "notice","hidden": false,"component": "system/notice/index","meta": {"title": "通知公告","icon": "message","noCache": false,"link": null}},{"name": "Log","path": "log","hidden": false,"redirect": "noRedirect","component": "ParentView","alwaysShow": true,"meta": {"title": "日志管理","icon": "log","noCache": false,"link": null},"children": [{"name": "Operlog","path": "operlog","hidden": false,"component": "monitor/operlog/index","meta": {"title": "操作日志","icon": "form","noCache": false,"link": null}},{"name": "Logininfor","path": "logininfor","hidden": false,"component": "monitor/logininfor/index","meta": {"title": "登录日志","icon": "logininfor","noCache": false,"link": null}}]}]}
const data2 = { "name": "Monitor", "path": "/monitor", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统监控", "icon": "monitor", "noCache": false, "link": null }, "children": [{ "name": "Online", "path": "online", "hidden": false, "component": "monitor/online/index", "meta": { "title": "在线用户", "icon": "online", "noCache": false, "link": null } }, { "name": "Job", "path": "job", "hidden": false, "component": "monitor/job/index", "meta": { "title": "定时任务", "icon": "job", "noCache": false, "link": null } }, { "name": "Druid", "path": "druid", "hidden": false, "component": "monitor/druid/index", "meta": { "title": "数据监控", "icon": "druid", "noCache": false, "link": null } }, { "name": "Server", "path": "server", "hidden": false, "component": "monitor/server/index", "meta": { "title": "服务监控", "icon": "server", "noCache": false, "link": null } }, { "name": "Cache", "path": "cache", "hidden": false, "component": "monitor/cache/index", "meta": { "title": "缓存监控", "icon": "redis", "noCache": false, "link": null } }, { "name": "CacheList", "path": "cacheList", "hidden": false, "component": "monitor/cache/list", "meta": { "title": "缓存列表", "icon": "redis-list", "noCache": false, "link": null } }] }
const data3 = { "name": "Tool", "path": "/tool", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统工具", "icon": "tool", "noCache": false, "link": null }, "children": [{ "name": "Build", "path": "build", "hidden": false, "component": "tool/build/index", "meta": { "title": "表单构建", "icon": "build", "noCache": false, "link": null } }, { "name": "Gen", "path": "gen", "hidden": false, "component": "tool/gen/index", "meta": { "title": "代码生成", "icon": "code", "noCache": false, "link": null } }, { "name": "Swagger", "path": "swagger", "hidden": false, "component": "tool/swagger/index", "meta": { "title": "系统接口", "icon": "swagger", "noCache": false, "link": null } }] }
// const data4 = {"name": "Http://ruoyi.vip","path": "http://ruoyi.vip","hidden": false,"component": "Layout","meta": {"title": "若依官网","icon": "guide","noCache": false,"link": "http://ruoyi.vip"}}
const menuData = [data1, data2, data3]
// 匹配views里面所有的.vue文件
const modules = import.meta.glob('@/views/**/*.vue')
import { onMounted } from 'vue'
onMounted(() => {
const newRouteRecord = JSON.parse(JSON.stringify(menuData));
filterAsyncRouter(newRouteRecord)
newRouteRecord.forEach(route => {
router.addRoute(route) // 动态添加可访问路由表
})
})
/**
* 异步路由过滤器 - 核心路由配置处理器
* 功能:
* 1. 递归处理路由配置树,动态加载Vue组件
* 2. 特殊处理Layout组件和ParentView结构
* 3. 规范化路由配置结构
*
* @param {Array} asyncRouterArr - 原始异步路由配置数组
* @returns {Array} 处理后的标准化路由配置数组
*
* 处理逻辑:
* 1. 遍历路由配置,处理子路由配置
* 2. 动态加载组件(转换字符串路径为真实组件)
* 3. 递归处理嵌套子路由
* 4. 清理空children和redirect属性
*/
const filterAsyncRouter = (asyncRouterArr) => {
asyncRouterArr.filter(routeMap => {
// 处理子路由
if (routeMap.children) {
routeMap.children = filterChildrenForRouter(routeMap.children);
}
if (routeMap.component) {
// Layout 组件特殊处理
if (routeMap.component === 'Layout') {
routeMap.component = Layout
} else {
routeMap.component = loadView(routeMap.component)
}
}
// 递归处理子路由
if (routeMap.children?.length) {
filterAsyncRouter(routeMap.children);
} else {
delete routeMap.children;
delete routeMap.redirect;
}
return true;
});
}
/**
* 子路由结构转换器 - 路由层级扁平化处理器
* 功能:
* 1. 处理ParentView类型的路由结构
* 2. 合并嵌套子路由路径
* 3. 将多级路由转换为扁平结构
*
* @param {Array} childrenArr - 原子路由配置数组
* @returns {Array} 转换后的扁平化子路由数组
*
* 处理逻辑:
* 1. 当遇到ParentView组件时,将其子路由提升到当前层级
* 2. 合并父级路径到子路由path
* 3. 保留普通路由配置
*/
const filterChildrenForRouter = (childrenArr) => {
let children = [];
childrenArr.forEach(el => {
if (el.children?.length && el.component === 'ParentView') {
children.push(...el.children.map(c => ({
...c,
path: `${el.path}/${c.path}`
})));
return;
}
children.push(el);
});
return children;
}
/**
* 动态组件加载器 - 模块解析器
* 功能:
* 根据组件路径字符串动态加载Vue组件
*
* @param {string} view - 组件路径字符串(例: "system/user/index")
* @returns {Component} Vue组件
*
* 处理逻辑:
* 1. 遍历预编译的模块集合(modules)
* 2. 匹配views目录下的对应组件文件
* 3. 返回组件异步加载函数
*/
const loadView = (view) => {
let res;
for (const path in modules) {
const dir = path.split('views/')[1].split('.vue')[0];
if (dir === view) {
res = () => modules[path]();
}
}
return res;
}
</script>
<template>
<el-container class="h-screen">
<el-aside width="200px">
<Sidebar :menu-data="menuData"/>
</el-aside>
<el-container>
<el-header height="48px">
<Navbar />
</el-header>
<AppMain />
</el-container>
</el-container>
</template>
<style>
.el-header {
--el-header-padding: 0;
height: auto;
}
</style>
4.修改MenuItem.vue
让el-menu-item的index属性的值和路由记录的path对应,才能正确的跳转。
<template>
<template v-if="hasChildren">
<el-sub-menu :index="item.path">
<template #title>
<el-icon v-if="item.meta?.icon">
<component :is="item.meta.icon" />
</el-icon>
<span>{{ item.meta?.title }}</span>
</template>
<template v-for="child in item.children" :key="child.path">
<MenuItem :item="child" :level="level + 1" :base-path="resolvePath(item.path)"/>
</template>
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="resolvePath(item.path)">
<el-icon v-if="item.meta?.icon">
<component :is="item.meta.icon" />
</el-icon>
<span>{{ item.meta?.title }}</span>
</el-menu-item>
</template>
</template>
<script setup>
import { defineProps, computed } from 'vue';
const props = defineProps({
item: {
type: Object,
required: true
},
level: {
type: Number,
default: 0
},
basePath: {
type: String,
default: ''
}
});
const hasChildren = computed(() => {
if (props.level >= 2) {
if (props.item.children) {
console.error('菜单层级超过限制,最多允许两层子菜单');
return false;
}
return false;
}
return props.item.children && props.item.children.length > 0;
});
const resolvePath = (routePath) => {
if (props.basePath.length === 0) {
return routePath
};
return getNormalPath(props.basePath + '/' + routePath)
}
/**
* 路径标准化处理器
* 功能: 规范化URL/文件路径格式,确保路径符合统一格式标准
* 核心处理逻辑:
* 1. 处理空值及无效路径
* 2. 转换双斜杠为单斜杠
* 3. 去除路径末尾的斜杠
*
* @param {string} p - 原始路径字符串
* @returns {string} 标准化后的路径
*/
const getNormalPath = (p) => {
// 空值安全处理:当传入空值/undefined字符串时直接返回原值
if (p.length === 0 || !p || p == 'undefined') {
return p
};
// 双斜杠转换:替换路径中的双斜杠为单斜杠
let res = p.replace('/\/+/g', '/')
// 末尾斜杠清理:当标准化后路径以斜杠结尾时移除末尾斜杠
if (res[res.length - 1] === '/') {
return res.slice(0, res.length - 1)
}
return res;
}
</script>
5.给用户管理新建对应页面
根据用户管理的信息"component": "system/user/index"
新建文件src/views/system/user/index.vue
<script setup lang="ts">
</script>
<template>
<div>
用户管理
</div>
</template>
<style scoped lang="scss">
</style>
二.功能验证
运行项目,浏览器访问http://localhost:5173/index
点击用户管理,Layout不变,内容区显示变成用户管理页面。
三.知识点拓展
1. 动态路由注册(addRoute)
router.addRoute(route) // 动态添加路由
• 作用:运行时动态添加路由配置
• 特点:
• 不需要重启应用即可生效
• 常用于权限管理系统
• 可以嵌套添加父子路由
• 参数结构:
{
path: '/user',
component: UserComponent,
children: [...]
}
2. 组件递归渲染
<!-- MenuItem组件内调用自身 -->
<MenuItem :item="child" :level="level + 1"/>
• 实现原理:
• 组件在自己的模板中调用自身
• 通过props传递不同层级的参数
• 需要设置终止条件(示例中通过level >=2 限制层级)
• 应用场景:树形菜单、多级导航等嵌套结构
3. 动态组件加载
// 动态加载视图组件
const loadView = (view) => {
return () => import(`@/views/${view}.vue`)
}
• 核心机制:
• 使用import()
实现代码分割
• 返回一个返回Promise的工厂函数
• Vue会自动处理异步加载状态
4. 模块批量导入(重点)
const modules = import.meta.glob('@/views/**/*.vue')
• 功能解析:
• import.meta.glob:Vite提供的特殊导入方法
• '@/views//*.vue’**:匹配views目录下所有vue文件
• 返回值:键值对集合,键为文件路径,值为动态导入函数
• 生成结构示例:
{
'./views/user/index.vue': () => import('./views/user/index.vue'),
'./views/about.vue': () => import('./views/about.vue')
}
• 优势:
• 实现真正的按需加载
• 自动处理文件路径映射
• 支持热模块替换(HMR)
5. 路由元信息(Meta)
meta: {
title: '用户管理',
icon: 'user',
noCache: false
}
• 使用方式:
• 通过route.meta
访问配置信息
• 配合v-if
控制菜单显示逻辑
• 可以通过全局守卫读取路由元信息
• 典型应用:
• 页面标题管理
• 菜单图标配置
• 缓存控制
6. 组合式API应用
import { useRouter } from 'vue-router'
const router = useRouter()
• 特性:
• useRouter
提供路由实例
• 在setup()中直接使用
• 配合<script setup>
语法更简洁
• 对比选项式API优势:
• 更好的类型推导
• 更灵活的逻辑组合
• 更清晰的依赖关系
7. 异步组件处理
component: () => import('@/views/index.vue')
• 工作原理:
• 访问路由时才会加载组件
• 自动生成独立的chunk文件
• 支持加载状态处理(loading/error)
• 优化效果:
• 减少首屏加载体积
• 提升页面加载速度
• 自动代码分割
8. 路由路径处理
const resolvePath = (routePath) => {
return basePath + '/' + routePath
}
• 核心要点:
• 处理嵌套路由的路径拼接
• 使用正则表达式标准化路径
• 防止出现//
等异常路径
• 注意事项:
• 绝对路径与相对路径的区别
• 动态参数路径的特殊处理
• 与路由配置的严格对应