├── .browserslistrc ├── public ├── favicon.ico └── index.html ├── src ├── assets │ ├── logo.png │ ├── index.css │ └── index.less ├── api │ ├── user.js │ ├── http.js │ ├── index.js │ └── page.js ├── App.vue ├── components │ ├── BreadCrumb.vue │ ├── Main.vue │ ├── main │ │ ├── TopBar.vue │ │ └── Card.vue │ ├── Aside.vue │ └── Header.vue ├── main.js ├── plugins │ └── element.js ├── store │ └── index.js ├── views │ ├── NotFound.vue │ ├── Home.vue │ ├── Login.vue │ └── Page1.vue ├── mock │ ├── my-radom.js │ └── index.js └── router │ └── index.js ├── .editorconfig ├── babel.config.js ├── .gitignore ├── .eslintrc.js ├── package.json └── README.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aizener/admin-permission/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aizener/admin-permission/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | * { 6 | box-sizing: border-box; 7 | } 8 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import http from './http' 2 | 3 | export const login = data => { 4 | return http('/login', 'post', data) 5 | } -------------------------------------------------------------------------------- /src/assets/index.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | * { 7 | box-sizing: border-box; 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: [ 6 | [ 7 | 'component', 8 | { 9 | libraryName: 'element-ui', 10 | styleLibraryName: 'theme-chalk' 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/api/http.js: -------------------------------------------------------------------------------- 1 | import Axios from 'axios' 2 | 3 | const axios = Axios.create({ 4 | // baseURL: process.env.NODE_ENV === 'development' ? '' : '', 5 | }) 6 | 7 | export default (url, method = 'get', data = {}) => { 8 | return axios({ 9 | url, 10 | method, 11 | data 12 | }) 13 | } -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | const context = require.context('./', false, /.js$/) 2 | 3 | const modules = {} 4 | context.keys().forEach(fileName => { 5 | if (!['./index.js', './http.js'].includes(fileName)) { 6 | Object.assign(modules, context(fileName)) 7 | } 8 | }) 9 | 10 | export default modules -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'eol-last': 'off' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/BreadCrumb.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import './plugins/element.js' 6 | import './assets/index.less' 7 | import Api from './api' 8 | import './mock' 9 | import { MessageBox, Message } from 'element-ui' 10 | import 'animate.css' 11 | 12 | Vue.config.productionTip = false 13 | Vue.prototype.$api = Api 14 | Vue.prototype.$confirm = MessageBox.confirm 15 | Vue.prototype.$message = Message 16 | 17 | new Vue({ 18 | router, 19 | store, 20 | render: h => h(App) 21 | }).$mount('#app') 22 | -------------------------------------------------------------------------------- /src/api/page.js: -------------------------------------------------------------------------------- 1 | import http from './http' 2 | 3 | export const getList = data => { 4 | return http('/list', 'get', data) 5 | } 6 | 7 | export const getTotal = () => { 8 | return http('/list/total') 9 | } 10 | 11 | export const getListByValue = data => { 12 | return http('/list/value', 'get', data) 13 | } 14 | 15 | export const addList = data => { 16 | return http('/list/add', 'post', data) 17 | } 18 | 19 | export const updateList = data => { 20 | return http('/list/update', 'put', data) 21 | } 22 | 23 | export const deleteList = data => { 24 | return http('/list/delete', 'delete', data) 25 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/plugins/element.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { 3 | Container, 4 | Header, 5 | Aside, 6 | Main, 7 | Button, 8 | Menu, 9 | MenuItem, 10 | Submenu, 11 | Breadcrumb, 12 | BreadcrumbItem, 13 | Form, 14 | FormItem, 15 | Table, 16 | TableColumn, 17 | Input, 18 | Icon, 19 | Pagination, 20 | Loading 21 | } from 'element-ui' 22 | 23 | Vue 24 | .use(Container) 25 | .use(Header) 26 | .use(Aside) 27 | .use(Main) 28 | .use(Button) 29 | .use(Menu) 30 | .use(MenuItem) 31 | .use(Submenu) 32 | .use(Breadcrumb) 33 | .use(BreadcrumbItem) 34 | .use(Form) 35 | .use(FormItem) 36 | .use(Table) 37 | .use(TableColumn) 38 | .use(Input) 39 | .use(Icon) 40 | .use(Pagination) 41 | .use(Loading) -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | breads: [], 9 | user: {} 10 | }, 11 | mutations: { 12 | addBread (state, bread) { 13 | const index = state.breads.findIndex(_bread => _bread.name === bread.name) 14 | if (index > -1) { 15 | state.breads.splice(index + 1, state.breads.length - index - 1) 16 | } else { 17 | state.breads.push(bread) 18 | } 19 | }, 20 | removeBread (state, bread) { 21 | state.breads = state.breads.filter(_bread => _bread !== bread) 22 | }, 23 | setUser (state, user) { 24 | state.user = user 25 | } 26 | }, 27 | actions: { 28 | }, 29 | modules: { 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/Main.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 36 | -------------------------------------------------------------------------------- /src/components/main/TopBar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | -------------------------------------------------------------------------------- /src/mock/my-radom.js: -------------------------------------------------------------------------------- 1 | // 使用 Mock 2 | var Mock = require('mockjs') 3 | 4 | Mock.Random.extend({ 5 | likes: function () { 6 | const likes = [ 7 | '喜欢打游戏,看电影,尤其是英雄联盟和欧美大片。', 8 | '喜欢做饭,尤其是西餐,喜欢做甜点,自己每次都吃得饱饱的。', 9 | '我最爱去游泳了,当然也喜欢潜水,在海底下看各种好看的鱼鱼。', 10 | '我最最喜欢的就是去旅游了,看沿途的风景,真是美呆了。', 11 | '我的爱好是打篮球,我很喜欢打篮球,我的偶像是科比。', 12 | '我超喜欢去蹦迪了,感觉整个身体都在那里放松了。', 13 | '哈哈哈,我喜欢的是和女孩子一起玩,因为男女搭配,干活不累嘛。', 14 | '我没啥爱好,唯一的爱好就是宅。', 15 | '我喜欢看动漫,更喜欢日漫,我可是一个二次元哦。', 16 | '我喜欢cosplay,喜欢cos动漫里的每一个角色。' 17 | ] 18 | return this.pick(likes) 19 | } 20 | }) 21 | Mock.Random.extend({ 22 | address: function () { 23 | const address = [ 24 | '深圳市南山区科技园南区R2-B三楼', 25 | '深圳南山区科技园汇景豪苑海欣阁', 26 | '深圳市南山区白石洲中信红树湾', 27 | '上海市普陀区金沙江路 1517 弄', 28 | '四川成都市中德英伦联邦C区', 29 | '北京市中南海老四合院靠左', 30 | '广州市中心中央银行33号' 31 | ] 32 | return this.pick(address) 33 | } 34 | }) -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 33 | 34 | 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "permission-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "animate.css": "^4.1.0", 12 | "axios": "^0.19.2", 13 | "core-js": "^3.6.5", 14 | "element-ui": "^2.4.5", 15 | "vue": "^2.6.11", 16 | "vue-router": "^3.2.0", 17 | "vuex": "^3.4.0" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "~4.4.0", 21 | "@vue/cli-plugin-eslint": "~4.4.0", 22 | "@vue/cli-plugin-router": "~4.4.0", 23 | "@vue/cli-plugin-vuex": "~4.4.0", 24 | "@vue/cli-service": "~4.4.0", 25 | "@vue/eslint-config-standard": "^5.1.2", 26 | "babel-eslint": "^10.1.0", 27 | "babel-plugin-component": "^1.1.1", 28 | "eslint": "^6.7.2", 29 | "eslint-plugin-import": "^2.20.2", 30 | "eslint-plugin-node": "^11.1.0", 31 | "eslint-plugin-promise": "^4.2.1", 32 | "eslint-plugin-standard": "^4.0.0", 33 | "eslint-plugin-vue": "^6.2.2", 34 | "less": "^3.0.4", 35 | "less-loader": "^5.0.0", 36 | "mockjs": "^1.1.0", 37 | "vue-cli-plugin-element": "~1.0.1", 38 | "vue-template-compiler": "^2.6.11" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | import Login from '../views/Login.vue' 5 | import NotFound from '../views/NotFound.vue' 6 | 7 | Vue.use(VueRouter) 8 | const routes = [ 9 | { 10 | path: '/', 11 | name: 'Home', 12 | component: Home, 13 | redirect: '/menu/one', 14 | children: [ 15 | { 16 | path: '/menu/one', 17 | component: () => import('@/views/Page1.vue') 18 | }, { 19 | path: '/menu/two', 20 | component: () => import('@/views/Page1.vue') 21 | }, { 22 | path: '/menu/three', 23 | component: () => import('@/views/Page1.vue') 24 | }, { 25 | path: '/menu/four', 26 | component: () => import('@/views/Page1.vue') 27 | }, { 28 | path: '/menu/five', 29 | component: () => import('@/views/Page1.vue') 30 | } 31 | ] 32 | }, 33 | { 34 | path: '/login', 35 | name: 'Login', 36 | component: Login 37 | }, 38 | { 39 | path: '*', 40 | name: 'NotFound', 41 | component: NotFound 42 | } 43 | ] 44 | 45 | const router = new VueRouter({ 46 | routes 47 | }) 48 | 49 | const originalPush = VueRouter.prototype.push 50 | // 重写了原型上的push方法,统一的处理了错误信息 51 | VueRouter.prototype.push = function push (location) { 52 | return originalPush.call(this, location).catch(err => err) 53 | } 54 | 55 | export default router 56 | -------------------------------------------------------------------------------- /src/components/Aside.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 33 | 34 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 介绍 2 | 3 | > 这是一个用来练习Vue后台权限管理的简单模板,是我看了一个[B站视频](https://www.bilibili.com/video/BV15Q4y1K79c?t=5306)时,苦于没资料,于是自己花了点时间搭了一个,这里做来分享,分为两版: 4 | 5 | * 基础模板:有基本操作,mock等功能了,没实现权限判定; 6 | * 实现模板:基于基础模板做了权限的判定。 7 | 8 | ### 安装&运行 9 | 10 | 1. 克隆项目 11 | 12 | `git clone git@github.com:Aizener/admin-permission.git` 13 | 14 | 这个直接把基础模板和实现的模板都克隆下来的,develop的是基础模板,master的实现模板。 15 | 16 | 2. 安装依赖: 17 | 18 | 执行:`yarn`或者`yarn install`; 19 | 20 | 3. 基础模板: 21 | 22 | 基础模板在`develop`分支,可以通过`git checkout develop`来切换; 23 | 24 | 4. 实现模板: 25 | 26 | 基础模板在`master`分支, 可以通过`git checkout master`来切换; 27 | 28 | 5. 启动服务 29 | 30 | `yarn serve` 31 | 32 | 6. 效果图: 33 | 34 | 35 | 36 | ### 基本实现 37 | 38 | 这里说一下这个Demo的实现,通过什么技术,用的哪些知识点完成开发。以及,关于对于权限管理的一些理解与实现,大体分为以下几类: 39 | 40 | #### 视图 41 | 42 | 模板是通过[ElementUI](https://element.eleme.cn/)来搭建的,这个基于的UI组件库非常好用。 43 | 44 | #### 数据Mock 45 | 46 | 数据是通过[mockjs](http://mockjs.com/)来实现的,但是因为这个库好像不能模仿响应状态码,不过用起来还是挺方便的。 47 | 48 | #### 请求 49 | 50 | 请求数据的库,我使用的是[axios](http://www.axios-js.com/)这个库,这个库使用也非常的方便简单。 51 | 52 | #### 数据保存 53 | 54 | 数据的保存通过Vuex和sesstionStorage来实现的。 55 | 56 | ### 权限判断 57 | 58 | 基本实现知识完成了一些基本的东西,权限判断的话,前端主要是分为一下四类: 59 | 60 | #### 路由级别的判定 61 | 62 | 通过**beforeEach**全局守卫钩子,来进行token验证,是否能通过路由来登入其他页面; 63 | 64 | #### 菜单级别的判定 65 | 66 | 通过后端返回的`json`菜单权限数据,进行动态渲染,这里通过`router.addRoutes`动态添加菜单,没有的就不会出现了; 67 | 68 | #### 元素级别的判定 69 | 70 | 通过后端返回的`json`操作权限数据,绑定在`router`元信息上,再通过`this.$route.meta`在页面取出对应权限。再通过自定义指令的实现完成禁止、移除等。 71 | 72 | #### 请求级别的判定 73 | 74 | 通过axios的拦截器,判定某个用户在某个页面的操作是否有权限,通过`router.currentRoute`获取元信息来判定。 75 | 76 | ### 补充(关于代码的一些东西) 77 | 78 | 用户有两个,一个是普通用户,一个是管理员 79 | 80 | - 普通用户用户名就是:普通用户,密码是:normal; 81 | - 管理员用户名就是:管理员,密码是:admin。 82 | 83 | 权限判定的指令使用: 84 | 85 | `v-permission.disabled="condition"` 如果condition为true,则禁用当前绑定指令的标签; 86 | 87 | `v-permission.remove="condition"` 如果condition为true,则移除当前绑定指令的标签。 88 | 89 | 结尾:大体就这些了,接口方面已经写好交互了。 90 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | 44 | 86 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 77 | 78 | -------------------------------------------------------------------------------- /src/components/main/Card.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 82 | 83 | -------------------------------------------------------------------------------- /src/mock/index.js: -------------------------------------------------------------------------------- 1 | // 使用 Mock 2 | const Mock = require('mockjs') 3 | require('./my-radom') 4 | const Random = Mock.Random 5 | Mock.setup({ 6 | timeout: '500-1000' 7 | }) 8 | 9 | const list = [] 10 | 11 | for (let i = 0; i < 20; i++) { 12 | list.push({ 13 | id: i + 1, 14 | date: Random.date(), 15 | name: Random.cname(), 16 | address: Random.address(), 17 | likes: Random.likes() 18 | }) 19 | } 20 | 21 | const users = [ 22 | { 23 | id: 1, 24 | username: '普通用户', 25 | password: 'normal', 26 | token: 'abcdefghijklmnopqrstuvwxyz', 27 | rights: [{ 28 | id: 1, 29 | authName: '一级菜单', 30 | icon: 'icon-menu', 31 | children: [{ 32 | id: 11, 33 | authName: '一级项目1', 34 | path: '/', 35 | rights: ['view', 'edit', 'add', 'delete'] 36 | }, { 37 | id: 11, 38 | authName: '一级项目2', 39 | path: '/', 40 | rights: ['view'] 41 | }] 42 | }] 43 | }, 44 | { 45 | id: 2, 46 | username: '管理员', 47 | password: 'admin', 48 | token: 'abcdefghijklmnopqrstuvwxyz'.split('').reverse().join(''), 49 | rights: [{ 50 | id: 1, 51 | authName: '一级菜单', 52 | icon: 'icon-menu', 53 | children: [{ 54 | id: 11, 55 | authName: '一级项目1', 56 | path: '/', 57 | rights: ['view', 'edit', 'add', 'delete'] 58 | }, { 59 | id: 11, 60 | authName: '一级项目2', 61 | path: '/', 62 | rights: ['view', 'edit', 'add', 'delete'] 63 | }] 64 | }, { 65 | id: 2, 66 | authName: '二级菜单', 67 | icon: 'icon-menu', 68 | children: [{ 69 | id: 22, 70 | authName: '二级项目1', 71 | path: '/', 72 | rights: ['view', 'edit', 'add', 'delete'] 73 | }] 74 | }] 75 | } 76 | ] 77 | 78 | // 获取列表 79 | Mock.mock('/list', 'get', options => { 80 | const { current } = JSON.parse(options.body) 81 | return list.slice(((current - 1) * 10), current * 10) 82 | }) 83 | 84 | // 总数 85 | Mock.mock('/list/total', 'get', () => { 86 | return list.length 87 | }) 88 | 89 | // 查询 90 | Mock.mock('/list/value', 'get', options => { 91 | const { value } = JSON.parse(options.body) 92 | const _list = list.filter(item => { 93 | if (item.name.includes(value) || item.address.includes(value) || item.likes.includes(value)) { 94 | return true 95 | } 96 | return false 97 | }) 98 | return { 99 | list: _list, 100 | total: _list.length 101 | } 102 | }) 103 | 104 | // 添加 105 | Mock.mock('/list/add', 'post', options => { 106 | const { rowData } = JSON.parse(options.body) 107 | rowData.id = list[list.length - 1].id + 1 108 | rowData.date = new Date().toLocaleDateString().replace(/\//g, '-') 109 | list.unshift(rowData) 110 | return rowData 111 | }) 112 | 113 | // 修改 114 | Mock.mock('/list/update', 'put', options => { 115 | const { rowData } = JSON.parse(options.body) 116 | let _rowData = {} 117 | list.forEach((item, idx) => { 118 | if (item.id === rowData.id) { 119 | _rowData = rowData 120 | list[idx] = rowData 121 | } 122 | }) 123 | return _rowData 124 | }) 125 | 126 | // 删除 127 | Mock.mock('/list/delete', 'delete', options => { 128 | const { id } = JSON.parse(options.body) 129 | const index = list.findIndex(item => item.id === id) 130 | const item = index > 0 ? list[index] : {} 131 | list.splice(index, 1) 132 | return item 133 | }) 134 | 135 | // 用户登录 136 | Mock.mock('/login', 'post', options => { 137 | const { username, password } = JSON.parse(options.body) 138 | const user = users.find(item => { 139 | return item.username === username && item.password === password 140 | }) 141 | return user 142 | }) -------------------------------------------------------------------------------- /src/views/Page1.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 197 | 198 | --------------------------------------------------------------------------------