├── .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 | 
108 | 
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 |
2 |
8 |
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 |
2 |
3 |
![]()
4 |
未到找指定页面
5 |
6 |
7 |
8 |
9 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
{{crumbs}}
89 |
90 |
91 |
92 |
96 |
97 |
98 |
![]()
99 |
100 |
101 |
102 |
103 | {{userName}}
104 |
105 |
106 |
107 |
108 |
109 | 修改密码
110 | 基本资料
111 | 退出登陆
112 |
113 |
114 |
115 |
116 |
117 |
118 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
516 |
517 |
--------------------------------------------------------------------------------
/src/components/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
WELCOME
5 |
9 |
14 |
15 |
注册账号 | 忘记密码
16 |
17 |
18 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
30 |
31 |
--------------------------------------------------------------------------------
/src/views/Msg.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 查看消息
4 |
5 |
6 |
7 |
8 |
13 |
14 |
17 |
--------------------------------------------------------------------------------
/src/views/Other.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 单独的路由
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 |
21 |
--------------------------------------------------------------------------------
/src/views/Password.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 修改密码
4 |
5 |
6 |
7 |
8 |
18 |
19 |
22 |
--------------------------------------------------------------------------------
/src/views/T1.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 查询:
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
281 |
282 |
--------------------------------------------------------------------------------
/src/views/UserInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | 基本资料
4 |
5 |
6 |
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 | }
--------------------------------------------------------------------------------