├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ └── index.js ├── assets │ └── imgs │ │ ├── 404.jpg │ │ ├── bg00.jpg │ │ ├── bg01.jpg │ │ ├── bg02.jpg │ │ ├── bg03.jpg │ │ ├── bg04.jpg │ │ ├── bg05.jpg │ │ ├── bg06.jpg │ │ ├── logo.png │ │ └── user.jpg ├── components │ ├── 404.vue │ ├── Index.vue │ └── Login.vue ├── main.js ├── permission.js ├── router │ └── index.js ├── store │ └── index.js ├── utils │ ├── createRoutes.js │ ├── index.js │ ├── loading.js │ └── request.js └── views │ ├── Home.vue │ ├── Msg.vue │ ├── Other.vue │ ├── Password.vue │ ├── T1.vue │ └── UserInfo.vue ├── tests └── unit │ └── utils.spec.js ├── update.md └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 4 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 140 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/airbnb', 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'array-element-newline': ['error', 'consistent'], 14 | 'indent': ['error', 4, { 'MemberExpression': 0, 'SwitchCase': 1 }], 15 | 'quotes': ['error', 'single'], 16 | 'comma-dangle': ['error', 'always-multiline'], 17 | 'semi': ['error', 'never'], 18 | 'object-curly-spacing': ['error', 'always'], 19 | 'max-len': ['error', 140], 20 | 'no-new': 'off', 21 | 'linebreak-style': 'off', 22 | 'import/extensions': 'off', 23 | 'eol-last': 'off', 24 | 'no-shadow': 'off', 25 | 'no-unused-vars': 'warn', 26 | 'import/no-cycle': 'off', 27 | 'arrow-parens': 'off', 28 | 'eqeqeq': 'off', 29 | 'no-param-reassign': 'off', 30 | 'import/prefer-default-export': 'off', 31 | 'no-use-before-define': 'off', 32 | 'no-continue': 'off', 33 | 'prefer-destructuring': 'off', 34 | 'no-plusplus': 'off', 35 | 'prefer-const': 'off', 36 | 'global-require': 'off', 37 | 'no-prototype-builtins': 'off', 38 | 'consistent-return': 'off', 39 | 'vue/require-component-is': 'off', 40 | 'prefer-template': 'off', 41 | 'one-var-declaration-per-line': 'off', 42 | 'one-var': 'off', 43 | 'import/named': 'off', 44 | 'object-curly-newline': 'off', 45 | 'default-case': 'off', 46 | 'import/no-dynamic-require': 'off', 47 | }, 48 | parserOptions: { 49 | parser: 'babel-eslint', 50 | }, 51 | overrides: [ 52 | { 53 | files: [ 54 | '**/__tests__/*.{j,t}s?(x)', 55 | '**/tests/unit/**/*.spec.{j,t}s?(x)', 56 | ], 57 | env: { 58 | jest: true, 59 | }, 60 | }, 61 | ], 62 | } -------------------------------------------------------------------------------- /.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 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 bin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue 轻量级后台管理系统基础模板(停止维护,不建议使用) 2 | 3 | ### [在线预览](https://woai3c.github.io/vue-admin-template) 4 | ### [更新日志](https://github.com/woai3c/vue-admin-template/blob/master/update.md) 5 | ### 相关依赖 6 | * [vue-router](https://router.vuejs.org/zh/) 7 | * [iview](https://www.iviewui.com/docs/guide/install) 8 | * [axios](https://www.kancloud.cn/yunye/axios/234845) 9 | * [vuex](https://vuex.vuejs.org/zh/) 10 | 11 | ### 功能 12 | 13 | #### 登录页 14 | * 一周七天自动切换不同的壁纸(建议自己配置) 15 | 16 | #### 标签栏 17 | * 点击标签切换页面 18 | * 刷新当前标签页 19 | * 关闭其他标签/关闭所有标签 20 | 21 | **注意:** 组件的名称和路由的名称一定要一致,例如 `Home.vue` 组件名称 `name: home`,则在路由文件中也要给它设置为 `name: home`,否则页面内容不能缓存 22 | 23 | ```js 24 | // 在router文件中 25 | { 26 | path: 'home', 27 | name: 'home', 28 | component: () => import('../views/Home.vue') 29 | } 30 | 31 | // 在Home.vue中 32 | export default { 33 | name: 'home' 34 | } 35 | ``` 36 | 37 | #### 侧边栏 38 | * 伸展/收缩 39 | * 页面宽度过小自动收缩 40 | * 多级菜单(利用iView组件) 41 | 42 | #### 用户相关 43 | * 消息通知 44 | * 用户头像 45 | * 基本资料 46 | 47 | #### 动态菜单栏 48 | * 根据数据动态生成菜单 49 | * 在菜单项上添加 hidden 属性可以隐藏该菜单项,但还是可以正常访问页面,具体请看 DEMO 及其相关代码 50 | 51 | #### 面包屑 52 | * 展示当前页面的路径 53 | 54 | #### 权限控制 55 | * 如果在未登陆的情况下访问指定页面 将会重定向到登陆页 56 | 57 | #### [eslint + vscode 自动格式化代码](https://github.com/woai3c/Front-end-articles/blob/master/eslint-vscode-format.md) 58 | 具体配置方法请点击上面的链接,如果不需要 eslint,请将相关依赖卸载以及根目录下的 `.eslintrc.js` 删除。 59 | 60 | #### [jest 单元测试](https://vue-test-utils.vuejs.org/zh/guides/testing-single-file-components-with-jest.html) 61 | 如果不需要,请卸载相关依赖及删除根目录下的 `tests` 目录 62 | 63 | #### 页面标题 `document.title` 64 | 在 `src/utils/index` 下可设置默认的 `title`,在每个路由配置项上可设置对应的 `title`,具体示例请看代码 65 | 66 | #### 其它 67 | * 利用`axios`拦截器 实现了`ajax`请求前展示`loading` 请求结束关闭`loading` 68 | 69 | ### 注意 70 | * 源码可见 并且添加了必要的注释 可以自行更改 71 | 72 | 73 | `Index`组件一般情况下只需要传数据就行 其他不用关注 74 | 75 | 市面上有大量的vue后台管理系统模板 但是功能都太丰富了 而且有很多组件用不上 所以写了这么一个最基础的 只有必要功能的模板 76 | UI库使用的是`iView` 有大量的组件可用 77 | 78 | ### 使用 79 | #### 下载 80 | ``` 81 | git clone https://github.com/woai3c/vue-admin-template.git 82 | 83 | cd vue-admin-template 84 | 85 | npm i 86 | ``` 87 | 88 | #### 开发 89 | ``` 90 | npm run serve 91 | ``` 92 | 93 | #### 打包 94 | ```` 95 | npm run build 96 | ```` 97 | 打包后的文件不能放在服务器根目录,否则会出现空白页面。 98 | 99 | 如果确实要把文件放在服务器根目录则需要更改打包的路径,打开 `vue.config.js` 文件,将如下代码删去即可。 100 | ```js 101 | publicPath: './', 102 | ``` 103 | 104 | ## License 105 | MIT 106 | ## 赞助 107 | ![](https://github.com/woai3c/nand2tetris/blob/master/img/wx.jpg) 108 | ![](https://github.com/woai3c/nand2tetris/blob/master/img/zfb.jpg) 109 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest', 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin-template", 3 | "version": "1.0.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 | "test": "vue-cli-service test:unit" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.19.0", 13 | "core-js": "^3.31.0", 14 | "view-design": "^4.0.2", 15 | "vue": "^2.6.10", 16 | "vue-router": "^3.0.6", 17 | "vuex": "^3.1.2" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "^4.1.0", 21 | "@vue/cli-plugin-eslint": "^4.1.0", 22 | "@vue/cli-plugin-unit-jest": "^4.1.0", 23 | "@vue/cli-service": "^4.1.0", 24 | "@vue/eslint-config-airbnb": "^4.0.0", 25 | "@vue/test-utils": "1.0.0-beta.29", 26 | "babel-eslint": "^10.0.3", 27 | "eslint": "^5.16.0", 28 | "eslint-plugin-vue": "^5.0.0", 29 | "vue-template-compiler": "^2.6.10" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-admin-template 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 27 | 28 | 62 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchUserData() { 4 | return request.get('https://api.github.com/users/woai3c') 5 | } -------------------------------------------------------------------------------- /src/assets/imgs/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/404.jpg -------------------------------------------------------------------------------- /src/assets/imgs/bg00.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/bg00.jpg -------------------------------------------------------------------------------- /src/assets/imgs/bg01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/bg01.jpg -------------------------------------------------------------------------------- /src/assets/imgs/bg02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/bg02.jpg -------------------------------------------------------------------------------- /src/assets/imgs/bg03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/bg03.jpg -------------------------------------------------------------------------------- /src/assets/imgs/bg04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/bg04.jpg -------------------------------------------------------------------------------- /src/assets/imgs/bg05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/bg05.jpg -------------------------------------------------------------------------------- /src/assets/imgs/bg06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/bg06.jpg -------------------------------------------------------------------------------- /src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /src/assets/imgs/user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woai3c/vue-admin-template/bc2c89d89abc738526af865b23417243c8899ea8/src/assets/imgs/user.jpg -------------------------------------------------------------------------------- /src/components/404.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/Index.vue: -------------------------------------------------------------------------------- 1 | 156 | 157 | 516 | 517 | -------------------------------------------------------------------------------- /src/components/Login.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 87 | 88 | 153 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | import ViewUI from 'view-design' 4 | import App from './App' 5 | import store from './store' 6 | import router from './router' 7 | import 'view-design/dist/styles/iview.css' 8 | import './permission' 9 | 10 | Vue.config.productionTip = false 11 | Vue.use(ViewUI) 12 | 13 | Vue.prototype.$axios = axios 14 | 15 | new Vue({ 16 | el: '#app', 17 | router, 18 | store, 19 | render: h => h(App), 20 | }) -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import { LoadingBar } from 'view-design' 2 | import router from './router' 3 | import store from './store' 4 | import createRoutes from '@/utils/createRoutes' 5 | import { getDocumentTitle, resetTokenAndClearUser } from './utils' 6 | 7 | // 是否有菜单数据 8 | let hasMenus = false 9 | router.beforeEach(async (to, from, next) => { 10 | document.title = getDocumentTitle(to.meta.title) 11 | LoadingBar.start() 12 | if (localStorage.getItem('token')) { 13 | if (to.path === '/login') { 14 | next({ path: '/' }) 15 | } else if (hasMenus) { 16 | next() 17 | } else { 18 | try { 19 | // 这里可以用 await 配合请求后台数据来生成路由 20 | // const data = await axios.get('xxx') 21 | // const routes = createRoutes(data) 22 | const routes = createRoutes(store.state.menuItems) 23 | // 动态添加路由 24 | router.addRoutes(routes) 25 | hasMenus = true 26 | next({ path: to.path || '/' }) 27 | } catch (error) { 28 | resetTokenAndClearUser() 29 | next(`/login?redirect=${to.path}`) 30 | } 31 | } 32 | } else { 33 | hasMenus = false 34 | if (to.path === '/login') { 35 | next() 36 | } else { 37 | next(`/login?redirect=${to.path}`) 38 | } 39 | } 40 | }) 41 | 42 | router.afterEach(() => { 43 | LoadingBar.finish() 44 | }) -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | const commonRoutes = [ 7 | { 8 | path: '/login', 9 | name: 'login', 10 | meta: { title: '登录' }, 11 | component: () => import('../components/Login.vue'), 12 | }, 13 | { 14 | path: '/other', // 点击侧边栏跳到一个单独的路由页面,需要定义,层级和其他顶级路由一样 15 | name: 'other', 16 | meta: { title: '单独的路由' }, 17 | component: () => import('../views/Other.vue'), 18 | }, 19 | { 20 | path: '/404', 21 | name: '404', 22 | meta: { title: '404' }, 23 | component: () => import('../components/404.vue'), 24 | }, 25 | { path: '/', redirect: '/home' }, 26 | ] 27 | 28 | // 本地所有的页面 需要配合后台返回的数据生成页面 29 | export const asyncRoutes = { 30 | home: { 31 | path: 'home', 32 | name: 'home', 33 | meta: { title: '主页' }, 34 | component: () => import('../views/Home.vue'), 35 | }, 36 | t1: { 37 | path: 't1', 38 | name: 't1', 39 | meta: { title: '表格' }, 40 | component: () => import('../views/T1.vue'), 41 | }, 42 | password: { 43 | path: 'password', 44 | name: 'password', 45 | meta: { title: '修改密码' }, 46 | component: () => import('../views/Password.vue'), 47 | }, 48 | msg: { 49 | path: 'msg', 50 | name: 'msg', 51 | meta: { title: '通知消息' }, 52 | component: () => import('../views/Msg.vue'), 53 | }, 54 | userinfo: { 55 | path: 'userinfo', 56 | name: 'userinfo', 57 | meta: { title: '用户信息' }, 58 | component: () => import('../views/UserInfo.vue'), 59 | }, 60 | } 61 | 62 | const createRouter = () => new Router({ 63 | routes: commonRoutes, 64 | }) 65 | 66 | const router = createRouter() 67 | 68 | export function resetRouter() { 69 | const newRouter = createRouter() 70 | router.matcher = newRouter.matcher 71 | } 72 | 73 | export default router -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | const store = new Vuex.Store({ 7 | state: { 8 | isShowLoading: false, // 全局 loading 9 | // 左侧菜单栏数据 10 | menuItems: [ 11 | { 12 | name: 'home', // 要跳转的路由名称 不是路径 13 | size: 18, // icon大小 14 | type: 'md-home', // icon类型 15 | text: '主页', // 文本内容 16 | }, 17 | { 18 | name: 'other', // 要跳转的路由名称 不是路径 19 | size: 18, // icon大小 20 | type: 'ios-egg-outline', // icon类型 21 | text: '单独的路由', // 点击侧边栏跳到一个单独的路由页面,需要提前在 router.js 定义 22 | }, 23 | { 24 | size: 18, // icon大小 25 | type: 'md-arrow-forward', // icon类型 26 | text: '外链', 27 | url: 'https://www.baidu.com', 28 | isExternal: true, // 外链 跳到一个外部的 URL 页面 29 | }, 30 | { 31 | text: '二级菜单', 32 | type: 'ios-paper', 33 | children: [ 34 | { 35 | type: 'ios-grid', 36 | name: 't1', 37 | text: '表格', 38 | // hidden 属性 隐藏此菜单 可以通过在地址栏上输入对应的 URL 来显示页面 39 | // hidden: true, 40 | }, 41 | { 42 | size: 18, // icon大小 43 | type: 'md-arrow-forward', // icon类型 44 | text: '外链', 45 | url: 'https://www.baidu.com', 46 | isExternal: true, // 外链 跳到一个外部的 URL 页面 47 | }, 48 | { 49 | text: '三级菜单', 50 | type: 'ios-paper', 51 | children: [ 52 | { 53 | type: 'ios-notifications-outline', 54 | name: 'msg', 55 | text: '查看消息', 56 | }, 57 | { 58 | type: 'md-lock', 59 | name: 'password', 60 | text: '修改密码', 61 | }, 62 | { 63 | type: 'md-person', 64 | name: 'userinfo', 65 | text: '基本资料', 66 | }, 67 | { 68 | size: 18, // icon大小 69 | type: 'md-arrow-forward', // icon类型 70 | text: '外链', 71 | url: 'https://www.baidu.com', 72 | isExternal: true, // 外链 跳到一个外部的 URL 页面 73 | }, 74 | ], 75 | }, 76 | ], 77 | }, 78 | ], 79 | }, 80 | mutations: { 81 | setMenus(state, items) { 82 | state.menuItems = [...items] 83 | }, 84 | setLoading(state, isShowLoading) { 85 | state.isShowLoading = isShowLoading 86 | }, 87 | }, 88 | }) 89 | 90 | export default store -------------------------------------------------------------------------------- /src/utils/createRoutes.js: -------------------------------------------------------------------------------- 1 | import { asyncRoutes } from '@/router' 2 | 3 | // 将菜单信息转成对应的路由信息 动态添加 4 | export default function createRoutes(data) { 5 | const result = [] 6 | const children = [] 7 | 8 | result.push({ 9 | path: '/', 10 | component: () => import('../components/Index.vue'), 11 | children, 12 | }) 13 | 14 | data.forEach(item => { 15 | generateRoutes(children, item) 16 | }) 17 | 18 | // 最后添加404页面 否则会在登陆成功后跳到404页面 19 | result.push( 20 | { path: '*', redirect: '/404' }, 21 | ) 22 | 23 | return result 24 | } 25 | 26 | function generateRoutes(children, item) { 27 | if (item.name) { 28 | if (asyncRoutes[item.name]) children.push(asyncRoutes[item.name]) 29 | } else if (item.children) { 30 | item.children.forEach(e => { 31 | generateRoutes(children, e) 32 | }) 33 | } 34 | } -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { resetRouter } from '@/router' 2 | 3 | export function resetTokenAndClearUser() { 4 | // 退出登陆 清除用户资料 5 | localStorage.setItem('token', '') 6 | localStorage.setItem('userImg', '') 7 | localStorage.setItem('userName', '') 8 | // 重设路由 9 | resetRouter() 10 | } 11 | 12 | export const defaultDocumentTitle = 'vue-admin-template' 13 | export function getDocumentTitle(pageTitle) { 14 | if (pageTitle) return `${defaultDocumentTitle} - ${pageTitle}` 15 | return `${defaultDocumentTitle}` 16 | } -------------------------------------------------------------------------------- /src/utils/loading.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | let loadingCounter = 0 4 | 5 | export function showLoading() { 6 | if (loadingCounter === 0) { 7 | store.commit('setLoading', true) 8 | } 9 | 10 | loadingCounter++ 11 | } 12 | 13 | export function closeLoading() { 14 | loadingCounter-- 15 | if (loadingCounter <= 0) { 16 | loadingCounter = 0 17 | store.commit('setLoading', false) 18 | } 19 | } -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Message } from 'view-design' 3 | import router from '@/router' 4 | import { showLoading, closeLoading } from '@/utils/loading' 5 | import { resetTokenAndClearUser } from '@/utils' 6 | 7 | const service = axios.create({ 8 | baseURL: window.location.origin, 9 | timeout: 60000, 10 | }) 11 | 12 | service.interceptors.request.use(config => { 13 | showLoading() 14 | if (localStorage.getItem('token')) { 15 | config.headers.Authorization = localStorage.getItem('token') 16 | } 17 | 18 | return config 19 | }, (error) => Promise.reject(error)) 20 | 21 | service.interceptors.response.use(response => { 22 | closeLoading() 23 | const res = response.data 24 | // 这里是接口处理的一个示范,可以根据自己的项目需求更改 25 | // 错误处理 26 | if (res.code != 0 && res.msg) { 27 | Message.error({ 28 | content: res.msg, 29 | }) 30 | 31 | // token 无效,清空路由,退出登录 32 | if (res.code == 2) { 33 | resetTokenAndClearUser() 34 | router.push('login') 35 | } 36 | 37 | return Promise.reject() 38 | } 39 | 40 | // 如果接口正常,直接返回数据 41 | return res 42 | }, (error) => { 43 | closeLoading() 44 | if (error.name == 'Error') { 45 | Message.error({ 46 | content: error.msg, 47 | }) 48 | } else { 49 | Message.error({ 50 | content: error.response.data.data || error.message, 51 | }) 52 | } 53 | 54 | return Promise.reject(error) 55 | }) 56 | 57 | export default service 58 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/Msg.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /src/views/Other.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/Password.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/views/T1.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 281 | 282 | -------------------------------------------------------------------------------- /src/views/UserInfo.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /tests/unit/utils.spec.js: -------------------------------------------------------------------------------- 1 | import { defaultDocumentTitle, getDocumentTitle } from '@/utils' 2 | 3 | it('getDocumentTitle test', () => { 4 | const title = '这是一个测试' 5 | expect(getDocumentTitle(title)).toMatch(`${defaultDocumentTitle} - ${title}`) 6 | }) 7 | -------------------------------------------------------------------------------- /update.md: -------------------------------------------------------------------------------- 1 | ## 更新日志 2 | ### 2020.8.30 更新 3 | * build: 打包后的文件从绝对路径改成相对路径,也就是说打包后的文件不能放在服务器根目录下。 4 | ### 2020.8.14 更新 5 | * new: loading 从 `components/Index.vue` 挪到了 `App.vue`。axios 从 `components/Index.vue` 挪到了 `utils/request.js`,并对其进行了封装,方便复用。 6 | * new: 重构了 loading 和 axios 拦截器使用方式,并提供了一个 ajax DEMO 放在首页。 7 | ### 2020.6.5 更新 8 | * new: 新增外链功能,点击菜单可以跳到一个新页面,地址为指定的 URL。 9 | * new: 新增独立路由页面功能,点击侧边栏可以跳转到单独的路由页面(铺满屏幕,顶级路由)。 10 | 11 | 具体示例请查看源码 `src/store/index` 和 [demo](https://woai3c.github.io/) 12 | ### 2019.12.21 更新 13 | * refactor: 将 `404` 页面独立出来,单独展示(占满屏幕) 14 | * new: [新增 eslint,配合 vscode 可以自动格式化代码](https://github.com/woai3c/Front-end-articles/blob/master/eslint-vscode-format.md) 15 | * new: 新增 jest 单元测试 16 | * new: 页面标题 `document.title`,在 `src/utils/index` 下可设置默认的 `title`,在每个路由配置项上可设置对应的 `title`,具体示例请看代码 17 | 18 | ### 2019.12.13 更新 19 | * fix: 修复在IE下关闭标签栏时,页面抖动的问题 20 | * refactor: 同时将左右两栏的布局方式从 flex 布局更改为 fixed + margin 的方式 21 | 22 | ### 2019.10.30 更新 23 | * new: 在对应的菜单项上添加 `hidden` 属性,即可隐藏对应的菜单项,但还是可以在地址栏上输入对应的 URL 来访问页面。 24 | 使用方法 25 | ```js 26 | { 27 | type: 'ios-grid', 28 | name: 't1', 29 | text: '表格', 30 | hidden: true, // 隐藏此菜单 可以通过在地址栏上输入对应的 URL 来显示页面 31 | } 32 | ``` 33 | 34 | ### 2019.10.14 更新 35 | * fix: 修复窗口宽度过小不会收缩侧边栏的问题 36 | * new: 打开页面时,默认展开和路由对应的菜单栏 37 | 38 | ### 2019.8.19 更新 39 | * fix: `components/Index.vue` 文件第 31 行代码的 `v-show="isShowAsideTitle"` 会造成侧边栏收缩时二级菜单隐藏,目前已修复。 40 | ### 2019.7.24 更新 41 | * new: 增加页面进度条,跳转时显示 42 | 43 | ### 2019.6.25 更新 44 | * fix: 修复路由表冲突问题 45 | 46 | 退出当前用户,换账号重新登陆时,上个账号和现在的账号路由表会有冲突的问题,解决办法是在退出登陆时重置路由表。 47 | 48 | 具体实现请查看 `router/index.js`、`Login.vue` 和 `Index.vue` 的退出登陆回调方法。 49 | 50 | ### 2019.6.18 更新 51 | * new: 优化动态添加路由功能 52 | 53 | 以前的动态路由功能并不完善,首先要将所有的路由都添加到路由表里,然后根据后台返回的菜单栏数据来生成菜单。 54 | 55 | 导致的问题就是,虽然有页面在菜单栏上不显示,但由于已经添加到路由表里了,所以可以在地址栏上手动输入在菜单栏上不存在(但在路由表存在)的页面,进而可以越权访问。 56 | 57 | 现在除了必要的页面需要在一开始添加到路由表里,其他的页面都可以根据后台数据来自动生成。而且菜单栏上没有的页面,在地址栏上输入地址也是访问不了的。 58 | 59 | 另外,如果在未登陆时要访问某一指定页面,会重定向到登陆页,登陆成功后会自动跳到这个指定页面。 60 | 61 | 具体实现请看 `permission.js` 和 `util` 目录下的 `index.js` 文件 62 | 63 | ### 2019.3.14 更新 64 | 65 | * new: 增加404页面 66 | 67 | 假如跳转到一个不存在的页面时会重定向到404页面 68 | 69 | ### 2019.3.8 更新 70 | 71 | * new: 增加面包屑功能 用于展示当前页面的路径 72 | 73 | * new: 增加权限控制功能,如果未登陆,访问所有页面都重定向到登陆页 74 | 75 | ### 2019.3.1 更新 76 | * new: 增加动态菜单栏功能 77 | 78 | 菜单项中的 `icon` 使用的是 `iview` 组件的 `icon` 组件。 79 | 80 | 数据格式: 81 | ```js 82 | // 左侧菜单栏数据 83 | menuItems: [ 84 | { 85 | name: 'Home', // 要跳转的路由名称 不是路径 86 | size: 18, // icon大小 非必填 87 | type: 'md-home', // icon类型 非必填 88 | text: '主页' // 文本内容 89 | }, 90 | { 91 | text: '二级菜单', 92 | type: 'ios-paper', 93 | children: [ 94 | { 95 | type: 'ios-grid', 96 | name: 'T1', 97 | text: '表格', 98 | hidden: true, // 可以在菜单中隐藏此菜单项,但还是可以访问此页面,只是不能在菜单栏中看见。 99 | }, 100 | { 101 | text: '三级菜单', 102 | type: 'ios-paper', 103 | children: [ 104 | { 105 | type: 'ios-notifications-outline', 106 | name: 'Msg', 107 | text: '查看消息' 108 | }, 109 | { 110 | type: 'md-lock', 111 | name: 'Password', 112 | text: '修改密码' 113 | }, 114 | { 115 | type: 'md-person', 116 | name: 'UserInfo', 117 | text: '基本资料', 118 | } 119 | ] 120 | } 121 | ] 122 | } 123 | ] 124 | ``` 125 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | proxy: { 4 | '/api': { 5 | target: 'http://xxxx/device/', // 对应自己的接口 6 | changeOrigin: true, 7 | ws: true, 8 | pathRewrite: { 9 | '^/api': '', 10 | }, 11 | }, 12 | }, 13 | }, 14 | publicPath: './', 15 | } --------------------------------------------------------------------------------