一.介绍
项目github地址:https://github.com/Little-Orange7/cmms-vue
一般的后台管理系统都采用单页面应用,布局基本上都差不多,我这里设计界面的时候,采用了有四个模块的经典布局模式,分别展示不同的信息,具体请看项目代码。登录的菜单采用的是动态配置的方式,打开的页面会有Tab标签,如果打开的页面设置为缓存,则页面在关闭之前,数据是缓存在内存中的,切换tab的时候页面不会刷新,如果关闭了tab,则重新打开会重新获取数据。
二.布局
采用经典的布局模式
1.Header
这部分有登录用户的个人信息及其他的一些信息设置。
2.Aside
这部分是动态加载菜单的地方,不同的用户对应的角色不同,则看到相应的菜单也不一样。
3.Main
数据的主窗口区,菜单的的页面都会在这个区域展示出来。
4.Footer
展示一些版权及其他相关信息。
三.实现的关键点
1.菜单的动态加载
菜单信息是配置在数据库中的,根据不同的用户角色,来动态展示;采用的是elementUI的树形导航菜单控件实现,菜单的动态展示用递归加载子菜单,其中难点在于动态的加载router信息,解析从后台获取的路由数据后,懒加载对应的页面组件,解析路由也用到递归方式来解析页面对应的组件。
树形导航菜单的递归实现:
<!-- 遍历路由表,生成左侧菜单 --> <el-menu router unique-opened @select="pushTabMsg" :default-active="activeKey"> <!--因为递归的组件中如果包含el-menu,则子目录不会缩进显示,所以为了保证只有一个el-menu,只把子组件作为递归组件单独抽离出来--> <sub-menu :routes="routes"></sub-menu> </el-menu>
递归加载子菜单:
<!--el-menu的递归子组件,不包含el-menu--> <div> <template v-for="(item,index) in routes"> <!-- 一级菜单的情况 start--> <!-- index跟浏览器地址对应,这样菜单才能显示选中状态 --> <el-menu-item v-if="item.children === null||item.children.length === 0" :index="item.path" :key="index"> <i v-if="item.meta.iconCls" :class="item.meta.iconCls"></i><!-- 设置icon --> <span slot="title">{{item.meta.title}}</span><!-- 菜单名称 --> </el-menu-item> <!-- 一级菜单的情况 end--> <!-- 多级菜单 start--> <el-submenu v-else :index="item.meta.title+item.meta.iconCls" :key="index"><!--:index的值必须唯一,此处使用图标+标题来唯一确定--> <template slot="title"> <i v-if="item.meta.iconCls" :class="item.meta.iconCls" style="color: #409eff;margin-right: 5px"></i> <span slot="title">{{item.meta.title}}</span> </template> <sub-menu :routes="item.children"></sub-menu><!--递归自身,加载子菜单--> </el-submenu> <!-- 多级菜单 end--> </template> </div>
动态加载路由信息:
let psRoutes = parseRoutes(data)
router.addRoutes(psRoutes)
递归解析路由parseRoutes方法的实现:
这里主要是采用懒加载解析router中每一个路由对应的页面组件;
export const parseRoutes = (routes) => { let psRoutes = [] routes.forEach(router => { let { path, component, name, meta, parentId, children, folder } = router if (children && children instanceof Array) { children = parseRoutes(children) } let psRouter = { path: path, name: name, meta: meta, parentId: parentId, children: children, folder: folder, component (resolve) { // 此处的动态路由拼接必须至少要在前面加'@/'或者'../',('@/'表示src的目录)这样在编译的所有模块,但是运行时才会确定变量的值,这样来实现懒加载。 // '@/'和'../'区别:../是可以直接ctrl路径找到该文件,@/则不能。 if (parentId === 1) { require(['@/components' + component], resolve) } else { require(['@/views' + component], resolve) } } } psRoutes.push(psRouter) }) return psRoutes }
(这部分参考了松哥微人事的路由解析,大家可以去关注一下。)
2.页面的Tab页实现
这部分使用了elementUI的Tab标签页的控件,具体实现动态加载标签的增加的方法是根据路由地址的变化来实现的,如果监听到路由地址变化,则做相应的增加;但是页面的展示并不是在Tab的内容页面展示的,是在Tab的加了一个单独展示的控件,Tab只是提供了一个连接,然后根据这个连接来将对应的页面展示在这个单独的控件区域。
<nav-tabs>是Tab的加载区域,main区域是页面的展示区:
<el-container> <nav-tabs></nav-tabs> <el-main style="height: 100px; border: 1px solid #eee" > <keep-alive :include="keepAliveComp"> <router-view class="homeRouterView"></router-view> </keep-alive> </el-main> <el-footer>版权所有</el-footer> </el-container>
在<nav-tabs>监听当前路由的变化:
watch: { // 监听路由对象的变化 $route (to, from) { this.setTabs(to) } },
实现Tab页的增加及页面的展示:
// 当前页面要和当前路由保持一致 setTabs (route) { const isExist = this.tabsList.some(item => { return item.path === route.fullPath }) if (!isExist) { this.tabsList.push({ title: route.meta.title, path: route.fullPath, name: route.name, keepAlive: route.meta.keepAlive }) } this.currentTabsValue = route.fullPath }
3.菜单和Tab页的关联
点击菜单时,要将打开的菜单以Tab的形式加载出来,并且在main区展示对应的页面;反过来,点击不同的Tab,要将其对应的菜单以高亮的形式激活。
实现方法是在菜单组件中监听路由变化,根据路由变化来激活相应的打开菜单:
watch: { // 监听当前路由对象的变化 $route (to, from) { this.activeCurrentMenu(to) } }
// 根据当前路由路径来激活当前侧边的菜单 activeCurrentMenu (route) { this.activeKey = route.fullPath }
这样就实现了打开的tab和激活菜单关联在一起了。
项目github地址:https://github.com/Little-Orange7/cmms-vue
(项目目前在持续的更新中,欢迎有兴趣的小伙伴加入)