From a7ccc6078b29694ce41e2e108e6ba0f2288962d9 Mon Sep 17 00:00:00 2001 From: Serge Gao Date: Thu, 14 Feb 2019 19:26:09 +0800 Subject: [PATCH 1/4] feat dynamic routes --- src/api/routes.js | 8 + src/mock/index.js | 4 + src/mock/routes.js | 570 ++++++++++++++++++++++++++++++++ src/permission.js | 42 ++- src/router/index.js | 269 +-------------- src/store/modules/permission.js | 65 ++-- 6 files changed, 650 insertions(+), 308 deletions(-) create mode 100644 src/api/routes.js create mode 100644 src/mock/routes.js diff --git a/src/api/routes.js b/src/api/routes.js new file mode 100644 index 00000000..95fc96c9 --- /dev/null +++ b/src/api/routes.js @@ -0,0 +1,8 @@ +import request from '@/utils/request' + +export function fetchAsyncRoutes() { + return request({ + url: '/routes', + method: 'get' + }) +} diff --git a/src/mock/index.js b/src/mock/index.js index 3e00e918..08afec3b 100644 --- a/src/mock/index.js +++ b/src/mock/index.js @@ -3,6 +3,7 @@ import loginAPI from './login' import articleAPI from './article' import remoteSearchAPI from './remoteSearch' import transactionAPI from './transaction' +import routesAPI from './routes' // 修复在使用 MockJS 情况下,设置 withCredentials = true,且未被拦截的跨域请求丢失 Cookies 的问题 // https://github.com/nuysoft/Mock/issues/300 @@ -18,6 +19,9 @@ Mock.XHR.prototype.send = function() { // timeout: '350-600' // }) +// 路由表相关 +Mock.mock(/\/routes/, 'get', routesAPI.getAsyncRoutesMap) + // 登录相关 Mock.mock(/\/login\/login/, 'post', loginAPI.loginByUsername) Mock.mock(/\/login\/logout/, 'post', loginAPI.logout) diff --git a/src/mock/routes.js b/src/mock/routes.js new file mode 100644 index 00000000..330e903f --- /dev/null +++ b/src/mock/routes.js @@ -0,0 +1,570 @@ +const asyncRoutesMap = [ + { + id: '1', + path: '/permission', + component: 'layout/Layout', + redirect: '/permission/index', + alwaysShow: true, // will always show the root menu + meta: { + title: 'permission', + icon: 'lock', + roles: ['admin', 'editor'] // you can set roles in root nav + }, + children: [ + { + id: '2', + path: 'page', + component: 'permission/page', + name: 'PagePermission', + meta: { + title: 'pagePermission', + roles: ['admin'] + } + }, + { + id: '3', + path: 'directive', + component: 'permission/directive', + name: 'DirectivePermission', + meta: { + title: 'directivePermission', + roles: ['admin', 'editor'] + } + } + ] + }, + + { + id: '4', + path: '/icon', + component: 'layout/Layout', + children: [ + { + id: '5', + path: 'index', + component: 'svg-icons/index', + name: 'Icons', + meta: { title: 'icons', icon: 'icon', noCache: true, roles: ['admin', 'editor'] } + } + ] + }, + { + id: '6', + path: '/components', + component: 'layout/Layout', + redirect: 'noredirect', + name: 'ComponentDemo', + meta: { + title: 'components', + icon: 'component', + roles: ['admin', 'editor'] + }, + children: [ + { + id: '7', + path: 'tinymce', + component: 'components-demo/tinymce', + name: 'TinymceDemo', + meta: { title: 'tinymce', roles: ['admin', 'editor'] } + }, + { + id: '8', + path: 'markdown', + component: 'components-demo/markdown', + name: 'MarkdownDemo', + meta: { title: 'markdown', roles: ['admin', 'editor'] } + }, + { + id: '9', + path: 'json-editor', + component: 'components-demo/jsonEditor', + name: 'JsonEditorDemo', + meta: { title: 'jsonEditor', roles: ['admin', 'editor'] } + }, + { + id: '10', + path: 'splitpane', + component: 'components-demo/splitpane', + name: 'SplitpaneDemo', + meta: { title: 'splitPane', roles: ['admin', 'editor'] } + }, + { + id: '11', + path: 'avatar-upload', + component: 'components-demo/avatarUpload', + name: 'AvatarUploadDemo', + meta: { title: 'avatarUpload', roles: ['admin', 'editor'] } + }, + { + id: '12', + path: 'dropzone', + component: 'components-demo/dropzone', + name: 'DropzoneDemo', + meta: { title: 'dropzone', roles: ['admin', 'editor'] } + }, + { + id: '13', + path: 'sticky', + component: 'components-demo/sticky', + name: 'StickyDemo', + meta: { title: 'sticky', roles: ['admin', 'editor'] } + }, + { + id: '14', + path: 'count-to', + component: 'components-demo/countTo', + name: 'CountToDemo', + meta: { title: 'countTo', roles: ['admin', 'editor'] } + }, + { + id: '15', + path: 'mixin', + component: 'components-demo/mixin', + name: 'ComponentMixinDemo', + meta: { title: 'componentMixin', roles: ['admin', 'editor'] } + }, + { + id: '16', + path: 'back-to-top', + component: 'components-demo/backToTop', + name: 'BackToTopDemo', + meta: { title: 'backToTop', roles: ['admin', 'editor'] } + }, + { + id: '17', + path: 'drag-dialog', + component: 'components-demo/dragDialog', + name: 'DragDialogDemo', + meta: { title: 'dragDialog', roles: ['admin', 'editor'] } + }, + { + id: '18', + path: 'drag-select', + component: 'components-demo/dragSelect', + name: 'DragSelectDemo', + meta: { title: 'dragSelect', roles: ['admin', 'editor'] } + }, + { + id: '19', + path: 'dnd-list', + component: 'components-demo/dndList', + name: 'DndListDemo', + meta: { title: 'dndList', roles: ['admin', 'editor'] } + }, + { + id: '20', + path: 'drag-kanban', + component: 'components-demo/dragKanban', + name: 'DragKanbanDemo', + meta: { title: 'dragKanban', roles: ['admin', 'editor'] } + } + ] + }, + { + id: '21', + path: '/charts', + component: 'layout/Layout', + redirect: 'noredirect', + name: 'Charts', + meta: { + title: 'charts', + icon: 'chart', + roles: ['admin', 'editor'] + }, + children: [ + { + id: '22', + path: 'keyboard', + component: 'charts/keyboard', + name: 'KeyboardChart', + meta: { title: 'keyboardChart', noCache: true, roles: ['admin', 'editor'] } + }, + { + id: '23', + path: 'line', + component: 'charts/line', + name: 'LineChart', + meta: { title: 'lineChart', noCache: true, roles: ['admin', 'editor'] } + }, + { + id: '24', + path: 'mixchart', + component: 'charts/mixChart', + name: 'MixChart', + meta: { title: 'mixChart', noCache: true, roles: ['admin', 'editor'] } + } + ] + }, + { + id: '25', + path: '/nested', + component: 'layout/Layout', + redirect: '/nested/menu1/menu1-1', + name: 'Nested', + meta: { + title: 'nested', + icon: 'nested', + roles: ['admin', 'editor'] + }, + children: [ + { + id: '26', + path: 'menu1', + component: 'nested/menu1/index', // Parent router-view + name: 'Menu1', + meta: { title: 'menu1', roles: ['admin', 'editor'] }, + redirect: '/nested/menu1/menu1-1', + children: [ + { + id: '27', + path: 'menu1-1', + component: 'nested/menu1/menu1-1', + name: 'Menu1-1', + meta: { title: 'menu1-1', roles: ['admin', 'editor'] } + }, + { + id: '28', + path: 'menu1-2', + component: 'nested/menu1/menu1-2', + name: 'Menu1-2', + redirect: '/nested/menu1/menu1-2/menu1-2-1', + meta: { title: 'menu1-2', roles: ['admin', 'editor'] }, + children: [ + { + id: '29', + path: 'menu1-2-1', + component: 'nested/menu1/menu1-2/menu1-2-1', + name: 'Menu1-2-1', + meta: { title: 'menu1-2-1', roles: ['admin', 'editor'] } + }, + { + id: '30', + path: 'menu1-2-2', + component: 'nested/menu1/menu1-2/menu1-2-2', + name: 'Menu1-2-2', + meta: { title: 'menu1-2-2', roles: ['admin', 'editor'] } + } + ] + }, + { + id: '31', + path: 'menu1-3', + component: 'nested/menu1/menu1-3', + name: 'Menu1-3', + meta: { title: 'menu1-3', roles: ['admin', 'editor'] } + } + ] + }, + { + id: '32', + path: 'menu2', + name: 'Menu2', + component: 'nested/menu2/index', + meta: { title: 'menu2', roles: ['admin', 'editor'] } + } + ] + }, + { + id: '33', + path: '/table', + component: 'layout/Layout', + redirect: '/table/complex-table', + name: 'Table', + meta: { + title: 'Table', + icon: 'table', + roles: ['admin', 'editor'] + }, + children: [ + { + id: '34', + path: 'dynamic-table', + component: 'table/dynamicTable/index', + name: 'DynamicTable', + meta: { title: 'dynamicTable', roles: ['admin', 'editor'] } + }, + { + id: '35', + path: 'drag-table', + component: 'table/dragTable', + name: 'DragTable', + meta: { title: 'dragTable', roles: ['admin', 'editor'] } + }, + { + id: '36', + path: 'inline-edit-table', + component: 'table/inlineEditTable', + name: 'InlineEditTable', + meta: { title: 'inlineEditTable', roles: ['admin', 'editor'] } + }, + { + id: '37', + path: 'tree-table', + component: 'table/treeTable/treeTable', + name: 'TreeTableDemo', + meta: { title: 'treeTable', roles: ['admin', 'editor'] } + }, + { + id: '38', + path: 'custom-tree-table', + component: 'table/treeTable/customTreeTable', + name: 'CustomTreeTableDemo', + meta: { title: 'customTreeTable', roles: ['admin', 'editor'] } + }, + { + id: '39', + path: 'complex-table', + component: 'table/complexTable', + name: 'ComplexTable', + meta: { title: 'complexTable', roles: ['admin', 'editor'] } + } + ] + }, + { + id: '40', + path: '/example', + component: 'layout/Layout', + redirect: '/example/list', + name: 'Example', + meta: { + title: 'example', + icon: 'example', + roles: ['admin', 'editor'] + }, + children: [ + { + id: '41', + path: 'create', + component: 'example/create', + name: 'CreateArticle', + meta: { title: 'createArticle', icon: 'edit', roles: ['admin', 'editor'] } + }, + { + id: '42', + path: 'edit/:id(\\d+)', + component: 'example/edit', + name: 'EditArticle', + meta: { title: 'editArticle', noCache: true, roles: ['admin', 'editor'] }, + hidden: true + }, + { + id: '43', + path: 'list', + component: 'example/list', + name: 'ArticleList', + meta: { title: 'articleList', icon: 'list', roles: ['admin', 'editor'] } + } + ] + }, + + { + path: '/tab', + component: 'layout/Layout', + children: [ + { + id: '44', + path: 'index', + component: 'tab/index', + name: 'Tab', + meta: { title: 'tab', icon: 'tab', roles: ['admin', 'editor'] } + } + ] + }, + { + id: '45', + path: '/error', + component: 'layout/Layout', + redirect: 'noredirect', + name: 'ErrorPages', + meta: { + title: 'errorPages', + icon: '404', + roles: ['admin', 'editor'] + }, + children: [ + { + id: '46', + path: '401', + component: 'errorPage/401', + name: 'Page401', + meta: { title: 'page401', noCache: true, roles: ['admin', 'editor'] } + }, + { + id: '47', + path: '404', + component: 'errorPage/404', + name: 'Page404', + meta: { title: 'page404', noCache: true, roles: ['admin', 'editor'] } + } + ] + }, + + { + id: '48', + path: '/error-log', + component: 'layout/Layout', + redirect: 'noredirect', + children: [ + { + path: 'log', + component: 'errorLog/index', + name: 'ErrorLog', + meta: { title: 'errorLog', icon: 'bug', roles: ['admin', 'editor'] } + } + ] + }, + + { + id: '49', + path: '/excel', + component: 'layout/Layout', + redirect: '/excel/export-excel', + name: 'Excel', + meta: { + title: 'excel', + icon: 'excel', + roles: ['admin', 'editor'] + }, + children: [ + { + id: '50', + path: 'export-excel', + component: 'excel/exportExcel', + name: 'ExportExcel', + meta: { title: 'exportExcel', roles: ['admin', 'editor'] } + }, + { + id: '51', + path: 'export-selected-excel', + component: 'excel/selectExcel', + name: 'SelectExcel', + meta: { title: 'selectExcel', roles: ['admin', 'editor'] } + }, + { + id: '52', + path: 'upload-excel', + component: 'excel/uploadExcel', + name: 'UploadExcel', + meta: { title: 'uploadExcel', roles: ['admin', 'editor'] } + } + ] + }, + + // { + // id: '53', + // path: '/zip', + // component: 'layout/Layout', + // redirect: '/zip/download', + // alwaysShow: true, + // meta: { title: 'zip', icon: 'zip', roles: ['admin', 'editor'] }, + // children: [ + // { + // id: '54', + // path: 'download', + // component: 'zip/index', + // name: 'ExportZip', + // meta: { title: 'exportZip', roles: ['admin', 'editor'] } + // } + // ] + // }, + + { + id: '55', + path: '/pdf', + component: 'layout/Layout', + redirect: '/pdf/index', + children: [ + { + id: '56', + path: 'index', + component: 'pdf/index', + name: 'PDF', + meta: { title: 'pdf', icon: 'pdf', roles: ['admin', 'editor'] } + } + ] + }, + { + id: '57', + path: '/pdf/download', + component: 'pdf/download', + hidden: true + }, + + { + id: '58', + path: '/theme', + component: 'layout/Layout', + redirect: 'noredirect', + meta: { + roles: ['admin', 'editor'] + }, + children: [ + { + id: '59', + path: 'index', + component: 'theme/index', + name: 'Theme', + meta: { title: 'theme', icon: 'theme', roles: ['admin', 'editor'] } + } + ] + }, + + { + id: '60', + path: '/clipboard', + component: 'layout/Layout', + redirect: 'noredirect', + meta: { + roles: ['admin', 'editor'] + }, + children: [ + { + id: '61', + path: 'index', + component: 'clipboard/index', + name: 'ClipboardDemo', + meta: { title: 'clipboardDemo', icon: 'clipboard', roles: ['admin', 'editor'] } + } + ] + }, + + { + id: '62', + path: '/i18n', + component: 'layout/Layout', + meta: { + roles: ['admin', 'editor'] + }, + children: [ + { + id: '63', + path: 'index', + component: 'i18n-demo/index', + name: 'I18n', + meta: { title: 'i18n', icon: 'international', roles: ['admin', 'editor'] } + } + ] + }, + + { + id: '64', + path: 'external-link', + component: 'layout/Layout', + meta: { + roles: ['admin', 'editor'] + }, + children: [ + { + id: '65', + path: 'https://github.com/PanJiaChen/vue-element-admin', + meta: { title: 'externalLink', icon: 'link', roles: ['admin', 'editor'] } + } + ] + } +] + +export default { + getAsyncRoutesMap() { + return asyncRoutesMap + } +} diff --git a/src/permission.js b/src/permission.js index e556cb00..d602582d 100644 --- a/src/permission.js +++ b/src/permission.js @@ -2,41 +2,48 @@ import router from './router' import store from './store' import { Message } from 'element-ui' import NProgress from 'nprogress' // progress bar -import 'nprogress/nprogress.css'// progress bar style +import 'nprogress/nprogress.css' // progress bar style import { getToken } from '@/utils/auth' // getToken from cookie -NProgress.configure({ showSpinner: false })// NProgress Configuration +NProgress.configure({ showSpinner: false }) // NProgress Configuration // permission judge function function hasPermission(roles, permissionRoles) { - if (roles.indexOf('admin') >= 0) return true // admin permission passed directly + if (roles.findIndex(role => role === 'admin') >= 0) return true // admin permission passed directly if (!permissionRoles) return true return roles.some(role => permissionRoles.indexOf(role) >= 0) } -const whiteList = ['/login', '/auth-redirect']// no redirect whitelist +const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist router.beforeEach((to, from, next) => { NProgress.start() // start progress bar - if (getToken()) { // determine if there has token + if (getToken()) { + // determine if there has token /* has token*/ if (to.path === '/login') { next({ path: '/' }) NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it } else { - if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 - store.dispatch('GetUserInfo').then(res => { // 拉取user_info - const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop'] - store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表 - router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表 - next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record + if (store.getters.roles.length === 0) { + // 判断当前用户是否已拉取完user_info信息 + store + .dispatch('GetUserInfo') + .then(res => { + // 拉取user_info + const roles = res.data.roles // note: roles must be a object array! such as: [{id: '1', name: 'editor'}, {id: '2', name: 'developer'}] + store.dispatch('GenerateRoutes', { roles }).then(excessRoutes => { + // 根据roles权限生成可访问的路由表 + router.addRoutes(excessRoutes) // 动态添加可访问路由表 + next({ ...to, replace: true }) // hack方法 确保addRouters已完成 ,set the replace: true so the navigation will not leave a history record + }) }) - }).catch((err) => { - store.dispatch('FedLogOut').then(() => { - Message.error(err) - next({ path: '/' }) + .catch(err => { + store.dispatch('FedLogOut').then(() => { + Message.error(err) + next({ path: '/' }) + }) }) - }) } else { // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ if (hasPermission(store.getters.roles, to.meta.roles)) { @@ -49,7 +56,8 @@ router.beforeEach((to, from, next) => { } } else { /* has no token*/ - if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 + if (whiteList.indexOf(to.path) !== -1) { + // 在免登录白名单,直接进入 next() } else { next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页 diff --git a/src/router/index.js b/src/router/index.js index dda18156..a43a22b5 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -6,12 +6,6 @@ Vue.use(Router) /* Layout */ import Layout from '@/views/layout/Layout' -/* Router Modules */ -import componentsRouter from './modules/components' -import chartsRouter from './modules/charts' -import tableRouter from './modules/table' -import nestedRouter from './modules/nested' - /** note: Submenu only appear when children.length>=1 * detail see https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html **/ @@ -31,7 +25,7 @@ import nestedRouter from './modules/nested' breadcrumb: false if false, the item will hidden in breadcrumb(default is true) } **/ -export const constantRouterMap = [ +export const constantRoutes = [ { path: '/redirect', component: Layout, @@ -104,265 +98,10 @@ export const constantRouterMap = [ } ] +export const generalRoutes = [{ path: '*', redirect: '/404', hidden: true }] + export default new Router({ // mode: 'history', // require service support scrollBehavior: () => ({ y: 0 }), - routes: constantRouterMap + routes: constantRoutes }) - -export const asyncRouterMap = [ - { - path: '/permission', - component: Layout, - redirect: '/permission/index', - alwaysShow: true, // will always show the root menu - meta: { - title: 'permission', - icon: 'lock', - roles: ['admin', 'editor'] // you can set roles in root nav - }, - children: [ - { - path: 'page', - component: () => import('@/views/permission/page'), - name: 'PagePermission', - meta: { - title: 'pagePermission', - roles: ['admin'] // or you can only set roles in sub nav - } - }, - { - path: 'directive', - component: () => import('@/views/permission/directive'), - name: 'DirectivePermission', - meta: { - title: 'directivePermission' - // if do not set roles, means: this page does not require permission - } - } - ] - }, - - { - path: '/icon', - component: Layout, - children: [ - { - path: 'index', - component: () => import('@/views/svg-icons/index'), - name: 'Icons', - meta: { title: 'icons', icon: 'icon', noCache: true } - } - ] - }, - - /** When your routing table is too long, you can split it into small modules**/ - componentsRouter, - chartsRouter, - nestedRouter, - tableRouter, - - { - path: '/example', - component: Layout, - redirect: '/example/list', - name: 'Example', - meta: { - title: 'example', - icon: 'example' - }, - children: [ - { - path: 'create', - component: () => import('@/views/example/create'), - name: 'CreateArticle', - meta: { title: 'createArticle', icon: 'edit' } - }, - { - path: 'edit/:id(\\d+)', - component: () => import('@/views/example/edit'), - name: 'EditArticle', - meta: { title: 'editArticle', noCache: true }, - hidden: true - }, - { - path: 'list', - component: () => import('@/views/example/list'), - name: 'ArticleList', - meta: { title: 'articleList', icon: 'list' } - } - ] - }, - - { - path: '/tab', - component: Layout, - children: [ - { - path: 'index', - component: () => import('@/views/tab/index'), - name: 'Tab', - meta: { title: 'tab', icon: 'tab' } - } - ] - }, - - { - path: '/error', - component: Layout, - redirect: 'noredirect', - name: 'ErrorPages', - meta: { - title: 'errorPages', - icon: '404' - }, - children: [ - { - path: '401', - component: () => import('@/views/errorPage/401'), - name: 'Page401', - meta: { title: 'page401', noCache: true } - }, - { - path: '404', - component: () => import('@/views/errorPage/404'), - name: 'Page404', - meta: { title: 'page404', noCache: true } - } - ] - }, - - { - path: '/error-log', - component: Layout, - redirect: 'noredirect', - children: [ - { - path: 'log', - component: () => import('@/views/errorLog/index'), - name: 'ErrorLog', - meta: { title: 'errorLog', icon: 'bug' } - } - ] - }, - - { - path: '/excel', - component: Layout, - redirect: '/excel/export-excel', - name: 'Excel', - meta: { - title: 'excel', - icon: 'excel' - }, - children: [ - { - path: 'export-excel', - component: () => import('@/views/excel/exportExcel'), - name: 'ExportExcel', - meta: { title: 'exportExcel' } - }, - { - path: 'export-selected-excel', - component: () => import('@/views/excel/selectExcel'), - name: 'SelectExcel', - meta: { title: 'selectExcel' } - }, - { - path: 'upload-excel', - component: () => import('@/views/excel/uploadExcel'), - name: 'UploadExcel', - meta: { title: 'uploadExcel' } - } - ] - }, - - { - path: '/zip', - component: Layout, - redirect: '/zip/download', - alwaysShow: true, - meta: { title: 'zip', icon: 'zip' }, - children: [ - { - path: 'download', - component: () => import('@/views/zip/index'), - name: 'ExportZip', - meta: { title: 'exportZip' } - } - ] - }, - - { - path: '/pdf', - component: Layout, - redirect: '/pdf/index', - children: [ - { - path: 'index', - component: () => import('@/views/pdf/index'), - name: 'PDF', - meta: { title: 'pdf', icon: 'pdf' } - } - ] - }, - { - path: '/pdf/download', - component: () => import('@/views/pdf/download'), - hidden: true - }, - - { - path: '/theme', - component: Layout, - redirect: 'noredirect', - children: [ - { - path: 'index', - component: () => import('@/views/theme/index'), - name: 'Theme', - meta: { title: 'theme', icon: 'theme' } - } - ] - }, - - { - path: '/clipboard', - component: Layout, - redirect: 'noredirect', - children: [ - { - path: 'index', - component: () => import('@/views/clipboard/index'), - name: 'ClipboardDemo', - meta: { title: 'clipboardDemo', icon: 'clipboard' } - } - ] - }, - - { - path: '/i18n', - component: Layout, - children: [ - { - path: 'index', - component: () => import('@/views/i18n-demo/index'), - name: 'I18n', - meta: { title: 'i18n', icon: 'international' } - } - ] - }, - - { - path: 'external-link', - component: Layout, - children: [ - { - path: 'https://github.com/PanJiaChen/vue-element-admin', - meta: { title: 'externalLink', icon: 'link' } - } - ] - }, - - { path: '*', redirect: '/404', hidden: true } -] diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js index 13f60efb..96038dc2 100644 --- a/src/store/modules/permission.js +++ b/src/store/modules/permission.js @@ -1,5 +1,7 @@ -import { asyncRouterMap, constantRouterMap } from '@/router' +import { constantRoutes, generalRoutes } from '@/router' +import { fetchAsyncRoutes } from '@/api/routes' +const _import = path => () => import(`@/views/${path}`) /** * 通过meta.role判断是否与当前用户权限匹配 * @param roles @@ -13,50 +15,61 @@ function hasPermission(roles, route) { } } +// 将从服务器获得的路由表转换为vue-router的路由表 +function mapAsyncRoutes(asyncRoutes) { + return asyncRoutes.map(route => { + route.component && (route.component = _import(route.component)) + if (route.children) { + route.children = mapAsyncRoutes(route.children) + } + return route + }) +} + /** * 递归过滤异步路由表,返回符合用户角色权限的路由表 - * @param routes asyncRouterMap + * @param routes asyncRoutes * @param roles */ -function filterAsyncRouter(routes, roles) { - const res = [] - - routes.forEach(route => { - const tmp = { ...route } - if (hasPermission(roles, tmp)) { - if (tmp.children) { - tmp.children = filterAsyncRouter(tmp.children, roles) - } - res.push(tmp) +function filterAsyncRoutes(routes, roles) { + return routes.filter(route => { + if (!hasPermission(roles, route)) { + return false } + if (route.children) { + route.children = filterAsyncRoutes(route.children, roles) + } + return true }) - - return res } const permission = { state: { routers: [], - addRouters: [] + addRouters: [], + asyncRoutes: [] }, mutations: { - SET_ROUTERS: (state, routers) => { - state.addRouters = routers - state.routers = constantRouterMap.concat(routers) + SET_ROUTERS: (state, routes) => { + state.addRouters = routes + state.routers = constantRoutes.concat(routes) } }, actions: { GenerateRoutes({ commit }, data) { return new Promise(resolve => { const { roles } = data - let accessedRouters - if (roles.includes('admin')) { - accessedRouters = asyncRouterMap - } else { - accessedRouters = filterAsyncRouter(asyncRouterMap, roles) - } - commit('SET_ROUTERS', accessedRouters) - resolve() + let accessedRoutes + fetchAsyncRoutes().then(res => { + const asyncRoutes = res.data + if (roles.includes('admin')) { + accessedRoutes = mapAsyncRoutes(asyncRoutes).concat(generalRoutes) + } else { + accessedRoutes = mapAsyncRoutes(filterAsyncRoutes(asyncRoutes, roles)).concat(generalRoutes) + } + commit('SET_ROUTERS', accessedRoutes) + resolve(accessedRoutes) + }) }) } } From 4080fba0bfd60dfd1a2e957933a118a32330b7cb Mon Sep 17 00:00:00 2001 From: Serge Gao Date: Fri, 15 Feb 2019 11:06:02 +0800 Subject: [PATCH 2/4] feat role management --- src/api/role.js | 31 +++++ src/mock/index.js | 9 +- src/mock/role.js | 174 +++++++++++++++++++++++ src/mock/routes.js | 45 +++--- src/store/modules/permission.js | 2 + src/views/permission/role.vue | 240 ++++++++++++++++++++++++++++++++ 6 files changed, 483 insertions(+), 18 deletions(-) create mode 100644 src/api/role.js create mode 100644 src/mock/role.js create mode 100644 src/views/permission/role.vue diff --git a/src/api/role.js b/src/api/role.js new file mode 100644 index 00000000..b1720835 --- /dev/null +++ b/src/api/role.js @@ -0,0 +1,31 @@ +import request from '@/utils/request' + +export function fetchRoles() { + return request({ + url: '/roles', + method: 'get' + }) +} + +export function deleteRole(id) { + return request({ + url: `/roles/${id}`, + method: 'delete' + }) +} + +export function newRole(data) { + return request({ + url: '/roles', + method: 'post', + data + }) +} + +export function updateRole(id, data) { + return request({ + url: `/roles/${id}`, + method: 'put', + data + }) +} diff --git a/src/mock/index.js b/src/mock/index.js index 08afec3b..45d64eaf 100644 --- a/src/mock/index.js +++ b/src/mock/index.js @@ -4,6 +4,7 @@ import articleAPI from './article' import remoteSearchAPI from './remoteSearch' import transactionAPI from './transaction' import routesAPI from './routes' +import roleAPI from './role' // 修复在使用 MockJS 情况下,设置 withCredentials = true,且未被拦截的跨域请求丢失 Cookies 的问题 // https://github.com/nuysoft/Mock/issues/300 @@ -19,7 +20,7 @@ Mock.XHR.prototype.send = function() { // timeout: '350-600' // }) -// 路由表相关 +// 路由相关 Mock.mock(/\/routes/, 'get', routesAPI.getAsyncRoutesMap) // 登录相关 @@ -27,6 +28,12 @@ Mock.mock(/\/login\/login/, 'post', loginAPI.loginByUsername) Mock.mock(/\/login\/logout/, 'post', loginAPI.logout) Mock.mock(/\/user\/info\.*/, 'get', loginAPI.getUserInfo) +// 角色相关 +Mock.mock(/\/roles/, 'get', roleAPI.getRoles) +Mock.mock(/\/roles$/, 'post', roleAPI.newRole) +Mock.mock(/\/roles\/[A-Za-z0-9]+/, 'put', roleAPI.updateRole) +Mock.mock(/\/roles\/[A-Za-z0-9]+/, 'delete', roleAPI.deleteRole) + // 文章相关 Mock.mock(/\/article\/list/, 'get', articleAPI.getList) Mock.mock(/\/article\/detail/, 'get', articleAPI.getArticle) diff --git a/src/mock/role.js b/src/mock/role.js new file mode 100644 index 00000000..615a3859 --- /dev/null +++ b/src/mock/role.js @@ -0,0 +1,174 @@ +import Mock from 'mockjs' + +// admin 角色可以访问所有菜单 +const roles = [ + { + id: 'editor', // 角色id + name: 'editor', + description: '编辑', + accessibleRoutes: [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '19', + '20', + '21', + '22', + '23', + '24', + '25', + '26', + '27', + '28', + '29', + '30', + '31', + '32', + '33', + '34', + '35', + '36', + '37', + '38', + '39', + '40', + '41', + '42', + '43', + '44', + '45', + '46', + '47', + '48', + '49', + '50', + '51', + '52', + '53', + '54', + '55', + '56', + '57', + '58', + '59', + '60', + '61', + '62', + '63', + '64', + '65', + '66', + '67' + ] // 可访问的菜单id列表 + }, + { + id: '2', // 角色id + name: 'operator', + description: '运营', + accessibleRoutes: [ + '1', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '19', + '20', + '21', + '22', + '23', + '24', + '25', + '26', + '27', + '28', + '29', + '30', + '31', + '32', + '33', + '34', + '35', + '36', + '37', + '38', + '39', + '40', + '41', + '42', + '43', + '44', + '45', + '46', + '47', + '48', + '49', + '50', + '51', + '52', + '53', + '54', + '55', + '56', + '57', + '58', + '59', + '60', + '61', + '62', + '63', + '64', + '65' + ] // 可访问的菜单id列表 + } +] + +export default { + getRoles() { + return roles + }, + newRole() { + const res = { + data: Mock.mock('id') + } + return res + }, + updateRole() { + const res = { + data: 'success' + } + return res + }, + deleteRole() { + const res = { + data: 'success' + } + return res + } +} diff --git a/src/mock/routes.js b/src/mock/routes.js index 330e903f..480334a4 100644 --- a/src/mock/routes.js +++ b/src/mock/routes.js @@ -30,6 +30,16 @@ const asyncRoutesMap = [ title: 'directivePermission', roles: ['admin', 'editor'] } + }, + { + id: '66', + path: 'role', + component: 'permission/role', + name: 'role', + meta: { + title: 'roleManagement', + roles: ['admin'] + } } ] }, @@ -406,6 +416,7 @@ const asyncRoutesMap = [ redirect: 'noredirect', children: [ { + id: '67', path: 'log', component: 'errorLog/index', name: 'ErrorLog', @@ -450,23 +461,23 @@ const asyncRoutesMap = [ ] }, - // { - // id: '53', - // path: '/zip', - // component: 'layout/Layout', - // redirect: '/zip/download', - // alwaysShow: true, - // meta: { title: 'zip', icon: 'zip', roles: ['admin', 'editor'] }, - // children: [ - // { - // id: '54', - // path: 'download', - // component: 'zip/index', - // name: 'ExportZip', - // meta: { title: 'exportZip', roles: ['admin', 'editor'] } - // } - // ] - // }, + { + id: '53', + path: '/zip', + component: 'layout/Layout', + redirect: '/zip/download', + alwaysShow: true, + meta: { title: 'zip', icon: 'zip', roles: ['admin', 'editor'] }, + children: [ + { + id: '54', + path: 'download', + component: 'zip/index', + name: 'ExportZip', + meta: { title: 'exportZip', roles: ['admin', 'editor'] } + } + ] + }, { id: '55', diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js index 96038dc2..f52287b8 100644 --- a/src/store/modules/permission.js +++ b/src/store/modules/permission.js @@ -8,6 +8,8 @@ const _import = path => () => import(`@/views/${path}`) * @param route */ function hasPermission(roles, route) { + // 如果是隐藏的菜单, 都是可访问的, 因为隐藏的菜单不会出现在左侧菜单栏, 不可编辑权限 + if (route.hidden) return true if (route.meta && route.meta.roles) { return roles.some(role => route.meta.roles.includes(role)) } else { diff --git a/src/views/permission/role.vue b/src/views/permission/role.vue new file mode 100644 index 00000000..b6023a55 --- /dev/null +++ b/src/views/permission/role.vue @@ -0,0 +1,240 @@ + + + + + From a8e3a45fb55180a21d836678c5fa527966f36cdb Mon Sep 17 00:00:00 2001 From: Serge Gao Date: Fri, 15 Feb 2019 11:37:21 +0800 Subject: [PATCH 3/4] perf i18n for role permission --- src/lang/en.js | 8 ++++- src/lang/es.js | 1 + src/lang/zh.js | 8 ++++- src/mock/routes.js | 2 +- src/views/permission/role.vue | 55 ++++++++++++++++++----------------- 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/lang/en.js b/src/lang/en.js index 05b34598..5fcb275f 100644 --- a/src/lang/en.js +++ b/src/lang/en.js @@ -6,6 +6,7 @@ export default { guide: 'Guide', permission: 'Permission', pagePermission: 'Page Permission', + rolePermission: 'Role Permission', directivePermission: 'Directive Permission', icons: 'Icons', components: 'Components', @@ -86,9 +87,14 @@ export default { github: 'Github Repository' }, permission: { + newRole: 'New Role', + editPermission: 'Edit Permission', roles: 'Your roles', switchRoles: 'Switch roles', - tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.' + tips: 'In some cases it is not suitable to use v-permission, such as element Tab component or el-table-column and other asynchronous rendering dom cases which can only be achieved by manually setting the v-if.', + delete: 'Delete', + confirm: 'Confirm', + cancel: 'Cancel' }, guide: { description: 'The guide page is useful for some people who entered the project for the first time. You can briefly introduce the features of the project. Demo is based on ', diff --git a/src/lang/es.js b/src/lang/es.js index 8575d382..78844c22 100755 --- a/src/lang/es.js +++ b/src/lang/es.js @@ -5,6 +5,7 @@ export default { documentation: 'Documentación', guide: 'Guía', permission: 'Permisos', + rolePermission: 'Permisos de rol', pagePermission: 'Permisos de la página', directivePermission: 'Permisos de la directiva', icons: 'Iconos', diff --git a/src/lang/zh.js b/src/lang/zh.js index 1fd18355..411d25ab 100644 --- a/src/lang/zh.js +++ b/src/lang/zh.js @@ -5,6 +5,7 @@ export default { documentation: '文档', guide: '引导页', permission: '权限测试页', + rolePermission: '角色权限', pagePermission: '页面权限', directivePermission: '指令权限', icons: '图标', @@ -86,9 +87,14 @@ export default { github: 'Github 地址' }, permission: { + newRole: '新增角色', + editPermission: '编辑权限', roles: '你的权限', switchRoles: '切换权限', - tips: '在某些情况下,不适合使用 v-permission。例如:Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。' + tips: '在某些情况下,不适合使用 v-permission。例如:Element-UI 的 Tab 组件或 el-table-column 以及其它动态渲染 dom 的场景。你只能通过手动设置 v-if 来实现。', + delete: '删除', + confirm: '确定', + cancel: '取消' }, guide: { description: '引导页对于一些第一次进入项目的人很有用,你可以简单介绍下项目的功能。本 Demo 是基于', diff --git a/src/mock/routes.js b/src/mock/routes.js index 480334a4..678f4ee9 100644 --- a/src/mock/routes.js +++ b/src/mock/routes.js @@ -37,7 +37,7 @@ const asyncRoutesMap = [ component: 'permission/role', name: 'role', meta: { - title: 'roleManagement', + title: 'rolePermission', roles: ['admin'] } } diff --git a/src/views/permission/role.vue b/src/views/permission/role.vue index b6023a55..53eff624 100644 --- a/src/views/permission/role.vue +++ b/src/views/permission/role.vue @@ -1,47 +1,50 @@