Commit cf56a6c30fd2d9b8d0ce887f69895c474df818be
1 parent
80a28914e8
Exists in
master
init role permission
Showing
10 changed files
with
124 additions
and
16 deletions
Show diff stats
README-zh.md
1 | 1 | ||
2 | 如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) | ||
3 | 2 | ||
4 | ## 相关项目 | 3 | ## 相关项目 |
5 | 4 | ||
6 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) | 5 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) |
7 | 6 | ||
8 | - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) | 7 | - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) |
9 | 8 | ||
10 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) | 9 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) |
11 | 10 | ||
12 | - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) | 11 | - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) |
13 | 12 | ||
14 | 写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: | 13 | 写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: |
15 | 14 | ||
16 | - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) | 15 | - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) |
17 | - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) | 16 | - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) |
18 | - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) | 17 | - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) |
19 | - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) | 18 | - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) |
20 | - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) | 19 | - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) |
21 | 20 | ||
22 | ## Build Setup | 21 | ## Build Setup |
23 | 22 | ||
24 | ```bash | 23 | ```bash |
25 | 24 | ||
25 | |||
26 | # 进入项目目录 | 26 | # 进入项目目录 |
27 | cd gulu-admin | 27 | cd gulu-admin |
28 | 28 | ||
29 | # 安装依赖 | 29 | # 安装依赖 |
30 | npm install | 30 | npm install |
31 | 31 | ||
32 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 | 32 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 |
33 | npm install --registry=https://registry.npm.taobao.org | 33 | npm install --registry=https://registry.npm.taobao.org |
34 | 34 | ||
35 | # 启动服务 | 35 | # 启动服务 |
36 | npm run dev | 36 | npm run dev |
37 | ``` | 37 | ``` |
38 | 38 | ||
39 | 浏览器访问 [http://localhost:9528](http://localhost:9528) | 39 | 浏览器访问 [http://localhost:9528](http://localhost:9528) |
40 | 40 | ||
41 | ## 发布 | 41 | ## 发布 |
42 | 42 | ||
43 | ```bash | 43 | ```bash |
44 | # 构建测试环境 | 44 | # 构建测试环境 |
45 | npm run build:stage | 45 | npm run build:stage |
46 | 46 | ||
47 | # 构建生产环境 | 47 | # 构建生产环境 |
48 | npm run build:prod | 48 | npm run build:prod |
49 | ``` | 49 | ``` |
50 | 50 | ||
51 | ## 其它 | 51 | ## 其它 |
52 | 52 | ||
53 | ```bash | 53 | ```bash |
54 | # 预览发布环境效果 | 54 | # 预览发布环境效果 |
55 | npm run preview | 55 | npm run preview |
56 | 56 | ||
57 | # 预览发布环境效果 + 静态资源分析 | 57 | # 预览发布环境效果 + 静态资源分析 |
58 | npm run preview -- --report | 58 | npm run preview -- --report |
59 | 59 | ||
60 | # 代码格式检查 | 60 | # 代码格式检查 |
61 | npm run lint | 61 | npm run lint |
62 | 62 | ||
63 | # 代码格式检查并自动修复 | 63 | # 代码格式检查并自动修复 |
64 | npm run lint -- --fix | 64 | npm run lint -- --fix |
65 | ``` | 65 | ``` |
66 | 66 | ||
67 | 更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) | ||
68 | |||
69 | ## Demo | ||
70 | |||
71 | ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) | ||
72 | |||
73 |
README.md
1 | 1 | ||
2 | |||
3 | ## Build Setup | ||
4 | |||
5 | |||
2 | ```bash | 6 | ```bash |
3 | 7 | ||
4 | # enter the project directory | 8 | # enter the project directory |
5 | cd gulu-admin | 9 | cd gulu-admin |
6 | 10 | ||
7 | # install dependency | 11 | # install dependency |
8 | npm install | 12 | npm install |
9 | 13 | ||
10 | # develop | 14 | # develop |
11 | npm run dev | 15 | npm run dev |
12 | ``` | 16 | ``` |
13 | 17 | ||
14 | This will automatically open http://localhost:9528 | 18 | This will automatically open http://localhost:9528 |
15 | 19 | ||
16 | ## Build | 20 | ## Build |
17 | 21 | ||
18 | ```bash | 22 | ```bash |
19 | # build for test environment | 23 | # build for test environment |
20 | npm run build:stage | 24 | npm run build:stage |
21 | 25 | ||
22 | # build for production environment | 26 | # build for production environment |
23 | npm run build:prod | 27 | npm run build:prod |
24 | ``` | 28 | ``` |
25 | 29 | ||
26 | ## Advanced | 30 | ## Advanced |
27 | 31 | ||
28 | ```bash | 32 | ```bash |
29 | # preview the release environment effect | 33 | # preview the release environment effect |
30 | npm run preview | 34 | npm run preview |
31 | 35 | ||
32 | # preview the release environment effect + static resource analysis | 36 | # preview the release environment effect + static resource analysis |
33 | npm run preview -- --report | 37 | npm run preview -- --report |
34 | 38 | ||
35 | # code format check | 39 | # code format check |
36 | npm run lint | 40 | npm run lint |
37 | 41 | ||
38 | # code format check and auto fix | 42 | # code format check and auto fix |
39 | npm run lint -- --fix | 43 | npm run lint -- --fix |
40 | ``` | 44 | ``` |
41 | 45 | ||
42 | Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information | 46 | Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information |
43 | 47 | ||
44 | ## Demo | 48 | ## Demo |
45 | 49 | ||
46 | ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) | 50 | ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) |
47 | 51 | ||
48 | ## Extra | 52 | ## Extra |
49 | 53 | ||
50 | If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) | 54 | If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) |
51 | 55 | ||
52 | For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour)) | 56 | For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour)) |
53 | 57 | ||
54 | ## Related Project | 58 | ## Related Project |
55 | 59 | ||
56 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) | 60 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) |
57 | 61 | ||
58 | - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) | 62 | - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) |
59 | 63 | ||
60 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) | 64 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) |
61 | 65 | ||
62 | - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) | 66 | - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) |
63 | 67 | ||
64 | ## Browsers support | ||
65 | 68 |
src/layout/components/Sidebar/index.vue
1 | <template> | 1 | <template> |
2 | <div :class="{'has-logo':showLogo}"> | 2 | <div :class="{'has-logo':showLogo}"> |
3 | <logo v-if="showLogo" :collapse="isCollapse" /> | 3 | <logo v-if="showLogo" :collapse="isCollapse" /> |
4 | <el-scrollbar wrap-class="scrollbar-wrapper"> | 4 | <el-scrollbar wrap-class="scrollbar-wrapper"> |
5 | <el-menu | 5 | <el-menu |
6 | :default-active="activeMenu" | 6 | :default-active="activeMenu" |
7 | :collapse="isCollapse" | 7 | :collapse="isCollapse" |
8 | :background-color="variables.menuBg" | 8 | :background-color="variables.menuBg" |
9 | :text-color="variables.menuText" | 9 | :text-color="variables.menuText" |
10 | :unique-opened="false" | 10 | :unique-opened="false" |
11 | :active-text-color="variables.menuActiveText" | 11 | :active-text-color="variables.menuActiveText" |
12 | :collapse-transition="false" | 12 | :collapse-transition="false" |
13 | mode="vertical" | 13 | mode="vertical" |
14 | > | 14 | > |
15 | <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" /> | 15 | <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" /> |
16 | </el-menu> | 16 | </el-menu> |
17 | </el-scrollbar> | 17 | </el-scrollbar> |
18 | </div> | 18 | </div> |
19 | </template> | 19 | </template> |
20 | 20 | ||
21 | <script> | 21 | <script> |
22 | import { mapGetters } from 'vuex' | 22 | import { mapGetters } from 'vuex' |
23 | import Logo from './Logo' | 23 | import Logo from './Logo' |
24 | import SidebarItem from './SidebarItem' | 24 | import SidebarItem from './SidebarItem' |
25 | import variables from '@/styles/variables.scss' | 25 | import variables from '@/styles/variables.scss' |
26 | 26 | ||
27 | export default { | 27 | export default { |
28 | components: { SidebarItem, Logo }, | 28 | components: { SidebarItem, Logo }, |
29 | computed: { | 29 | computed: { |
30 | ...mapGetters([ | 30 | ...mapGetters([ |
31 | 'permission_routes', | ||
31 | 'sidebar' | 32 | 'sidebar' |
32 | ]), | 33 | ]), |
33 | routes() { | ||
34 | return this.$router.options.routes | ||
35 | }, | ||
36 | activeMenu() { | 34 | activeMenu() { |
37 | const route = this.$route | 35 | const route = this.$route |
38 | const { meta, path } = route | 36 | const { meta, path } = route |
39 | // if set path, the sidebar will highlight the path you set | 37 | // if set path, the sidebar will highlight the path you set |
40 | if (meta.activeMenu) { | 38 | if (meta.activeMenu) { |
41 | return meta.activeMenu | 39 | return meta.activeMenu |
42 | } | 40 | } |
43 | return path | 41 | return path |
44 | }, | 42 | }, |
45 | showLogo() { | 43 | showLogo() { |
46 | return this.$store.state.settings.sidebarLogo | 44 | return this.$store.state.settings.sidebarLogo |
47 | }, | 45 | }, |
48 | variables() { | 46 | variables() { |
49 | return variables | 47 | return variables |
50 | }, | 48 | }, |
51 | isCollapse() { | 49 | isCollapse() { |
52 | return !this.sidebar.opened | 50 | return !this.sidebar.opened |
53 | } | 51 | } |
54 | } | 52 | } |
55 | } | 53 | } |
56 | </script> | 54 | </script> |
src/permission.js
1 | import router from './router' | 1 | import router from './router' |
2 | import store from './store' | 2 | import store from './store' |
3 | import { Message } from 'element-ui' | 3 | import { Message } from 'element-ui' |
4 | import NProgress from 'nprogress' // progress bar | 4 | import NProgress from 'nprogress' // progress bar |
5 | import 'nprogress/nprogress.css' // progress bar style | 5 | import 'nprogress/nprogress.css' // progress bar style |
6 | import { getToken } from '@/utils/auth' // get token from cookie | 6 | import { getToken } from '@/utils/auth' // get token from cookie |
7 | import getPageTitle from '@/utils/get-page-title' | 7 | import getPageTitle from '@/utils/get-page-title' |
8 | 8 | ||
9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration |
10 | 10 | ||
11 | const whiteList = ['/login'] // no redirect whitelist | 11 | const whiteList = ['/login'] // no redirect whitelist |
12 | 12 | ||
13 | router.beforeEach(async(to, from, next) => { | 13 | router.beforeEach(async(to, from, next) => { |
14 | // start progress bar | 14 | // start progress bar |
15 | NProgress.start() | 15 | NProgress.start() |
16 | 16 | ||
17 | // set page title | 17 | // set page title |
18 | document.title = getPageTitle(to.meta.title) | 18 | document.title = getPageTitle(to.meta.title) |
19 | 19 | ||
20 | // determine whether the user has logged in | 20 | // determine whether the user has logged in |
21 | const hasToken = getToken() | 21 | const hasToken = getToken() |
22 | 22 | ||
23 | if (hasToken) { | 23 | if (hasToken) { |
24 | if (to.path === '/login') { | 24 | if (to.path === '/login') { |
25 | // if is logged in, redirect to the home page | 25 | // if is logged in, redirect to the home page |
26 | next({ path: '/' }) | 26 | next({ path: '/' }) |
27 | NProgress.done() | 27 | NProgress.done() |
28 | } else { | 28 | } else { |
29 | const hasGetUserInfo = store.getters.name | 29 | // determine whether the user has obtained his permission roles through getInfo |
30 | if (hasGetUserInfo) { | 30 | const hasRoles = store.getters.roles && store.getters.roles.length > 0 |
31 | if (hasRoles) { | ||
31 | next() | 32 | next() |
32 | } else { | 33 | } else { |
33 | try { | 34 | try { |
34 | // get user info | 35 | // get user info |
35 | await store.dispatch('user/getInfo') | 36 | // note: roles must be a object array! such as: ['admin'] or ,['developer','editor'] |
37 | const { roles } = await store.dispatch('user/getInfo') | ||
36 | 38 | ||
37 | next() | 39 | // generate accessible routes map based on roles |
40 | const accessRoutes = await store.dispatch('permission/generateRoutes', roles) | ||
41 | |||
42 | // dynamically add accessible routes | ||
43 | router.addRoutes(accessRoutes) | ||
44 | |||
45 | // hack method to ensure that addRoutes is complete | ||
46 | // set the replace: true, so the navigation will not leave a history record | ||
47 | next({ ...to, replace: true }) | ||
38 | } catch (error) { | 48 | } catch (error) { |
39 | // remove token and go to login page to re-login | 49 | // remove token and go to login page to re-login |
40 | await store.dispatch('user/resetToken') | 50 | await store.dispatch('user/resetToken') |
41 | Message.error(error || 'Has Error') | 51 | Message.error(error || 'Has Error') |
42 | next(`/login?redirect=${to.path}`) | 52 | next(`/login?redirect=${to.path}`) |
43 | NProgress.done() | 53 | NProgress.done() |
44 | } | 54 | } |
45 | } | 55 | } |
46 | } | 56 | } |
47 | } else { | 57 | } else { |
48 | /* has no token*/ | 58 | /* has no token*/ |
49 | 59 | ||
50 | if (whiteList.indexOf(to.path) !== -1) { | 60 | if (whiteList.indexOf(to.path) !== -1) { |
51 | // in the free login whitelist, go directly | 61 | // in the free login whitelist, go directly |
52 | next() | 62 | next() |
53 | } else { | 63 | } else { |
54 | // other pages that do not have permission to access are redirected to the login page. | 64 | // other pages that do not have permission to access are redirected to the login page. |
55 | next(`/login?redirect=${to.path}`) | 65 | next(`/login?redirect=${to.path}`) |
56 | NProgress.done() | 66 | NProgress.done() |
57 | } | 67 | } |
58 | } | 68 | } |
59 | }) | 69 | }) |
60 | 70 | ||
61 | router.afterEach(() => { | 71 | router.afterEach(() => { |
62 | // finish progress bar | 72 | // finish progress bar |
63 | NProgress.done() | 73 | NProgress.done() |
64 | }) | 74 | }) |
65 | 75 |
src/router/index.js
1 | import Vue from 'vue' | 1 | import Vue from 'vue' |
2 | import Router from 'vue-router' | 2 | import Router from 'vue-router' |
3 | 3 | ||
4 | Vue.use(Router) | 4 | Vue.use(Router) |
5 | 5 | ||
6 | /* Layout */ | 6 | /* Layout */ |
7 | import Layout from '@/layout' | 7 | import Layout from '@/layout' |
8 | 8 | ||
9 | /** | 9 | /** |
10 | * Note: sub-menu only appear when route children.length >= 1 | 10 | * Note: sub-menu only appear when route children.length >= 1 |
11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html | 11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html |
12 | * | 12 | * |
13 | * hidden: true if set true, item will not show in the sidebar(default is false) | 13 | * hidden: true if set true, item will not show in the sidebar(default is false) |
14 | * alwaysShow: true if set true, will always show the root menu | 14 | * alwaysShow: true if set true, will always show the root menu |
15 | * if not set alwaysShow, when item has more than one children route, | 15 | * if not set alwaysShow, when item has more than one children route, |
16 | * it will becomes nested mode, otherwise not show the root menu | 16 | * it will becomes nested mode, otherwise not show the root menu |
17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb | 17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb |
18 | * name:'router-name' the name is used by <keep-alive> (must set!!!) | 18 | * name:'router-name' the name is used by <keep-alive> (must set!!!) |
19 | * meta : { | 19 | * meta : { |
20 | roles: ['admin','editor'] control the page roles (you can set multiple roles) | 20 | roles: ['admin','editor'] control the page roles (you can set multiple roles) |
21 | title: 'title' the name show in sidebar and breadcrumb (recommend set) | 21 | title: 'title' the name show in sidebar and breadcrumb (recommend set) |
22 | icon: 'svg-name' the icon show in the sidebar | 22 | icon: 'svg-name' the icon show in the sidebar |
23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) | 23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) |
24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set | 24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set |
25 | } | 25 | } |
26 | */ | 26 | */ |
27 | 27 | ||
28 | /** | 28 | /** |
29 | * constantRoutes | 29 | * constantRoutes |
30 | * a base page that does not have permission requirements | 30 | * a base page that does not have permission requirements |
31 | * all roles can be accessed | 31 | * all roles can be accessed |
32 | */ | 32 | */ |
33 | export const constantRoutes = [ | 33 | export const constantRoutes = [ |
34 | { | 34 | { |
35 | path: '/login', | 35 | path: '/login', |
36 | component: () => import('@/views/login/index'), | 36 | component: () => import('@/views/login/index'), |
37 | hidden: true | 37 | hidden: true |
38 | }, | 38 | }, |
39 | 39 | ||
40 | { | 40 | { |
41 | path: '/404', | 41 | path: '/404', |
42 | component: () => import('@/views/404'), | 42 | component: () => import('@/views/404'), |
43 | hidden: true | 43 | hidden: true |
44 | }, | 44 | }, |
45 | 45 | ||
46 | { | 46 | { |
47 | path: '/', | 47 | path: '/', |
48 | component: Layout, | 48 | component: Layout, |
49 | redirect: '/dashboard', | 49 | redirect: '/dashboard', |
50 | children: [{ | 50 | children: [{ |
51 | path: 'dashboard', | 51 | path: 'dashboard', |
52 | name: 'Dashboard', | 52 | name: 'Dashboard', |
53 | component: () => import('@/views/dashboard/index'), | 53 | component: () => import('@/views/dashboard/index'), |
54 | meta: { title: 'Dashboard', icon: 'dashboard' } | 54 | meta: { title: 'Dashboard', icon: 'dashboard' } |
55 | }] | 55 | }] |
56 | }, | 56 | }, |
57 | 57 | ||
58 | { | 58 | { |
59 | path: '/example', | 59 | path: '/example', |
60 | component: Layout, | 60 | component: Layout, |
61 | redirect: '/example/table', | 61 | redirect: '/example/table', |
62 | name: 'Example', | 62 | name: 'Example', |
63 | meta: { title: 'Example', icon: 'example' }, | 63 | meta: { title: 'Example', icon: 'example' }, |
64 | children: [ | 64 | children: [ |
65 | { | 65 | { |
66 | path: 'table', | 66 | path: 'table', |
67 | name: 'Table', | 67 | name: 'Table', |
68 | component: () => import('@/views/table/index'), | 68 | component: () => import('@/views/table/index'), |
69 | meta: { title: 'Table', icon: 'table' } | 69 | meta: { title: 'Table', icon: 'table' } |
70 | }, | 70 | }, |
71 | { | 71 | { |
72 | path: 'tree', | 72 | path: 'tree', |
73 | name: 'Tree', | 73 | name: 'Tree', |
74 | component: () => import('@/views/tree/index'), | 74 | component: () => import('@/views/tree/index'), |
75 | meta: { title: 'Tree', icon: 'tree' } | 75 | meta: { title: 'Tree', icon: 'tree' } |
76 | } | 76 | } |
77 | ] | 77 | ] |
78 | }, | 78 | }, |
79 | 79 | ||
80 | { | 80 | { |
81 | path: '/form', | 81 | path: '/form', |
82 | component: Layout, | 82 | component: Layout, |
83 | children: [ | 83 | children: [ |
84 | { | 84 | { |
85 | path: 'index', | 85 | path: 'index', |
86 | name: 'Form', | 86 | name: 'Form', |
87 | component: () => import('@/views/form/index'), | 87 | component: () => import('@/views/form/index'), |
88 | meta: { title: 'Form', icon: 'form' } | 88 | meta: { title: 'Form', icon: 'form' } |
89 | } | 89 | } |
90 | ] | 90 | ] |
91 | }, | 91 | } |
92 | ] | ||
92 | 93 | ||
94 | /** | ||
95 | * asyncRoutes | ||
96 | * the routes that need to be dynamically loaded based on user roles | ||
97 | */ | ||
98 | export const asyncRoutes = [ | ||
93 | { | 99 | { |
94 | path: '/nested', | 100 | path: '/nested', |
95 | component: Layout, | 101 | component: Layout, |
96 | redirect: '/nested/menu1', | 102 | redirect: '/nested/menu1', |
97 | name: 'Nested', | 103 | name: 'Nested', |
98 | meta: { | 104 | meta: { |
99 | title: 'Nested', | 105 | title: 'Nested', |
100 | icon: 'nested' | 106 | icon: 'nested' |
101 | }, | 107 | }, |
102 | children: [ | 108 | children: [ |
103 | { | 109 | { |
104 | path: 'menu1', | 110 | path: 'menu1', |
105 | component: () => import('@/views/nested/menu1/index'), // Parent router-view | 111 | component: () => import('@/views/nested/menu1/index'), // Parent router-view |
106 | name: 'Menu1', | 112 | name: 'Menu1', |
107 | meta: { title: 'Menu1' }, | 113 | meta: { title: 'Menu1' }, |
108 | children: [ | 114 | children: [ |
109 | { | 115 | { |
110 | path: 'menu1-1', | 116 | path: 'menu1-1', |
111 | component: () => import('@/views/nested/menu1/menu1-1'), | 117 | component: () => import('@/views/nested/menu1/menu1-1'), |
112 | name: 'Menu1-1', | 118 | name: 'Menu1-1', |
113 | meta: { title: 'Menu1-1' } | 119 | meta: { title: 'Menu1-1' } |
114 | }, | 120 | }, |
115 | { | 121 | { |
116 | path: 'menu1-2', | 122 | path: 'menu1-2', |
117 | component: () => import('@/views/nested/menu1/menu1-2'), | 123 | component: () => import('@/views/nested/menu1/menu1-2'), |
118 | name: 'Menu1-2', | 124 | name: 'Menu1-2', |
119 | meta: { title: 'Menu1-2' }, | 125 | meta: { title: 'Menu1-2' }, |
120 | children: [ | 126 | children: [ |
121 | { | 127 | { |
122 | path: 'menu1-2-1', | 128 | path: 'menu1-2-1', |
123 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'), | 129 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'), |
124 | name: 'Menu1-2-1', | 130 | name: 'Menu1-2-1', |
125 | meta: { title: 'Menu1-2-1' } | 131 | meta: { title: 'Menu1-2-1' } |
126 | }, | 132 | }, |
127 | { | 133 | { |
128 | path: 'menu1-2-2', | 134 | path: 'menu1-2-2', |
129 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'), | 135 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'), |
130 | name: 'Menu1-2-2', | 136 | name: 'Menu1-2-2', |
131 | meta: { title: 'Menu1-2-2' } | 137 | meta: { title: 'Menu1-2-2' } |
132 | } | 138 | } |
133 | ] | 139 | ] |
134 | }, | 140 | }, |
135 | { | 141 | { |
136 | path: 'menu1-3', | 142 | path: 'menu1-3', |
137 | component: () => import('@/views/nested/menu1/menu1-3'), | 143 | component: () => import('@/views/nested/menu1/menu1-3'), |
138 | name: 'Menu1-3', | 144 | name: 'Menu1-3', |
139 | meta: { title: 'Menu1-3' } | 145 | meta: { title: 'Menu1-3' } |
140 | } | 146 | } |
141 | ] | 147 | ] |
142 | }, | 148 | }, |
143 | { | 149 | { |
144 | path: 'menu2', | 150 | path: 'menu2', |
145 | component: () => import('@/views/nested/menu2/index'), | 151 | component: () => import('@/views/nested/menu2/index'), |
146 | meta: { title: 'menu2' } | 152 | meta: { title: 'menu2' } |
147 | } | 153 | } |
148 | ] | 154 | ] |
149 | }, | 155 | }, |
150 | 156 | ||
151 | { | 157 | { |
152 | path: 'external-link', | 158 | path: 'external-link', |
153 | component: Layout, | 159 | component: Layout, |
154 | children: [ | 160 | children: [ |
155 | { | 161 | { |
156 | path: 'https://panjiachen.github.io/vue-element-admin-site/#/', | 162 | path: 'https://panjiachen.github.io/vue-element-admin-site/#/', |
157 | meta: { title: 'External Link', icon: 'link' } | 163 | meta: { title: 'External Link', icon: 'link' } |
158 | } | 164 | } |
159 | ] | 165 | ] |
160 | }, | 166 | }, |
161 | 167 | ||
162 | // 404 page must be placed at the end !!! | 168 | // 404 page must be placed at the end !!! |
163 | { path: '*', redirect: '/404', hidden: true } | 169 | { path: '*', redirect: '/404', hidden: true } |
164 | ] | 170 | ] |
165 | 171 | ||
166 | const createRouter = () => new Router({ | 172 | const createRouter = () => new Router({ |
167 | // mode: 'history', // require service support | 173 | // mode: 'history', // require service support |
168 | scrollBehavior: () => ({ y: 0 }), | 174 | scrollBehavior: () => ({ y: 0 }), |
169 | routes: constantRoutes | 175 | routes: constantRoutes |
170 | }) | 176 | }) |
171 | 177 | ||
172 | const router = createRouter() | 178 | const router = createRouter() |
173 | 179 | ||
174 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 | 180 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 |
175 | export function resetRouter() { | 181 | export function resetRouter() { |
176 | const newRouter = createRouter() | 182 | const newRouter = createRouter() |
177 | router.matcher = newRouter.matcher // reset router | 183 | router.matcher = newRouter.matcher // reset router |
178 | } | 184 | } |
179 | 185 | ||
180 | export default router | 186 | export default router |
181 | 187 |
src/store/getters.js
1 | const getters = { | 1 | const getters = { |
2 | sidebar: state => state.app.sidebar, | 2 | sidebar: state => state.app.sidebar, |
3 | device: state => state.app.device, | 3 | device: state => state.app.device, |
4 | token: state => state.user.token, | 4 | token: state => state.user.token, |
5 | avatar: state => state.user.avatar, | 5 | avatar: state => state.user.avatar, |
6 | name: state => state.user.name | 6 | name: state => state.user.name, |
7 | roles: state => state.user.roles, | ||
8 | permission_routes: state => state.permission.routes | ||
7 | } | 9 | } |
8 | export default getters | 10 | export default getters |
9 | 11 |
src/store/index.js
1 | import Vue from 'vue' | 1 | import Vue from 'vue' |
2 | import Vuex from 'vuex' | 2 | import Vuex from 'vuex' |
3 | import getters from './getters' | 3 | import getters from './getters' |
4 | import app from './modules/app' | 4 | import app from './modules/app' |
5 | import permission from './modules/permission' | ||
5 | import settings from './modules/settings' | 6 | import settings from './modules/settings' |
6 | import user from './modules/user' | 7 | import user from './modules/user' |
7 | 8 | ||
8 | Vue.use(Vuex) | 9 | Vue.use(Vuex) |
9 | 10 | ||
10 | const store = new Vuex.Store({ | 11 | const store = new Vuex.Store({ |
11 | modules: { | 12 | modules: { |
12 | app, | 13 | app, |
14 | permission, | ||
13 | settings, | 15 | settings, |
14 | user | 16 | user |
15 | }, | 17 | }, |
16 | getters | 18 | getters |
17 | }) | 19 | }) |
18 | 20 | ||
19 | export default store | 21 | export default store |
20 | 22 |
src/store/modules/permission.js
File was created | 1 | import { asyncRoutes, constantRoutes } from '@/router' | |
2 | |||
3 | /** | ||
4 | * Use meta.role to determine if the current user has permission | ||
5 | * @param roles | ||
6 | * @param route | ||
7 | */ | ||
8 | function hasPermission(roles, route) { | ||
9 | if (route.meta && route.meta.roles) { | ||
10 | return roles.some(role => route.meta.roles.includes(role)) | ||
11 | } else { | ||
12 | return true | ||
13 | } | ||
14 | } | ||
15 | |||
16 | /** | ||
17 | * Filter asynchronous routing tables by recursion | ||
18 | * @param routes asyncRoutes | ||
19 | * @param roles | ||
20 | */ | ||
21 | export function filterAsyncRoutes(routes, roles) { | ||
22 | const res = [] | ||
23 | |||
24 | routes.forEach(route => { | ||
25 | const tmp = { ...route } | ||
26 | if (hasPermission(roles, tmp)) { | ||
27 | if (tmp.children) { | ||
28 | tmp.children = filterAsyncRoutes(tmp.children, roles) | ||
29 | } | ||
30 | res.push(tmp) | ||
31 | } | ||
32 | }) | ||
33 | |||
34 | return res | ||
35 | } | ||
36 | |||
37 | const state = { | ||
38 | routes: [], | ||
39 | addRoutes: [] | ||
40 | } | ||
41 | |||
42 | const mutations = { | ||
43 | SET_ROUTES: (state, routes) => { | ||
44 | state.addRoutes = routes | ||
45 | state.routes = constantRoutes.concat(routes) | ||
46 | } | ||
47 | } | ||
48 | |||
49 | const actions = { | ||
50 | generateRoutes({ commit }, roles) { | ||
51 | return new Promise(resolve => { | ||
52 | let accessedRoutes | ||
53 | if (roles.includes('admin')) { | ||
54 | accessedRoutes = asyncRoutes || [] | ||
55 | } else { | ||
56 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) | ||
57 | } | ||
58 | commit('SET_ROUTES', accessedRoutes) | ||
59 | resolve(accessedRoutes) | ||
60 | }) | ||
61 | } | ||
62 | } | ||
63 | |||
64 | export default { | ||
65 | namespaced: true, | ||
66 | state, | ||
67 | mutations, | ||
68 | actions | ||
69 | } | ||
70 |
src/store/modules/user.js
1 | import { login, logout, getInfo } from '@/api/user' | 1 | import { login, logout, getInfo } from '@/api/user' |
2 | import { getToken, setToken, removeToken } from '@/utils/auth' | 2 | import { getToken, setToken, removeToken } from '@/utils/auth' |
3 | import { resetRouter } from '@/router' | 3 | import { resetRouter } from '@/router' |
4 | 4 | ||
5 | const getDefaultState = () => { | 5 | const getDefaultState = () => { |
6 | return { | 6 | return { |
7 | token: getToken(), | 7 | token: getToken(), |
8 | name: '', | 8 | name: '', |
9 | avatar: '' | 9 | avatar: '', |
10 | roles: [] | ||
10 | } | 11 | } |
11 | } | 12 | } |
12 | 13 | ||
13 | const state = getDefaultState() | 14 | const state = getDefaultState() |
14 | 15 | ||
15 | const mutations = { | 16 | const mutations = { |
16 | RESET_STATE: (state) => { | 17 | RESET_STATE: (state) => { |
17 | Object.assign(state, getDefaultState()) | 18 | Object.assign(state, getDefaultState()) |
18 | }, | 19 | }, |
19 | SET_TOKEN: (state, token) => { | 20 | SET_TOKEN: (state, token) => { |
20 | state.token = token | 21 | state.token = token |
21 | }, | 22 | }, |
22 | SET_NAME: (state, name) => { | 23 | SET_NAME: (state, name) => { |
23 | state.name = name | 24 | state.name = name |
24 | }, | 25 | }, |
25 | SET_AVATAR: (state, avatar) => { | 26 | SET_AVATAR: (state, avatar) => { |
26 | state.avatar = avatar | 27 | state.avatar = avatar |
28 | }, | ||
29 | SET_ROLES: (state, roles) => { | ||
30 | state.roles = roles | ||
27 | } | 31 | } |
28 | } | 32 | } |
29 | 33 | ||
30 | const actions = { | 34 | const actions = { |
31 | // user login | 35 | // user login |
32 | login({ commit }, userInfo) { | 36 | login({ commit }, userInfo) { |
33 | const { username, password } = userInfo | 37 | const { username, password } = userInfo |
34 | return new Promise((resolve, reject) => { | 38 | return new Promise((resolve, reject) => { |
35 | login({ username: username.trim(), password: password }).then(response => { | 39 | login({ username: username.trim(), password: password }).then(response => { |
36 | const { data } = response | 40 | const { data } = response |
37 | commit('SET_TOKEN', data.token) | 41 | commit('SET_TOKEN', data.token) |
38 | setToken(data.token) | 42 | setToken(data.token) |
39 | resolve() | 43 | resolve() |
40 | }).catch(error => { | 44 | }).catch(error => { |
41 | reject(error) | 45 | reject(error) |
42 | }) | 46 | }) |
43 | }) | 47 | }) |
44 | }, | 48 | }, |
45 | 49 | ||
46 | // get user info | 50 | // get user info |
47 | getInfo({ commit, state }) { | 51 | getInfo({ commit, state }) { |
48 | return new Promise((resolve, reject) => { | 52 | return new Promise((resolve, reject) => { |
49 | getInfo(state.token).then(response => { | 53 | getInfo(state.token).then(response => { |
50 | const { data } = response | 54 | const { data } = response |
51 | 55 | ||
52 | if (!data) { | 56 | if (!data) { |
53 | reject('Verification failed, please Login again.') | 57 | reject('Verification failed, please Login again.') |
54 | } | 58 | } |
55 | 59 | ||
56 | const { name, avatar } = data | 60 | const { roles, name, avatar } = data |
61 | |||
62 | // roles must be a non-empty array | ||
63 | if (!roles || roles.length <= 0) { | ||
64 | reject('getInfo: roles must be a non-null array!') | ||
65 | } | ||
57 | 66 | ||
67 | commit('SET_ROLES', roles) | ||
58 | commit('SET_NAME', name) | 68 | commit('SET_NAME', name) |
59 | commit('SET_AVATAR', avatar) | 69 | commit('SET_AVATAR', avatar) |
60 | resolve(data) | 70 | resolve(data) |
61 | }).catch(error => { | 71 | }).catch(error => { |
62 | reject(error) | 72 | reject(error) |
63 | }) | 73 | }) |
64 | }) | 74 | }) |
65 | }, | 75 | }, |
66 | 76 | ||
67 | // user logout | 77 | // user logout |
68 | logout({ commit, state }) { | 78 | logout({ commit, state }) { |
69 | return new Promise((resolve, reject) => { | 79 | return new Promise((resolve, reject) => { |
70 | logout(state.token).then(() => { | 80 | logout(state.token).then(() => { |
71 | removeToken() // must remove token first | 81 | removeToken() // must remove token first |
72 | resetRouter() | 82 | resetRouter() |
73 | commit('RESET_STATE') | 83 | commit('RESET_STATE') |
74 | resolve() | 84 | resolve() |
75 | }).catch(error => { | 85 | }).catch(error => { |
76 | reject(error) | 86 | reject(error) |
77 | }) | 87 | }) |
78 | }) | 88 | }) |
79 | }, | 89 | }, |
80 | 90 | ||
81 | // remove token | 91 | // remove token |
82 | resetToken({ commit }) { | 92 | resetToken({ commit }) { |
83 | return new Promise(resolve => { | 93 | return new Promise(resolve => { |
84 | removeToken() // must remove token first | 94 | removeToken() // must remove token first |
85 | commit('RESET_STATE') | 95 | commit('RESET_STATE') |
86 | resolve() | 96 | resolve() |
87 | }) | 97 | }) |
88 | } | 98 | } |
89 | } | 99 | } |
90 | 100 | ||
91 | export default { | 101 | export default { |
92 | namespaced: true, | 102 | namespaced: true, |
93 | state, | 103 | state, |
94 | mutations, | 104 | mutations, |
95 | actions | 105 | actions |
96 | } | 106 | } |
97 | 107 | ||
98 | 108 |
src/views/dashboard/index.vue
1 | <template> | 1 | <template> |
2 | <div class="dashboard-container"> | 2 | <div class="dashboard-container"> |
3 | <div class="dashboard-text">name: {{ name }}</div> | 3 | <div class="dashboard-text">name: {{ name }}</div> |
4 | <div class="dashboard-text">roles: <span v-for="role in roles" :key="role">{{ role }}</span></div> | ||
4 | </div> | 5 | </div> |
5 | </template> | 6 | </template> |
6 | 7 | ||
7 | <script> | 8 | <script> |
8 | import { mapGetters } from 'vuex' | 9 | import { mapGetters } from 'vuex' |
9 | 10 | ||
10 | export default { | 11 | export default { |
11 | name: 'Dashboard', | 12 | name: 'Dashboard', |
12 | computed: { | 13 | computed: { |
13 | ...mapGetters([ | 14 | ...mapGetters([ |
14 | 'name' | 15 | 'name', |
16 | 'roles' | ||
15 | ]) | 17 | ]) |
16 | } | 18 | } |
17 | } | 19 | } |
18 | </script> | 20 | </script> |
19 | 21 | ||
20 | <style lang="scss" scoped> | 22 | <style lang="scss" scoped> |
21 | .dashboard { | 23 | .dashboard { |
22 | &-container { | 24 | &-container { |
23 | margin: 30px; | 25 | margin: 30px; |
24 | } | 26 | } |
25 | &-text { | 27 | &-text { |
26 | font-size: 30px; | 28 | font-size: 30px; |
27 | line-height: 46px; | 29 | line-height: 46px; |
28 | } | 30 | } |
29 | } | 31 | } |
30 | </style> | 32 | </style> |
31 | 33 |