最近完成了个人小爱ADMIN后台管理系统基本功能,同时进行了页面总体布局和样式的全新改版。新增了系统权限功能的实现,同时以为后台系统全部的菜单都左置,会限制菜单的扩展,所以我改进了三级菜单的显示。前端
权限路由思路:
根据用户登陆的roles信息与路由中配置的roles信息进行比较过滤,生成能够访问的路由表,并经过router.addRoutes(store.getters.addRouters)动态添加可访问权限路由表,从而实现左侧和顶栏菜单的展现。vue
在router/index.js中,给相应的菜单设置默认的roles信息;
以下:node
给"权限设置"菜单设置的权限为:webpack
{ path: '/permission', name: 'permission', meta: { title: '权限设置', roles: ['admin', 'editor'] //不一样的角色均可以看到 } }
给其子菜单"页面权限",设置权限为:ios
{ path: 'page', name: 'pagePer', meta: { title: '页面权限', roles: ['admin'] //只有"admin"能够看到该菜单 }, component: () => import('@/page/permission/page'), }
给其子菜单"按钮权限"设置权限为:git
{ path: 'directive', name: 'directivePer', meta: { title: '按钮权限', roles:['editor'] //只有"editor"能够看到该菜单 }, component: () => import('@/page/permission/directive'), }
代码以下:github
function hasPermission(roles, permissionRoles) { if (roles.indexOf('admin') >= 0) return true if (!permissionRoles) return true return roles.some(role => permissionRoles.indexOf(role) >= 0) } const whiteList = ['/login'] // 不重定向白名单 router.beforeEach((to, from, next) => { NProgress.start() // 设置浏览器头部标题 const browserHeaderTitle = to.meta.title store.commit('SET_BROWSERHEADERTITLE', { browserHeaderTitle: browserHeaderTitle }) // 点击登陆时,拿到了token并存入了cookie,保证页面刷新时,始终能够拿到token if (getToken('Token')) { if(to.path === '/login') { next({ path: '/' }) NProgress.done() } else { // 用户登陆成功以后,每次点击路由都进行了角色的判断; if (store.getters.roles.length === 0) { let token = getToken('Token'); getUserInfo({"token":token}).then().then(res => { // 根据token拉取用户信息 let userList = res.data.userList; store.commit("SET_ROLES",userList.roles); store.commit("SET_NAME",userList.name); store.commit("SET_AVATAR",userList.avatar); store.dispatch('GenerateRoutes', { "roles":userList.roles }).then(() => { // 根据roles权限生成可访问的路由表 router.addRoutes(store.getters.addRouters) // 动态添加可访问权限路由表 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 }) }).catch((err) => { store.dispatch('LogOut').then(() => { Message.error(err || 'Verification failed, please login again') next({ path: '/' }) }) }) } else { // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ if (hasPermission(store.getters.roles, to.meta.roles)) { next()// } else { next({ path: '/401', replace: true, query: { noGoBack: true }}) } } } } else { if (whiteList.indexOf(to.path) !== -1) { // 点击退出时,会定位到这里 next() } else { next('/login') NProgress.done() } } }) router.afterEach(() => { NProgress.done() // 结束Progress setTimeout(() => { const browserHeaderTitle = store.getters.browserHeaderTitle setTitle(browserHeaderTitle) }, 0) })
一、路由对象区分权限路由对象和非权限路由对象;初始化时,将非权限路由对象赋值给Router;同时设置权限路由中的meta对象,如:meta:{roles:['admin','editor']},表示该roles所拥有的路由权限;web
二、经过用户登陆成功以后返回的roles值,进行路由的匹配并生成新的路由对象;ajax
三、用户成功登陆并跳转到首页时,根据刚刚生成的路由对象,渲染左侧的菜单;即,不一样的用户看到的菜单是不同的;vuex
一、用户点击登陆按钮,经过路由导航钩子router.beforeEach()函数肯定下一步的跳转逻辑,以下:
1.一、用户已经登陆成功过,并从cookie中拿到了token值; 1.1.一、用户访问登陆页面,直接定位到登陆页面; 1.1.一、用户访问非登陆页面,须要根据用户是否有roles信息,进行不一样的业务逻辑,以下: (1)、初始状况下,用户roles信息为空; 1.经过getUserInfo()函数,根据token拉取用户信息;并经过store将该用户roles,name,avatar信息存储于vuex; 2.经过store.dispatch('GenerateRoutes', { roles })去从新过滤和生成路由,经过router.addRoutes()合并路由表; 3.若是在获取用户信息接口时出现错误,则调取store.dispatch('LogOut')接口,返回到login页面; (2)、用户已经拥有roles信息; 1.点击页面路由,经过roles权限判断 hasPermission()。若是用户有该路由权限,直接跳转对应的页面;若是没有权限,则跳转至401提示页面;
2.用户点击退出,token已被清空
1.若是设置了白名单用户,则直接跳转到相应的页面; 2.反之,则跳转至登陆页面;
详细代码,请参考src/permission.js
测试帐号:
(1). username: admin,password: 123456;admin拥有最高权限,能够查看全部的页面和按钮;
(2). username: editor,password: 123456;editor只有被赋予权限的页面和按钮才能够看到;
如图所示,在完成通常后台系统所具备的二级导航菜单功能以后,我发现其实不少的后台管理系统都有三级导航菜单,可是若是都把三级菜单放到左侧菜单作阶梯状排列,就会显得比较紧凑,所以我以为把全部的三级菜单放到顶部是一个不错的选择。
点击左侧菜单,找到其对应的菜单(顶栏菜单)排放于顶部导航栏;
经过element-ui,NavMenu 导航菜单来进行顶部菜单的展现,注意顶栏和侧栏设置的区别;同时将其引用于头部组件headNav.vue中;
格式以下:
export const topRouterMap = [ { 'parentName':'infoShow', 'topmenulist':[ { path: 'infoShow1', name: 'infoShow1', meta: { title: '我的信息子菜单1', icon: 'fa-asterisk', routerType: 'topmenu' }, component: () => import('@/page/fundList/moneyData') } ] }, { 'parentName':'chinaTabsList', 'topmenulist':[ { path:'chinaTabsList1', name:'chinaTabsList1', meta:{ title:'区域投资子菜单1', icon:'fa-asterisk', routerType:'topmenu' }, component: () => import('@/page/fundList/moneyData') } ] } ]
定义topRouterMap为路由总数组;经过parentName来与左侧路由创建联系;经过topmenulist表示该顶栏路由的值;经过meta.routerType的值为"topmenu"或"leftmenu"来区分是顶栏路由,仍是左侧路由;
思路:点击左侧菜单,须要显示顶部对应的菜单。由于左侧菜单要和顶部菜单创建联系。咱们知道导航菜单在用户登陆时,会根据用户的role信息进行权限过滤;那么,在过滤权限路由数据以前,咱们能够经过addTopRouter()将全部的三级菜单进行过滤添加,添加完成以后,继续进行角色过滤,能够保证将不具有权限的顶部菜单也过滤掉。
// src/store/permission.js,经过循环过滤,生成新的二级菜单 function addTopRouter(){ asyncRouterMap.forEach( (item) => { if(item.children && item.children.length >= 1){ item.children.forEach((sitem) => { topRouterMap.forEach((citem) => { if(sitem.name === citem.parentName){ let newChildren = item.children.concat(citem.topmenulist); item.children = newChildren; } }) }) } }) return asyncRouterMap; }
在组件topMenu.vue中,用户默认进来或者点击左侧菜单,触发setLeftInnerMenu()函数,以下:
setLeftInnerMenu(){ const titleList = this.$route.matched[1].meta.titleList; const currentTitle = titleList && this.$route.matched[2].meta.title; if( titleList && this.$route.matched[1].meta.routerType === 'leftmenu'){ // 点击的为 左侧的2级菜单 this.$store.dispatch('ClickLeftInnerMenu',{'titleList':titleList}); this.$store.dispatch('ClickTopMenu',{'title':currentTitle}); }else{ // 点击左侧1级菜单 this.$store.dispatch('ClickLeftInnerMenu',{'titleList':[]}); this.$store.dispatch('ClickTopMenu',{'title':''}); } }
经过当前路由this.$route.meta.routerType的值判断,用户是点击顶部菜单仍是左侧菜单。若是点击顶部菜单,经过this.$store触发异步动做'ClickLeftInnerMenu'并传递参数'name',vuex中经过state.topRouters = filterTopRouters(state.routers,data)过滤当前路由信息;代码以下:
// src/store/permission.js,获取到当前路由对应顶部子菜单 function filterTopRouters(data){ let topRouters = topRouterMap.find((item)=>{ return item.parentName === data.name }) if(!mutils.isEmpty(topRouters)){ return topRouters.topmenulist; } }
topMenu.vue中,经过 computed:{ ...mapGetters(['topRouters'])}进行对应顶部路由数据的展现。用户每次点击左侧菜单时,顶部路由都进行了从新赋值并渲染,保证了数据的准确性。
当顶部菜单的数据量过大时,咱们须要设置横向滚动条并设置滚动条的样式。
如图:
在使用easy-mock模拟数据的过程当中,发现其对表格固定数据不能实现增删改等功能,而且因为它们是免费提供服务,致使用户量较大时,服务器常常没法访问,于是选择了使用mockjs进行本地数据模拟。
Mock.js是一款模拟数据生成器,旨在帮助前端攻城师独立于后端进行开发,帮助编写单元测试。提供了如下模拟功能:
1.根据数据模板生成模拟数据,经过mockjs提供的方法,你能够轻松地创造大量随机的文本,数字,布尔值,日期,邮箱,连接,图片,颜色等.
2.模拟 Ajax 请求,生成并返回模拟数据,mockjs能够进行强大的ajax拦截.能判断请求类型,获取到url,请求参数等.而后能够返回mock的假数据,或者你本身编好的json文件.功能强大易上手.
3.基于 HTML 模板生成模拟数据
npm install mockjs --save-dev
如图:
mockjs/index.js,负责定义相关的mock接口,以下:
import Mock from 'mockjs' import tableAPI from './money' // 设置全局延时 没有延时的话有时候会检测不到数据变化 建议保留 Mock.setup({ timeout: '300-600' }) // 资金相关 Mock.mock(/\/money\/get/, 'get', tableAPI.getMoneyList) Mock.mock(/\/money\/remove/, 'get', tableAPI.deleteMoney) Mock.mock(/\/money\/batchremove/, 'get', tableAPI.batchremoveMoney) Mock.mock(/\/money\/add/, 'get', tableAPI.createMoney) Mock.mock(/\/money\/edit/, 'get', tableAPI.updateMoney)
mockjs/money.js,则定义相关的函数,实现模拟数据的业务逻辑,好比资金流水数据的增删改查等;数据的生成规则请参照mockjs官网文档,上面有详细的语法说明;
以下:
import './mockjs' //引用mock
src/api/money.js中,进行了统一的接口封装,在页面中调用对应函数,便可获取到相应的模拟数据。代码以下:
import request from '@/utils/axios' export function getMoneyIncomePay(params) { return request({ url: '/money/get', method: 'get', params: params }) } export function addMoney(params) { return request({ url: '/money/add', method: 'get', params: params }) }
因为项目早期使用vue-cli2.0构建项目,须要进行繁琐的webpack配置;vue-cli 3.0集成了webpack配置并在性能提高上作了很大优化。由于本项目使用vue-cli3.0进行构建和升级。现将相关注意事项总结以下,详细文档,请参考官网介绍。
Vue CLI 的包名称由 vue-cli 改为了 @vue/cli。
若是你已经全局安装了旧版本的 vue-cli (1.x 或 2.x),你须要先经过
npm uninstall vue-cli -g 或 yarn global remove vue-cli
卸载它。
Vue CLI 须要 Node.js 8.9 或更高版本 (推荐 8.11.0+)。你可使用 nvm 或 nvm-windows 在同一台电脑中管理多个 Node 版本。
npm install -g @vue/cli # OR yarn global add @vue/cli
若是但愿还保留 vue-cli2.x 的语法或使用 2.x 的模板,建议安装 cli-init
npm install -g @vue/cli-init # OR yarn global add @vue/cli-init
`
vue create 项目名称;
`
安装步骤选取相关的配置信息便可,直到完成。
在根目录下新建文件.env.development和.env.production,分别表示开发环境和生成环境配置;主要用于定义环境变量,并经过npm run serve或npm run build集成到不一样的环境中,供接口调用。代码以下:
.env.development
NODE_ENV = development VUE_APP_URL = "https://easy-mock.com/mock/5cd03667adb0973be6a3d8d1/api"
.env.production
NODE_ENV = production VUE_APP_URL = "https://easy-mock.com/mock/5cd03667adb0973be6a3d8d1/api"
使用方法,如本项目中,配置在utils/env.js中,代码以下:
// development和production环境是不一样的 let app_url = process.env.VUE_APP_URL export default { app_url }
因为使用vue-cli3.x生成项目,webpack相关配置已经集成到node_module中,若是但愿对 webpack 等进行细致化配置,须要在项目根目录下新建文件vue.config.js,具体配置可参考文档,下面是一份基本配置。
const TerserPlugin = require('terser-webpack-plugin') // 用于在生成环境剔除debuger和console const path = require('path'); const resolve = dir => { return path.join(__dirname, dir); }; const env = process.env.NODE_ENV let target = process.env.VUE_APP_URL // development和production环境是不一样的 module.exports = { publicPath: '/', outputDir: './dist', lintOnSave: false, // 关闭eslint // 打包时不生成.map文件 productionSourceMap: false, devServer: { open: true, host: '0.0.0.0', port: 8808 // 因为本项目数据经过easy-mock和mockjs模拟,不存在跨域问题,无需配置代理; // proxy: { // '/v2': { // target: target, // changeOrigin: true // } // } }, // webpack相关配置 chainWebpack: (config) => { config.entry.app = ['./src/main.js']; config.resolve.alias .set('@', resolve('src')) .set('cps', resolve('src/components')) }, configureWebpack:config => { // 为生产环境修改配置... if (process.env.NODE_ENV === 'production') { new TerserPlugin({ cache: true, parallel: true, sourceMap: true, // Must be set to true if using source-maps in production terserOptions: { compress: { drop_console: true, drop_debugger: true } } }) } else { // 为开发环境修改配置... } }, // 第三方插件配置 pluginOptions: { } }
项目配置完成好,安装好全部的依赖包。执行开发环境打包命令:npm run serve,便可运行项目;执行生成环境打包命令:npm run build,便可生成生产环境文件。
项目开发至此,一些基本的功能都已经完成,基本上可以知足项目须要。下篇文章会继续介绍"项目分享功能的实现细节"、"项目部署细节及注意事项(包括如何部署子目录)"、"项目性能优化细节",但愿你们敬请期待~
项目说明:
小爱ADMIN是彻底开源免费的管理系统集成方案,能够直接应用于相关后台管理系统模板;不少重点地方都作了详细的注释和解释。若是你也同样喜欢前端开发,欢迎加入咱们的讨论/学习群,群内能够提问答疑,分享学习资料;
欢迎加入答疑qq群。