# vue-admin-template **Repository Path**: devr00t/vue-admin-template ## Basic Information - **Project Name**: vue-admin-template - **Description**: 基于vue-admin-template改造的后台管理系统 - **Primary Language**: JavaScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2021-07-07 - **Last Updated**: 2021-11-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README > **vue 后台管理系统基础框架** > 本框架基于花裤衩的基础模板vue-admin-template改造而来。克隆项目版本为v4.4.0,更新于2020.6.21。 # 1 介绍 ## 1.1 目录结构 本项目已经为你生成了一个完整的开发框架,提供了涵盖中后台开发的各类功能和坑位,下面是整个项目的目录结构。 ``` Shell ├── build # 构建相关 ├── mock # 项目mock 模拟数据 ├── plop-templates # 基本模板 ├── public # 静态资源 │ │── favicon.ico # favicon图标 │ └── index.html # html模板 ├── src # 源代码 │ ├── api # 所有请求 │ ├── assets # 主题 字体等静态资源 │ ├── components # 全局公用组件 │ ├── directive # 全局指令 │ ├── filters # 全局 filter │ ├── icons # 项目所有 svg icons │ ├── lang # 国际化 language │ ├── layout # 全局 layout │ ├── router # 路由 │ ├── store # 全局 store管理 │ ├── styles # 全局样式 │ ├── utils # 全局公用方法 │ ├── vendor # 公用vendor │ ├── views # views 所有页面 │ ├── App.vue # 入口页面 │ ├── main.js # 入口文件 加载组件 初始化等 │ └── permission.js # 权限管理 ├── tests # 测试 ├── .env.xxx # 环境变量配置 ├── .eslintrc.js # eslint 配置项 ├── .babelrc # babel-loader 配置 ├── .travis.yml # 自动化CI配置 ├── vue.config.js # vue-cli 配置 ├── postcss.config.js # postcss 配置 └── package.json # package.json ``` ## 1.2 安装 ``` Shell # 克隆项目 git clone https://gitee.com/nichengjing/vue-admin-template.git # 进入项目目录 cd vue-admin-template # 安装依赖 npm install # 建议不要用 cnpm 安装 会有各种诡异的bug 可以通过如下操作解决 npm 下载速度慢的问题 npm install --registry=https://registry.npm.taobao.org # 本地开发 启动项目 npm run dev ``` 启动完成后会自动打开浏览器访问 http://localhost:9527, 你看到下面的页面就代表操作成功了。 ![alt 启动成功界面-首页](markdown-imgs/启动成功界面-首页.png) # 2 风格指南 ## 2.1 Component 所有的Component文件都是以大写开头 (PascalCase),这也是官方所推荐的。 但除了 index.vue。 例子: * @/components/BackToTop/index.vue * @/components/Charts/Line.vue * @/views/example/components/Button.vue ## 2.2 JS 文件 所有的.js文件都遵循横线连接 (kebab-case)。 例子: * @/utils/open-window.js * @/views/svg-icons/require-icons.js * @/components/MarkdownEditor/default-options.js ## 2.3 Views 在views文件下,代表路由的.vue文件都使用横线连接 (kebab-case),代表路由的文件夹也是使用同样的规则。 例子: * @/views/svg-icons/index.vue * @/views/svg-icons/require-icons.js 使用横线连接 (kebab-case)来命名views主要是出于以下几个考虑。 * 横线连接 (kebab-case) 也是官方推荐的命名规范之一 文档 * views下的.vue文件代表的是一个路由,所以它需要和component进行区分(component 都是大写开头) * 页面的url 也都是横线连接的,比如应该要保持统一 * 没有大小写敏感问题 # 3 keep-alive的使用 ## 3.1 应用场景 在开发中经常有从列表跳到详情页,然后返回详情页的时候需要缓存列表页的原来数据以及滚动位置,这个时候就需要保存状态,要缓存状态。 概括来讲: * 列表页面 —— 进入详情页 —— 后退到列表页(缓存列表页的原来数据以及滚动位置) * 重新进入列表页面,获取最新的数据 ## 3.2 如何使用 1、创建router实例的时候加上scrollBehavior方法 ``` js const createRouter = () => new Router({ routes: constantRoutes, scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } else { return { x: 0, y: 0 } } } }) ``` 2、在store的app子模块里加入需要缓存的组件的组件名,和对应的方法 ``` js export default new Vuex.Store({ state: { includeList: [] }, mutations: { SET_INCLUDELIST: (state, includeList) => { state.includeList = includeList } } }) ``` 3、在AppMain.vue中 ``` html ``` 4、在beforeRouteLeave钩子函数里控制需要缓存的组件 注意:beforeRouteLeave导航守卫是必须写在当前单页面中,不能写在App.vue中 ``` js beforeRouteEnter(to, from, next) { // 进入之前缓存本组件,解决第一次不缓存列表页的问题 next((vm) => { vm.$store.commit("SET_INCLUDELIST", ["good-list"]); }); }, beforeRouteLeave(to, from, next) { // 跳转到详情页时缓存当前列表页,反之不缓存 if (to.path.indexOf("good-detail") > -1) { this.$store.commit("SET_INCLUDELIST", ["good-list"]); } else { this.$store.commit("SET_INCLUDELIST", []); this.$destroy(); // 销毁vue实例 } next(); } ``` # 4 iconfont element-ui自带的图标不是很丰富,但管理后台图标的定制性又很强。这里推荐使用阿里的 iconfont ,简单好用又方便管理。 这里来简单介绍一下 iconfont 的使用方式。首先注册好 iconfont 账号之后,可以在我的项目中管理自己的 iconfont 。 ![alt iconfont项目信息](markdown-imgs/iconfont项目信息.png) ![alt iconfont图标管理](markdown-imgs/iconfont图标管理.png) > 下面提供了iconfont图标的两种使用方式,一般情况下默认使用第二种Symbol方式 ## 4.1 Font class方式 应用场景:需要配合element的组件使用时 1、点击下载至本地,将所有文件放入icons/font-class目录下 2、打开iconfont.css文件添加类,如下 ```css [class^="el-icon-my"], [class*=" el-icon-my"] { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } ``` 3、main.js中引入iconfont.css文件 ```js import '@/icons/font-class/iconfont.css' ``` 4、直接使用下面代码,不需要和原来一样写两个类名 ```html ``` ## 4.2 Symbol方式 应用场景:左侧菜单栏或需要支持多色图标 1、将单个图标svg下载,放入icons/svg目录下,重命名为home 2、直接使用下面代码 ```html ``` # 5 非父子组件之间传值 vuex中一般存放全局变量,如登录token,用户信息,或者是一些全局个人偏好设置等,每个页面里存放自己的 data 就行。 下面的eventBus组件之间传值的时候会有bug,[详见](https://www.jianshu.com/p/fde85549e3b0),所以还是使用vuex 1、声明一个空的Vue模块eventBus ```js import Vue from 'vue' const eventBus = new Vue() export default eventBus ``` 2、A页面通过eventBus.$emit传参给B页面 ```js import eventBus from '@/utils/eventbus.js' back(info){ // 传递一个map,addressInfo是key,info是value eventBus.$emit('addressInfo',info) // 调用router回退页面 this.$router.go(-1) } ``` 3、B页面接收参数 ```js import eventBus from '@/utils/eventbus.js'; activated(){ // 根据key名获取传递回来的参数,data就是map eventBus.$on('addressInfo', (data) => { console.log(data,"data"); }); }, ``` # 6 权限验证和动态路由 ## 6.1 铁职项目逻辑 * views/login/index.vue 中调用actions的LoginByEmail方法 * views/store/modules/user.js 中调用登录接口,调用mutations中的SET_TOKEN方法将返回token值赋值给state中的token变量 ```js login = { // 登录接口返回数据 data: "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJJZCI6IjEiLCJuYW1lIjoiYWRtaW4iLCJleHAiOjE2MDY0NTAzNjB9.KuWhVIjAbm-mlWq49952pBsNG3Yb7VroConkZahbxcX1YBxn6PmvkEhiZP2ElM7tVvS0-OATvY2JEI0UWw2atOJKCgHO6vmGb_9EmiTD2YDnJFxf1pVXErhok8wVB4C6BzOzf-nXPp_JxQyr2huNsfX7EGUHaxq9hKKl24YdqhE", name: "admin", onlyCode: null, status: 200, userId: "1", userName: "admin" } userInfo = { // 用户信息接口返回数据 description: "wq", password: "afdd0b4ad2ec172c586e2150770fbf9e", username: "admin", id: "1", menus: [ { code: "Menu", menu: "菜单管理", method: "GET", name: "访问", type: "menu", uri: "/Menu" } ] } allMenus = [ // 所有菜单接口返回数据 { code: "Menu" crtHost: "127.0.0.1,192.168.0.59", crtName: "admin", crtTime: "2020-11-26 11:44:00", crtUser: "1", id: "0Pm8kEMQGvy7jrpQCxSr1g3hcRa1hgL6", parentId: "qeCWCQjgTBDVEq0tC3KvEqn5kQ8xgPV9", title: "菜单管理", type: "menu", updHost: "127.0.0.1,192.168.0.59", updName: "admin", updTime: "2020-11-26 11:44:00", updUser: "1" } ] ``` * views/login/index.vue 进行路由跳转 * views/main.js 用户登录成功之后,我们会在全局钩子router.beforeEach中拦截路由,判断是否已获得token - 如果没有token则判断此path是否在白名单中,true则跳转,false则重定向到登录页 - 如果有token则判断此path是否为登录页,true则重定向到首页,false则调用actions中的GetInfo方法获取用户基本信息,调用actions中的GenerateRoutes方法生成可访问的路由表 ```js // 用户信息中的菜单数据 info.menus = [ { code: "userManager" menu: "用户管理" method: "GET" name: "访问" type: "menu" uri: "/admin/user" } ] // 格式化后的用户菜单数据 menus = { userManager: true } ``` * views/store/modules/permission.js 调用getAllMenus接口获取所有菜单数据,然后递归生成可访问的路由表 ```js // 所有菜单数据 const data = [ { attr1: "_import('admin/user/index')" code: "userManager" description: "" href: "/admin/user" icon: "el-icon-my-userManager" id: "1" parentId: "5" path: "/adminSys/baseManager/userManager" title: "用户管理" type: "menu" updHost: "127.0.0.1" updName: "admin3" updTime: "2020-11-02 10:54:47" updUser: "15" }, { attr1: "Layout" code: "adminSys" description: "" href: "/base" icon: "el-icon-my-dashboard" id: "13" parentId: "-1" path: "/adminSys" title: "首页" type: "dirt" updHost: "192.168.0.116,172.17.0.5" updName: "admin4" updTime: "2020-11-05 17:58:00" updUser: "35" } ] // 格式化后的所有菜单数据 const menuDatas = { userManager: { attr1: "_import('admin/user/index')" code: "userManager" description: "" href: "/admin/user" icon: "el-icon-my-userManager" id: "1" parentId: "5" path: "/adminSys/baseManager/userManager" title: "用户管理" type: "menu" updHost: "127.0.0.1" updName: "admin3" updTime: "2020-11-02 10:54:47" updUser: "15" }, adminSys: { attr1: "Layout" code: "adminSys" description: "" href: "/base" icon: "el-icon-my-dashboard" id: "13" parentId: "-1" path: "/adminSys" title: "首页" type: "dirt" updHost: "192.168.0.116,172.17.0.5" updName: "admin4" updTime: "2020-11-05 17:58:00" updUser: "35" } } // 生成的路由表 accessedRouters = [ { authority: "baseManager", icon: "el-icon-my-baseManager" name: "基础配置管理" path: "/baseManager", component: Layout, children: [ { authority: "userManager", icon: "el-icon-my-userManager", name: "用户管理", path: "userManager", component: _import('admin/user/index') } ] } ] ``` * views/main.js 将生成的路由表挂载到实例上 总结:不管前面逻辑如何复杂,最终动态生成路由表的逻辑在递归函数filterAsyncRouter中,入参有3个,asyncRouterMap为本地的路由表、menus为用户的菜单权限、menuDatas为所有菜单数据,filter方法筛选asyncRouterMap,只保留用户有权限的路由并将菜单的名称和图标赋给路由。 所有事情的本质:路由表还是由前端维护,动态生成的时候只取了具有权限的路由和其图标。 ## 6.2 基础模板改造 路由表数据格式需求:按照基本模板的数据格式,动态生成路由表。 1、前台数据 src\router\index.js ```js export const asyncRouterMap = [ // 首页 一级菜单 { path: '/', component: Layout, redirect: '/dashboard', children: [{ path: 'dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/index'), meta: { title: 'Dashboard', icon: 'dashboard' } }] }, // 普通 一级菜单 { path: '/form', component: Layout, children: [ { path: 'index', name: 'Form', component: () => import('@/views/form/index'), meta: { title: 'Form', icon: 'form' } } ] }, // 二级菜单 { path: '/example', component: Layout, redirect: '/example/table', name: 'Example', meta: { title: 'Example', icon: 'el-icon-s-help' }, children: [ { path: 'table', name: 'Table', component: () => import('@/views/table/index'), meta: { title: 'Table', icon: 'table' } }, { path: 'tree', name: 'Tree', component: () => import('@/views/tree/index'), meta: { title: 'Tree', icon: 'tree' } } ] }, // 三级菜单 { path: '/nested', component: Layout, redirect: '/nested/menu1', name: 'Nested', meta: { title: 'Nested', icon: 'nested' }, children: [ { path: 'menu1', component: () => import('@/views/nested/menu1/index'), // Parent router-view name: 'Menu1', meta: { title: 'Menu1' }, children: [ { path: 'menu1-2', component: () => import('@/views/nested/menu1/menu1-2'), name: 'Menu1-2', meta: { title: 'Menu1-2' }, children: [ { path: 'menu1-2-1', component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'), name: 'Menu1-2-1', meta: { title: 'Menu1-2-1' } } ] } ] } ] }, ] ``` 2、在filterAsyncRouter方法中 ```js route.meta.title = (menuDatas[route.name] || {}).title route.meta.icon = (menuDatas[route.name] || {}).icon ``` 3、layout/Sidebar/index.vue中`this.router.option.router`修改为`this.$store.getters.permission_routers` 4、没有权限限制的页面,meta中不填写roles属性。有权限限制的页面,但是如果不想在菜单中显示,则给路由添加hidden:true 5、路由配置项 ```js // 当设置 true 的时候该路由不会在侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1 hidden: true // (默认 false) //当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 redirect: 'noRedirect' // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 // 若你想不管路由下面的 children 声明的个数都显示你的根路由 // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由 alwaysShow: true name: 'router-name' // 设定路由的名字,一定要填写不然使用时会出现各种问题 meta: { roles: true // roles设置为true时,代表需要后台权限的路由 title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 icon: 'svg-name' // 设置该路由的图标,支持 svg-class,也支持 el-icon-x element-ui 的 icon // noCache: true // 如果设置为true,则不会被 缓存(默认 false) breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示(默认 true) // affix: true // 若果设置为true,它则会固定在tags-view中(默认 false) // 当路由设置了该属性,则会高亮相对应的侧边栏。 // 这在某些场景非常有用,比如:一个文章的列表页路由为:/article/list // 点击文章进入文章详情页,这时候路由为/article/1,但你想在侧边栏高亮文章列表的路由,就可以进行如下设置 activeMenu: '/article/list' } ``` 6、某些场景下,用户需要默认展开侧边栏的某些sub-menu,可以通过default-openeds来进行设置,首先找到侧边栏代码 ```html ``` # 7.后台接口和本地mock同时使用,打包时移除mock 1、utils/request.js中放开baseUrl ```js const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url // withCredentials: true, // send cookies when cross-domain requests timeout: 5000, // request timeout 使用代理后,此设置项无效 headers: { // IE模式下取消请求缓存 'Cache-Control': 'no-cache', Pragma: 'no-cache' } }) ``` 2、.env.development中`VUE_APP_BASE_API = ''` ```Shell # just a flag ENV = 'development' # base api # VUE_APP_BASE_API = '/dev-api' VUE_APP_BASE_API = '' ``` 3、vue.config.js中 ```js devServer: { port: port, open: true, overlay: { warnings: false, errors: true }, proxy: { [process.env.VUE_APP_BASE_API + '/api']: { target: 'http://192.168.0.200:9999', changeOrigin: true, pathRewrite: { ['^' + process.env.VUE_APP_BASE_API]: '' } } }, after: require('./mock/mock-server.js') } ``` 4、打包时移除mock,在mian.js中将下面代码注释 ```js /* if (process.env.NODE_ENV === 'production') { const { mockXHR } = require('../mock') mockXHR() } */ ``` 5、后台接口和mock接口共存 开发环境:webpack自动开启mock服务,只用将`/api`开头的后台接口代理,`/mock`开头的mock接口会自动请求本地mock服务 生成环境:在nginx中将`/api`开头的后台接口代理到后台服务,`/mock`开头的mock接口代理到mock服务 # 8.接口请求全局loading 1、src/store/modules/app.js 中添加对应变量和方法 2、src/utils/request.js 中axios中做拦截处理 3、src/layout/components/AppMain.vue 中loading显示隐藏 可以有效防止用户快速点击或网上慢时多次重复请求的问题 # 更新记录 **v0.1** :2020-11-03 克隆基本模板vue-admin-template。 **v0.2** : 2020-11-04 新增风格指南、keep-alive使用、iconfont字体图标两种方式使用、非父子组件传值、移除Mock并添加proxy代理。 **v0.3**:2020-11-09 新增权限验证和动态路由功能,左侧菜单根据框架自带的路由表数据结构生成。 **v0.4**:2020-11-11 新增首页和图表模块,删除【移除Mock并添加proxy代理】将其修改到【后台接口和本地mock同时使用,打包时移除mock】中。 **v0.5**: 2020-11-12 研究开发环境和生产环境下后台接口和mock接口共存的设置,新增接口请求全局loading效果,将综合表格不要的代码删除改成适合自己的风格。 **v0.6**:2020-11-13 菜单管理模块完成,角色管理模块完成30%。 **v0.7**:2020-11-16 角色管理模块完成,用户管理模块完成50%,剩余部分没有对应接口。 **v0.8**:2020-11-17 axios拦截重复请求参数不同时不拦截,菜单管理中的前端路由数据放到router中分模块管理,日志查询模块完成,登录页面改造引入jquery和其粒子背景插件,内容管理的分类管理模块完成。 **v0.9**:2020-11-18 js-md5插件引入将登录密码md5加密,内容管理的文章列表模块完成并将内容管理的所有本地mock接口调通,将所有接口的前缀放到环境变量中统一管理。 **v0.10**:2020-11-23 删除综合表格模块,内容管理的文章列表添加和编辑加入富文本编辑器,并改为单独页面 **v0.11**:2020-11-24 用户管理模块与后台对接;文章列表模块与后台对接,从编辑页面返回列表页时列表页保持原始状态,图片上传组件;非父子组件之间传值使用vuex,eventBus有bug书写比较麻烦 **v1.0**:2020-11-25 用户管理和内容管理对接完成,发布v1.0版本 **v1.0.1**:2020-11-26 修复了用户权限为空时登录失败的问题