├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── directoryTree.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── http │ │ ├── apiUrl.js │ │ ├── index.js │ │ └── service.js │ ├── images │ │ ├── 404.png │ │ ├── 404_cloud.png │ │ └── logo.png │ ├── mock │ │ └── index.js │ └── utils │ │ ├── get-page-title.js │ │ ├── index.js │ │ ├── token.js │ │ └── validate.js ├── components │ ├── Breadcrumb │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ └── SvgIcon │ │ └── index.vue ├── icons │ ├── index.js │ ├── svg │ │ ├── account.svg │ │ ├── dashboard.svg │ │ ├── drink.svg │ │ ├── example.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── form.svg │ │ ├── link.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── table.svg │ │ ├── tree.svg │ │ └── user.svg │ └── svgo.yml ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── Logo.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ ├── TagsView │ │ │ ├── ScrollPane.vue │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── permission.js ├── router │ ├── index.js │ └── modules │ │ └── table.js ├── settings.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── permission.js │ │ ├── settings.js │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── element-ui.scss │ ├── index.scss │ ├── login.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── table │ │ └── demo.scss │ ├── transition.scss │ └── variables.scss └── views │ ├── 404 │ └── index.vue │ ├── homepage │ └── index.vue │ ├── login │ ├── index.vue │ ├── register.vue │ └── resetPsw.vue │ ├── redirect │ └── index.vue │ └── table │ ├── demo.vue │ └── demoTest.vue └── vue.config.js /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/.eslintignore -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | //it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | "vue/max-attributes-per-line": [2, { 18 | "singleline": 10, 19 | "multiline": { 20 | "max": 1, 21 | "allowFirstLine": false 22 | } 23 | }], 24 | "vue/singleline-html-element-content-newline": "off", 25 | "vue/multiline-html-element-content-newline":"off", 26 | "vue/name-property-casing": ["error", "PascalCase"], 27 | "vue/no-v-html": "off", 28 | 'accessor-pairs': 2, 29 | 'arrow-spacing': [2, { 30 | 'before': true, 31 | 'after': true 32 | }], 33 | 'block-spacing': [2, 'always'], 34 | 'brace-style': [2, '1tbs', { 35 | 'allowSingleLine': true 36 | }], 37 | 'camelcase': [0, { 38 | 'properties': 'always' 39 | }], 40 | 'comma-dangle': [2, 'never'], 41 | 'comma-spacing': [2, { 42 | 'before': false, 43 | 'after': true 44 | }], 45 | 'comma-style': [2, 'last'], 46 | 'constructor-super': 2, 47 | 'curly': [2, 'multi-line'], 48 | 'dot-location': [2, 'property'], 49 | 'eol-last': 2, 50 | 'eqeqeq': ["error", "always", {"null": "ignore"}], 51 | 'generator-star-spacing': [2, { 52 | 'before': true, 53 | 'after': true 54 | }], 55 | 'handle-callback-err': [2, '^(err|error)$'], 56 | 'indent': [2, 2, { 57 | 'SwitchCase': 1 58 | }], 59 | 'jsx-quotes': [2, 'prefer-single'], 60 | 'key-spacing': [2, { 61 | 'beforeColon': false, 62 | 'afterColon': true 63 | }], 64 | 'keyword-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'new-cap': [2, { 69 | 'newIsCap': true, 70 | 'capIsNew': false 71 | }], 72 | 'new-parens': 2, 73 | 'no-array-constructor': 2, 74 | 'no-caller': 2, 75 | 'no-console': 'off', 76 | 'no-class-assign': 2, 77 | 'no-cond-assign': 2, 78 | 'no-const-assign': 2, 79 | 'no-control-regex': 0, 80 | 'no-delete-var': 2, 81 | 'no-dupe-args': 2, 82 | 'no-dupe-class-members': 2, 83 | 'no-dupe-keys': 2, 84 | 'no-duplicate-case': 2, 85 | 'no-empty-character-class': 2, 86 | 'no-empty-pattern': 2, 87 | 'no-eval': 2, 88 | 'no-ex-assign': 2, 89 | 'no-extend-native': 2, 90 | 'no-extra-bind': 2, 91 | 'no-extra-boolean-cast': 2, 92 | 'no-extra-parens': [2, 'functions'], 93 | 'no-fallthrough': 2, 94 | 'no-floating-decimal': 2, 95 | 'no-func-assign': 2, 96 | 'no-implied-eval': 2, 97 | 'no-inner-declarations': [2, 'functions'], 98 | 'no-invalid-regexp': 2, 99 | 'no-irregular-whitespace': 2, 100 | 'no-iterator': 2, 101 | 'no-label-var': 2, 102 | 'no-labels': [2, { 103 | 'allowLoop': false, 104 | 'allowSwitch': false 105 | }], 106 | 'no-lone-blocks': 2, 107 | 'no-mixed-spaces-and-tabs': 2, 108 | 'no-multi-spaces': 2, 109 | 'no-multi-str': 2, 110 | 'no-multiple-empty-lines': [2, { 111 | 'max': 1 112 | }], 113 | 'no-native-reassign': 2, 114 | 'no-negated-in-lhs': 2, 115 | 'no-new-object': 2, 116 | 'no-new-require': 2, 117 | 'no-new-symbol': 2, 118 | 'no-new-wrappers': 2, 119 | 'no-obj-calls': 2, 120 | 'no-octal': 2, 121 | 'no-octal-escape': 2, 122 | 'no-path-concat': 2, 123 | 'no-proto': 2, 124 | 'no-redeclare': 2, 125 | 'no-regex-spaces': 2, 126 | 'no-return-assign': [2, 'except-parens'], 127 | 'no-self-assign': 2, 128 | 'no-self-compare': 2, 129 | 'no-sequences': 2, 130 | 'no-shadow-restricted-names': 2, 131 | 'no-spaced-func': 2, 132 | 'no-sparse-arrays': 2, 133 | 'no-this-before-super': 2, 134 | 'no-throw-literal': 2, 135 | 'no-trailing-spaces': 2, 136 | 'no-undef': 2, 137 | 'no-undef-init': 2, 138 | 'no-unexpected-multiline': 2, 139 | 'no-unmodified-loop-condition': 2, 140 | 'no-unneeded-ternary': [2, { 141 | 'defaultAssignment': false 142 | }], 143 | 'no-unreachable': 2, 144 | 'no-unsafe-finally': 2, 145 | 'no-unused-vars': [2, { 146 | 'vars': 'all', 147 | 'args': 'none' 148 | }], 149 | 'no-useless-call': 2, 150 | 'no-useless-computed-key': 2, 151 | 'no-useless-constructor': 2, 152 | 'no-useless-escape': 0, 153 | 'no-whitespace-before-property': 2, 154 | 'no-with': 2, 155 | 'one-var': [2, { 156 | 'initialized': 'never' 157 | }], 158 | 'operator-linebreak': [2, 'after', { 159 | 'overrides': { 160 | '?': 'before', 161 | ':': 'before' 162 | } 163 | }], 164 | 'padded-blocks': [2, 'never'], 165 | 'quotes': [2, 'single', { 166 | 'avoidEscape': true, 167 | 'allowTemplateLiterals': true 168 | }], 169 | 'semi': [2, 'never'], 170 | 'semi-spacing': [2, { 171 | 'before': false, 172 | 'after': true 173 | }], 174 | 'space-before-blocks': [2, 'always'], 175 | 'space-before-function-paren': [2, 'never'], 176 | 'space-in-parens': [2, 'never'], 177 | 'space-infix-ops': 2, 178 | 'space-unary-ops': [2, { 179 | 'words': true, 180 | 'nonwords': false 181 | }], 182 | 'spaced-comment': [2, 'always', { 183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 184 | }], 185 | 'template-curly-spacing': [2, 'never'], 186 | 'use-isnan': 2, 187 | 'valid-typeof': 2, 188 | 'wrap-iife': [2, 'any'], 189 | 'yield-star-spacing': [2, 'both'], 190 | 'yoda': [2, 'never'], 191 | 'prefer-const': 2, 192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 193 | 'object-curly-spacing': [2, 'always', { 194 | objectsInObjects: false 195 | }], 196 | 'array-bracket-spacing': [2, 'never'] 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuecli3-ele-admin-template 2 | :star2: 一个基于vuecli3和vue-admin-template改造的响应式后台管理系统 3 | 4 | #### 软件架构 5 | vue-cli3 + element-UI 6 | 7 | #### 安装教程 8 | 9 | npm install 10 | 11 | #### 使用说明 12 | 13 | npm run serve 14 | 15 | #### 编译 16 | 17 | npm run build 18 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /directoryTree.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/directoryTree.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuecli3-ele-admin-template", 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 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.19.0", 13 | "babel-polyfill": "^6.26.0", 14 | "core-js": "^2.6.5", 15 | "es6-promise": "^4.2.8", 16 | "js-cookie": "^2.2.0", 17 | "lib-flexible": "^0.3.2", 18 | "normalize.css": "^8.0.1", 19 | "nprogress": "^0.2.0", 20 | "vue": "^2.6.10", 21 | "vue-router": "^3.0.3", 22 | "vuex": "^3.0.1", 23 | "vuex-persistedstate": "^2.5.4" 24 | }, 25 | "devDependencies": { 26 | "@vue/cli-plugin-babel": "^3.10.0", 27 | "@vue/cli-plugin-eslint": "^3.10.0", 28 | "@vue/cli-service": "^3.10.0", 29 | "@vue/eslint-config-prettier": "^5.0.0", 30 | "autoprefixer": "^9.6.1", 31 | "babel-eslint": "^10.0.1", 32 | "element-ui": "^2.9.1", 33 | "eslint": "^5.16.0", 34 | "eslint-plugin-prettier": "^3.1.0", 35 | "eslint-plugin-vue": "^5.0.0", 36 | "mockjs": "^1.0.1-beta3", 37 | "node-sass": "^4.9.0", 38 | "postcss-plugin-px2rem": "^0.8.1", 39 | "prettier": "^1.18.2", 40 | "px2rem-loader": "^0.1.9", 41 | "sass-loader": "^7.1.0", 42 | "svg-sprite-loader": "^3.8.0", 43 | "svgo": "1.0.5", 44 | "vue-cli-plugin-element": "^1.0.1", 45 | "vue-template-compiler": "^2.6.10" 46 | }, 47 | "browserslist": [ 48 | "> 1%", 49 | "last 2 versions", 50 | "not ie <= 8", 51 | "iOS >= 8", 52 | "Firefox >= 20", 53 | "Android >= 4.4" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | vuecli3-ele-admin-template 14 | 15 | 16 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /src/assets/http/apiUrl.js: -------------------------------------------------------------------------------- 1 | /* 全局定义接口url */ 2 | 3 | // host头,这里我们要使用代理,所以定义的字符串apiReplace是用来进行反向代理时的标记字符串。 4 | const apiHost = '/apiReplace/' 5 | // 密码登录 6 | const Login = `${apiHost}login` 7 | // 短信登录 8 | const LoginByVin = `${apiHost}loginByVin` 9 | // 发送短信 10 | const SendSms = `${apiHost}sendSms` 11 | // 表格模拟数据 12 | const GetPermissionData = `${apiHost}permission` 13 | // 表格模拟数据 14 | const GetTableData = `${apiHost}table` 15 | 16 | export default { 17 | Login, 18 | LoginByVin, 19 | SendSms, 20 | GetPermissionData, 21 | GetTableData 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/http/index.js: -------------------------------------------------------------------------------- 1 | /* 封装axios请求 */ 2 | /* 用法示例:(*)为必须参数 3 | this.$request.httpRequest({ 4 | headers: false, // 是否格式化参数 5 | (*)method: 'post', // 请求方式,post或get 6 | (*)url: this.API.ResetPassword, // 请求地址,请求地址的配置在@/api/apiUrl.js 7 | noLoading: true, // 是否显示全局Loading遮罩,默认每个请求都显示遮罩,即默认不设置该参数。如果需要某个请求不加遮罩,就设置noLoading: true即可 8 | returnFullData: true, // 是否返回完整数据,例如接口返回的数据格式为{ code:0, data: [], meaasge:''},则默认请求成功之后的回调函数的参数为data:[],如果设置returnFullData: true,则回调参数为{ code:0, data: [], meaasge:''} 9 | hideErrorMsg: true, // 是否展示错误提示 10 | (*)params: {}, // 请求参数,object类型 11 | (*)success: (data) => { // 请求成功之后的回调函数,data是回调参数 12 | // 在这里写请求成功后的逻辑 13 | }, 14 | error: (err) => { 请求不成功之后的回调函数,data是回调参数 15 | // 在这里写请求报错后的逻辑 16 | } 17 | }) 18 | */ 19 | import service from './service' 20 | import { Message, Loading } from 'element-ui' 21 | import Qs from 'qs' 22 | 23 | function requestMethods(options) { 24 | return new Promise((resolve, reject) => { 25 | try { 26 | switch (options.method) { 27 | case 'post': 28 | if (options.headers) { 29 | resolve( 30 | service({ 31 | url: options.url, 32 | method: 'post', 33 | data: options.params 34 | }) 35 | ) 36 | } else { 37 | resolve( 38 | service({ 39 | url: options.url, 40 | method: 'post', 41 | data: Qs.stringify(options.params) 42 | }) 43 | ) 44 | } 45 | break 46 | case 'get': 47 | resolve( 48 | service({ 49 | url: options.url, 50 | method: 'get', 51 | params: options.params 52 | }) 53 | ) 54 | break 55 | default: // 默认是get调用 56 | resolve( 57 | service({ 58 | url: options.url, 59 | method: 'get', 60 | params: options.params 61 | }) 62 | ) 63 | break 64 | } 65 | } catch (e) { 66 | Message({ 67 | message: 'HTTP请求方法出错!', 68 | type: 'error', 69 | duration: 3 * 1000 70 | }) 71 | reject('methods error!') 72 | } 73 | }) 74 | } 75 | 76 | function httpRequest(options = {}) { 77 | let loading 78 | if (!options.noLoading) { 79 | // 启用全局loading 80 | loading = Loading.service({ 81 | lock: true, 82 | text: '加载中...', 83 | spinner: 'el-icon-loading', 84 | background: 'rgba(0, 0, 0, 0.7)' 85 | }) 86 | } 87 | 88 | try { 89 | requestMethods(options).then(response => { 90 | // 成功返回结果的逻辑。根据接口定义的数据返回格式 修改判断条件 91 | const data = response.data 92 | if (data.resultCode === '1' || data.resultCode === 1) { 93 | // 成功 94 | const result = options.returnFullData ? data : data.data // 返回完整数据结构还是只返回有效数据 95 | options.success(result) 96 | } else { 97 | if (!options.hideErrorMsg) { 98 | // 失败 99 | let errorMsg = data.hasOwnProperty('resultMessage') ? data.resultMessage : '数据解析错误' 100 | switch (data.resultCode) { 101 | case '401': 102 | errorMsg = '暂无操作权限' 103 | break 104 | } 105 | Message.closeAll() 106 | Message({ 107 | message: errorMsg, 108 | type: 'error', 109 | customClass: 'errorloginwidth', 110 | duration: 3000 111 | }) 112 | } 113 | options.error(data) 114 | } 115 | if (!options.noLoading) { 116 | // loading完毕 117 | loading.close() 118 | } 119 | }).catch(e => { 120 | options.error(e.response) 121 | }) 122 | } catch (e) { 123 | Message({ 124 | message: 'Axios请求出错!', 125 | type: 'error', 126 | duration: 3 * 1000 127 | }) 128 | } 129 | } 130 | export default { 131 | httpRequest 132 | } 133 | -------------------------------------------------------------------------------- /src/assets/http/service.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Message } from 'element-ui' 3 | import { getToken } from '@/assets/utils/token' 4 | import router from '@/router' 5 | import store from '@/store' 6 | 7 | // 创建axios实例 8 | const service = axios.create({ 9 | baseURL: process.env.BASE_API, // api 的 base_url 10 | timeout: 10000 // 请求超时时间 11 | }) 12 | 13 | // request拦截器 14 | service.interceptors.request.use( 15 | config => { 16 | // 在此处设置请求头参数 17 | const token = getToken() 18 | if (token != null) { 19 | config.headers['Authorization'] = token 20 | } 21 | return config 22 | }, 23 | error => { 24 | // Do something with request error 25 | console.log(error) // for debug 26 | return Promise.reject(error) 27 | } 28 | ) 29 | 30 | // axios response 拦截器 31 | service.interceptors.response.use( 32 | response => { 33 | return response // 返回请求成功结果,status=200 34 | }, 35 | err => { 36 | // 请求失败时,即status!=200 37 | if (err && err.response) { 38 | switch (err.response.status) { 39 | case 400: 40 | err.message = '错误请求' 41 | break 42 | case 401: 43 | err.message = '未授权,请重新登录' 44 | break 45 | case 403: 46 | err.message = '禁止访问' 47 | break 48 | case 404: 49 | err.message = '请求错误,未找到该资源' 50 | break 51 | case 405: 52 | err.message = '请求方法未允许' 53 | break 54 | case 408: 55 | err.message = '请求超时' 56 | break 57 | case 413: 58 | err.message = '上传文件过大' 59 | break 60 | case 500: 61 | err.message = '服务器端出错' 62 | break 63 | case 501: 64 | err.message = '网络未实现' 65 | break 66 | case 502: 67 | err.message = '网络错误' 68 | break 69 | case 503: 70 | err.message = '服务不可用' 71 | break 72 | case 504: 73 | err.message = '网络超时' 74 | break 75 | case 505: 76 | err.message = 'http版本不支持该请求' 77 | break 78 | default: 79 | err.message = `连接错误,${err.response.msg}` 80 | } 81 | } else { 82 | err.message = '当前网络状态不佳' 83 | } 84 | Message.closeAll() 85 | Message({ 86 | message: err.message || '数据解析出错', 87 | type: 'error', 88 | customClass: 'errorloginwidth', 89 | duration: '3000' 90 | }) 91 | if (err.response && err.response.status === 401) { 92 | store.dispatch('FedLogOut') // 前端登出,移除token 93 | router.replace({ 94 | path: `/login?redirect=${window.location.href.split(/[#]/g)[1]}` 95 | }) 96 | } 97 | return Promise.reject(err) 98 | } 99 | ) 100 | 101 | export default service 102 | -------------------------------------------------------------------------------- /src/assets/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/src/assets/images/404.png -------------------------------------------------------------------------------- /src/assets/images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/src/assets/images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccccai/vuecli3-ele-admin-template/c5d8b2d7571ec90b6ed3ee84e6aa2a42f3969301/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/mock/index.js: -------------------------------------------------------------------------------- 1 | /* mock配置文件 2 | ps:mock模块会影响原生的ajax请求,使得服务器返回的blob类型变成乱码, 3 | 所以如果在代码中有使用blob时,需要在mainjs中把mock注释掉,才能正常使用 4 | */ 5 | // 引入mockjs 6 | import Mock from 'mockjs' 7 | // 获取 mock.Random 对象 8 | const Random = Mock.Random 9 | const result = { 10 | data: {}, 11 | resultCode: 1, 12 | resultMessage: 'success' 13 | } 14 | // mock一组数据 15 | const loginData = () => { 16 | result.data = { 17 | token: Random.string(10), 18 | userInfo: { 19 | name: 'cc', 20 | roles: [ 21 | { 22 | id: 2, 23 | role: 'superAdmin' 24 | } 25 | ] 26 | } 27 | } 28 | return result 29 | } 30 | 31 | const tableData = () => { 32 | const length = Random.integer(1, 20) 33 | const data = { 34 | totalCount: length, 35 | currentPage: 1, 36 | data: [] 37 | } 38 | for (let i = 0; i < length; i++) { 39 | data.data[i] = { 40 | id: Random.id(), 41 | name: Random.cname(), 42 | age: Random.integer(1, 100), 43 | gender: Random.cword('男女'), 44 | phone: `1${Random.integer(1000000000, 9999999999)}` 45 | } 46 | } 47 | result.data = data 48 | return result 49 | } 50 | 51 | const smsData = () => { 52 | result.data = { 53 | code: Random.natural(1000, 9999) 54 | } 55 | return result 56 | } 57 | 58 | const permissionData = () => { 59 | result.data = [ 60 | { 61 | name: 'Table', 62 | children: [ 63 | { 64 | name: 'TableDemo', 65 | auth: { 66 | add: true, 67 | check: true, 68 | delete: true, 69 | edit: true 70 | } 71 | } 72 | ] 73 | } 74 | ] 75 | return result 76 | } 77 | Mock.mock('/apiReplace/login', 'post', loginData) 78 | Mock.mock('/apiReplace/sendSms', 'post', smsData) 79 | Mock.mock('/apiReplace/permission', 'post', permissionData) 80 | Mock.mock('/apiReplace/table', 'post', tableData) 81 | -------------------------------------------------------------------------------- /src/assets/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || '奔腾开放云平台' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/utils/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /** 4 | * 公共基础工具类 5 | */ 6 | 7 | // 判断是否为空 8 | export function isNotEmpty(obj) { 9 | try { 10 | if (obj == null || obj == undefined) { 11 | return false 12 | } 13 | //判断数字是否是NaN 14 | if (typeof obj === 'number') { 15 | if (isNaN(obj)) { 16 | return false 17 | } else { 18 | return true 19 | } 20 | } 21 | //判断参数是否是布尔、函数、日期、正则,是则返回true 22 | if ( 23 | typeof obj === 'boolean' || 24 | typeof obj === 'function' || 25 | obj instanceof Date || 26 | obj instanceof RegExp 27 | ) { 28 | return true 29 | } 30 | //判断参数是否是字符串,去空,如果长度为0则返回false 31 | if (typeof obj === 'string') { 32 | if (obj.trim().length == 0) { 33 | return false 34 | } else { 35 | return true 36 | } 37 | } 38 | 39 | if (typeof obj === 'object') { 40 | //判断参数是否是数组,数组为空则返回false 41 | if (obj instanceof Array) { 42 | if (obj.length == 0) { 43 | return false 44 | } else { 45 | return true 46 | } 47 | } 48 | 49 | //判断参数是否是对象,判断是否是空对象,是则返回false 50 | if (obj instanceof Object) { 51 | //判断对象属性个数 52 | if (Object.getOwnPropertyNames(obj).length == 0) { 53 | return false 54 | } else { 55 | return true 56 | } 57 | } 58 | } 59 | } catch (e) { 60 | console.log(e) 61 | return false 62 | } 63 | } 64 | 65 | // 组装菜单 66 | export function formattingPermission(data) { 67 | let tmpData = [] 68 | const defaultExpanded = [] 69 | if (Object.prototype.toString.call(data) !== '[object Array]') { 70 | return { defaultExpanded: defaultExpanded, data: tmpData } 71 | } 72 | const level1 = [], level2 = [], level3 = [] 73 | // 组装数据,拼接成elementUI树形控件支持的形式 74 | data.forEach(item => { 75 | switch (item.level) { 76 | case 1 : 77 | level1.push({...item, children: []}) 78 | defaultExpanded.push(item.id) // 默认全部一级菜单是展开状态 79 | break 80 | case 2: 81 | level2.push({...item, children: []}) 82 | break 83 | case 3: 84 | level3.push({...item}) 85 | break 86 | } 87 | }) 88 | 89 | level3.forEach((lastLevelItem, index) => { 90 | level2.forEach((secondLevelItem, i) => { 91 | if (secondLevelItem.id === lastLevelItem.parentId) { 92 | level2[i].children.push({ 93 | ...lastLevelItem 94 | }) 95 | } 96 | }) 97 | }) 98 | 99 | level2.forEach((secondLevelItem, index) => { 100 | level1.forEach((firstLevelItem, i) => { 101 | if (firstLevelItem.id === secondLevelItem.parentId) { 102 | level1[i].children.push({ 103 | ...secondLevelItem 104 | }) 105 | } 106 | }) 107 | }) 108 | 109 | tmpData = [...level1] 110 | return { defaultExpanded: defaultExpanded, data: tmpData } 111 | } 112 | 113 | // 将数组装换成字符串类型 114 | export function arrayToString(arr) { 115 | if (Object.prototype.toString.call(arr) !== '[object Array]') { 116 | return 117 | } 118 | const length = arr.length 119 | if (length === 0) { 120 | return '' 121 | } else { 122 | return JSON.stringify(arr) 123 | } 124 | } 125 | 126 | export function parseTime(time, cFormat) { 127 | if (arguments.length === 0) { 128 | return null 129 | } 130 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 131 | let date 132 | if (typeof time === 'object') { 133 | date = time 134 | } else { 135 | if (('' + time).length === 10) time = parseInt(time) * 1000 136 | date = new Date(time) 137 | } 138 | const formatObj = { 139 | y: date.getFullYear(), 140 | m: date.getMonth() + 1, 141 | d: date.getDate(), 142 | h: date.getHours(), 143 | i: date.getMinutes(), 144 | s: date.getSeconds(), 145 | a: date.getDay() 146 | } 147 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 148 | let value = formatObj[key] 149 | // Note: getDay() returns 0 on Sunday 150 | if (key === 'a') { 151 | return ['日', '一', '二', '三', '四', '五', '六'][value] 152 | } 153 | if (result.length > 0 && value < 10) { 154 | value = '0' + value 155 | } 156 | return value || 0 157 | }) 158 | return time_str 159 | } 160 | 161 | export function formatTime(time, option) { 162 | time = +time * 1000 163 | const d = new Date(time) 164 | const now = Date.now() 165 | 166 | const diff = (now - d) / 1000 167 | 168 | if (diff < 30) { 169 | return '刚刚' 170 | } else if (diff < 3600) { 171 | // less 1 hour 172 | return Math.ceil(diff / 60) + '分钟前' 173 | } else if (diff < 3600 * 24) { 174 | return Math.ceil(diff / 3600) + '小时前' 175 | } else if (diff < 3600 * 24 * 2) { 176 | return '1天前' 177 | } 178 | if (option) { 179 | return parseTime(time, option) 180 | } else { 181 | return ( 182 | d.getMonth() + 183 | 1 + 184 | '月' + 185 | d.getDate() + 186 | '日' + 187 | d.getHours() + 188 | '时' + 189 | d.getMinutes() + 190 | '分' 191 | ) 192 | } 193 | } 194 | 195 | export function isExternal(path) { 196 | return /^(https?:|mailto:|tel:)/.test(path) 197 | } 198 | 199 | export function getUrlParam(name) { 200 | const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)') 201 | const r = window.location.search.substr(1).match(reg) 202 | if (r !== null) { 203 | return unescape(r[2]) 204 | } 205 | return null 206 | } 207 | -------------------------------------------------------------------------------- /src/assets/utils/token.js: -------------------------------------------------------------------------------- 1 | /** 2 | * token本地存储 3 | */ 4 | 5 | const TokenKey = 'User-Token' 6 | 7 | export function getToken() { 8 | return localStorage.getItem(TokenKey) 9 | } 10 | 11 | export function setToken(token) { 12 | return localStorage.setItem(TokenKey, token) 13 | } 14 | 15 | export function removeToken() { 16 | return localStorage.removeItem(TokenKey) 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/utils/validate.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /** 4 | * 表单验证工具类 5 | */ 6 | 7 | export function isvalidPositiveFloat(str) { 8 | // 正浮点数,不包括0 9 | const strRegex = /^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$/ 10 | return strRegex.test(str) 11 | } 12 | 13 | export function isvalidPositiveNumber(str) { 14 | // 正整数,不包括0 15 | const strRegex = /^[0-9]*[1-9][0-9]*$/ 16 | return strRegex.test(str) 17 | } 18 | 19 | /* 合法手机号码 */ 20 | export function isvalidPhoneNumber(str) { 21 | const phoneRegex = /^1[34578]\d{9}$/ 22 | return phoneRegex.test(str) 23 | } 24 | 25 | /* 合法密码 */ 26 | export function isvalidPassword(str) { 27 | // 字符或字母6-20位,不考虑全为数字和全为字符情况 28 | // const passwordRegex = /^[0-9a-zA-Z]{6,20}$/ // 不能保证不能是纯数字或纯英文 29 | const passwordRegex = /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$/ 30 | return passwordRegex.test(str) 31 | } 32 | 33 | /* 四位数字*/ 34 | export function isvalidCode(str) { 35 | const codeRegex = /^\d{6}$/ 36 | return codeRegex.test(str) 37 | } 38 | 39 | /* 合法uri*/ 40 | export function validateURL(textval) { 41 | const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 42 | return urlregex.test(textval) 43 | } 44 | 45 | /* 小写字母*/ 46 | export function validateLowerCase(str) { 47 | const reg = /^[a-z]+$/ 48 | return reg.test(str) 49 | } 50 | 51 | /* 大写字母*/ 52 | export function validateUpperCase(str) { 53 | const reg = /^[A-Z]+$/ 54 | return reg.test(str) 55 | } 56 | 57 | /* 大小写字母*/ 58 | export function validatAlphabets(str) { 59 | const reg = /^[A-Za-z]+$/ 60 | return reg.test(str) 61 | } 62 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 55 | 56 | 69 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | 35 | 44 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/icons/svg/account.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drink.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 82 | 83 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 50 | 51 | 59 | -------------------------------------------------------------------------------- /src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 64 | 65 | 86 | 87 | 180 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 43 | 44 | 103 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | 125 | 126 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 57 | -------------------------------------------------------------------------------- /src/layout/components/TagsView/ScrollPane.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 69 | 70 | 86 | -------------------------------------------------------------------------------- /src/layout/components/TagsView/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 201 | 202 | 270 | 271 | 296 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | export { default as TagsView } from './TagsView' 5 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 52 | 53 | 94 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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 ElementUI from 'element-ui' 6 | import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n 7 | import 'element-ui/lib/theme-chalk/index.css' 8 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 9 | import '@/styles/index.scss' // global css 10 | import 'element-ui/lib/theme-chalk/index.css' 11 | import '@/icons' // icon 12 | import '@/assets/mock' 13 | import '@/permission' // permission control 14 | import 'lib-flexible' // 使用lib-flexible来解决移动端适配 15 | // 解决低版本浏览器不支持promise问题 16 | import 'babel-polyfill' 17 | import Es6Promise from 'es6-promise' 18 | Es6Promise.polyfill() 19 | 20 | import Api from '@/assets/http/apiUrl' 21 | import Request from '@/assets/http' 22 | 23 | Vue.config.productionTip = false 24 | Vue.use(ElementUI, { 25 | locale 26 | }) 27 | Vue.prototype.API = Api 28 | Vue.prototype.$request = Request 29 | 30 | new Vue({ 31 | router, 32 | store, 33 | render: h => h(App) 34 | }).$mount('#app') 35 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from '@/router' 2 | import store from '@/store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // Progress 进度条 5 | import 'nprogress/nprogress.css'// Progress 进度条样式 6 | import getPageTitle from '@/assets/utils/get-page-title' 7 | 8 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 9 | 10 | const whiteList = ['/login', '/register', '/resetPsw'] // 不重定向白名单 11 | 12 | router.beforeEach(async(to, from, next) => { 13 | NProgress.start() 14 | 15 | // set page title 16 | document.title = getPageTitle(to.meta.title) 17 | 18 | // 有无token判断 19 | const token = localStorage.getItem('ADMIN_TOKEN') 20 | if (token) { 21 | if (whiteList.includes(to.path)) { 22 | next() 23 | NProgress.done() 24 | } else { 25 | // 判断当前用户是不是进行了刷新操作,防止进入死循环,如果存在就表示正常跳转,如果不存在就表示刷新了,vuex中的状态丢失了,需要重新挂载路由 26 | const hasUser = store.state.user.token 27 | if (hasUser) { 28 | next() 29 | } else { 30 | try { 31 | // 防止进入死循环 32 | await store.commit('SET_TOKEN', token) 33 | // 是不是超级管理员 34 | const isSuperAdmin = store.state.user.roles.some(item => item.id === 1) 35 | const accessRoutes = await store.dispatch('GenerateRoutes', isSuperAdmin) 36 | // 异步加载路由 37 | router.addRoutes(accessRoutes) 38 | router.options.routes = store.state.permission.routes 39 | // 设置replace:true,导航不会留下历史记录 40 | next({ ...to, replace: true }) 41 | } catch (error) { 42 | // 移除token,重定向到登录页 43 | await store.dispatch('ResetToken') 44 | Message.error(error || '身份验证出错,请重新登录。') 45 | next(`/login?redirect=${to.path}`) 46 | NProgress.done() 47 | } 48 | } 49 | } 50 | } else { 51 | // 没有token 52 | if (whiteList.indexOf(to.path) !== -1) { 53 | next() 54 | } else { 55 | // next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页 56 | next('/login') // 否则全部重定向到登录页 57 | NProgress.done() 58 | } 59 | } 60 | }) 61 | 62 | router.afterEach(() => { 63 | NProgress.done() // 结束Progress 64 | }) 65 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export const constantRoutes = [ 7 | { 8 | path: '/redirect', 9 | component: () => import('@/layout'), 10 | hidden: true, 11 | children: [ 12 | { 13 | path: '/redirect/:path*', 14 | component: resolve => void require(['@/views/redirect/index'], resolve) 15 | } 16 | ] 17 | }, 18 | { 19 | path: '/', 20 | redirect: '/home' 21 | }, 22 | { 23 | path: '/login', 24 | name: 'Login', 25 | component: resolve => void require(['@/views/login/index'], resolve), 26 | hidden: true 27 | }, 28 | { 29 | path: '/register', 30 | name: 'Register', 31 | component: resolve => void require(['@/views/login/register'], resolve), 32 | hidden: true 33 | }, 34 | { 35 | path: '/resetPsw', 36 | name: 'ResetPsw', 37 | component: resolve => void require(['@/views/login/resetPsw'], resolve), 38 | hidden: true 39 | }, 40 | 41 | { 42 | path: '/404', 43 | component: () => import('@/views/404'), 44 | hidden: true 45 | }, 46 | { 47 | path: '/home', 48 | component: () => import('@/layout'), 49 | hidden: true, 50 | children: [ 51 | { 52 | path: '/home', 53 | name: 'Home', 54 | component: resolve => void require(['@/views/homepage/index'], resolve), 55 | meta: { 56 | title: '主页', 57 | keepAlive: false, // 该字段表示该页面需要缓存 58 | isBack: false // 用于判断上一个页面是哪个 59 | } 60 | } 61 | ] 62 | } 63 | ] 64 | 65 | /** 66 | * 异步挂载的路由 67 | * 动态需要根据权限加载的路由表 68 | */ 69 | const modulesFiles = require.context('./modules', true, /\.js$/) 70 | const routesModules = [] 71 | // 自动引入modules目录下的所有模块 72 | modulesFiles.keys().reduce((modules, modulePath) => { 73 | const value = modulesFiles(modulePath) 74 | routesModules.push(value.default) 75 | }, {}) 76 | export const asyncRoutes = routesModules 77 | 78 | /** 79 | * 最终无法匹配到相应路由,重定向到404 80 | * 异步加载路由时,在生成完异步路由准备挂载时,需要将重定向404的匹配规则定义在最后面,否则刷新会出错。 81 | */ 82 | export const notFoundRoutes = [ 83 | { 84 | path: '*', 85 | redirect: '/404', 86 | hidden: true, 87 | meta: { 88 | title: '404' 89 | } 90 | } 91 | ] 92 | 93 | const createRouter = () => new Router({ 94 | // mode: 'history', // require service support 95 | scrollBehavior: () => ({ y: 0 }), 96 | routes: constantRoutes 97 | }) 98 | const router = createRouter() 99 | 100 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 101 | export function resetRouter() { 102 | const newRouter = createRouter() 103 | router.matcher = newRouter.matcher // reset router 104 | } 105 | 106 | export default router 107 | -------------------------------------------------------------------------------- /src/router/modules/table.js: -------------------------------------------------------------------------------- 1 | const table = { 2 | path: 'table', 3 | component: () => import('@/layout'), 4 | redirect: '/table/demo', 5 | name: 'Table', 6 | meta: { 7 | title: 'parentTitle', 8 | icon: 'table' 9 | }, 10 | children: [ 11 | { 12 | path: '/table/demo', 13 | name: 'TableDemo', 14 | component: resolve => void require(['@/views/table/demo'], resolve), 15 | meta: { 16 | title: 'tableDemo' 17 | } 18 | }, 19 | { 20 | path: '/table/demoTest', 21 | name: 'DemoTest', 22 | component: resolve => void require(['@/views/table/demoTest'], resolve), 23 | meta: { 24 | title: 'demoTest' 25 | } 26 | } 27 | ] 28 | } 29 | 30 | export default table 31 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: '后台管理系统', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether fix the header 8 | */ 9 | fixedHeader: false, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description Whether need tagsView 14 | */ 15 | tagsView: true, 16 | 17 | /** 18 | * @type {boolean} true | false 19 | * @description Whether show the logo in sidebar 20 | */ 21 | sidebarLogo: true 22 | } 23 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | visitedViews: state => state.tagsView.visitedViews, 5 | cachedViews: state => state.tagsView.cachedViews 6 | } 7 | export default getters 8 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import createPersistedState from 'vuex-persistedstate' 5 | 6 | Vue.use(Vuex) 7 | 8 | // (创建了)一个包含了modules文件夹(包含子目录)下面的,所有文件名以 `.js` 结尾的、能被 require 请求到的文件的上下文。 9 | const modulesFiles = require.context('./modules', true, /\.js$/) 10 | // keys() 方法用于从modules创建一个包含modules里键值的可迭代对象。 11 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 12 | // 模块名,取文件名 13 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 14 | // 获取键名为modulePath的文件内容 15 | const value = modulesFiles(modulePath) 16 | // 将文件中的默认导出模块赋值给迭代对象modules 17 | modules[moduleName] = value.default 18 | // 返回迭代对象modules 19 | return modules 20 | // 默认值是空对象{} 21 | }, {}) 22 | 23 | const store = new Vuex.Store({ 24 | modules, 25 | getters, 26 | plugins: [ 27 | // 存储vuex状态,使之刷新不丢失 28 | createPersistedState({ 29 | storage: window.localStorage, 30 | reducer(val) { 31 | return { 32 | // 将要存储的state中的值放在这里 33 | user: { 34 | name: val.user.name, 35 | roles: val.user.roles 36 | } 37 | } 38 | } 39 | }) 40 | ] 41 | }) 42 | 43 | export default store 44 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | /** 2 | * constantRoutes 常规路由,不需要权限即可访问 3 | * asyncRoutes 需要访问权限的路由 4 | * notFoundRoutes 404路由 5 | * resetRouter 重置路由的方法 6 | */ 7 | import { asyncRoutes, constantRoutes, notFoundRoutes, resetRouter } from '@/router' 8 | import API from '@/assets/http/apiUrl' 9 | import Request from '@/assets/http' 10 | 11 | const permission = { 12 | state: { 13 | routes: [], 14 | addRoutes: [] // 异步加载的路由 15 | }, 16 | 17 | mutations: { 18 | SET_ROUTES: (state, routes) => { 19 | state.addRoutes = routes 20 | state.routes = constantRoutes.concat(routes) 21 | } 22 | }, 23 | 24 | actions: { 25 | // 获取动态路由 26 | GenerateRoutes({ commit }, isSuperAdmin) { 27 | resetRouter() // 先初始化路由 28 | return new Promise((resolve, reject) => { 29 | // 如果是超级管理员,挂载全部路由全部权限 30 | if (isSuperAdmin) { 31 | // 重定向404的匹配规则需要在整个完整路由定义的最后面,否则刷新会出错。 32 | const accessedRoutes = [...asyncRoutes, ...notFoundRoutes] 33 | accessedRoutes.forEach(item => { 34 | if (item.children) { 35 | // 超级管理员赋全部权限 36 | item.children.forEach(elem => { 37 | elem.meta = { 38 | ...elem.meta, 39 | check: true, 40 | delete: true, 41 | add: true, 42 | edit: true 43 | } 44 | }) 45 | } 46 | }) 47 | commit('SET_ROUTES', accessedRoutes) 48 | resolve(accessedRoutes) 49 | } else { 50 | Request.httpRequest({ 51 | method: 'post', 52 | url: API.GetPermissionData, 53 | noLoading: true, 54 | params: {}, 55 | success: (data) => { 56 | console.log(data) 57 | let accessedRoutes = [] 58 | // 匹配前端路由和后台返回的菜单 59 | accessedRoutes = filterAsyncRoutes(asyncRoutes, data) 60 | // 重定向404的匹配规则需要在整个完整路由定义的最后面,否则刷新会出错。 61 | accessedRoutes.push(...notFoundRoutes) 62 | commit('SET_ROUTES', accessedRoutes) 63 | resolve(accessedRoutes) 64 | }, 65 | error: res => { 66 | reject(res) 67 | } 68 | }) 69 | } 70 | }) 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Filter asynchronous routing tables by recursion 77 | * 匹配后台返回的菜单信息和前端定义的路由 78 | * @param routes 前端定义好的异步路由 79 | * @param menus 后台返回的菜单 80 | */ 81 | export function filterAsyncRoutes(routes = [], menus = []) { 82 | const res = [] 83 | 84 | routes.forEach(route => { 85 | // 复制一遍路由,这样改变tmp的同时路由不会受影响 86 | const tmp = { 87 | ...route 88 | } 89 | 90 | // 是否匹配到了 91 | if (hasPermission(menus, tmp)) { // 有符合的匹配项 92 | // 找出那一条匹配成功的路由项 93 | const findMenu = menus.find((menu, index, menus) => { 94 | return menu.name.includes(tmp.name) 95 | }) 96 | 97 | // 赋权 98 | if (findMenu.hasOwnProperty('auth')) { 99 | tmp.meta = { 100 | ...tmp.meta, 101 | ...findMenu.auth 102 | } 103 | } 104 | 105 | // 如果该路由项中含有子路由,子路由也是需要和菜单进行匹配的 106 | if (findMenu.hasOwnProperty('children') && findMenu.children.length) { 107 | // 子路由匹配的步骤和父路由一样 108 | tmp.children = filterAsyncRoutes(tmp.children, findMenu.children) 109 | } else { 110 | // 将匹配不到的子路由从路由中删除 111 | delete tmp.children 112 | } 113 | 114 | // 最后得到的结果就是和后台返回菜单匹配一致的异步路由值 115 | res.push(tmp) 116 | } 117 | }) 118 | 119 | return res 120 | } 121 | 122 | /** 123 | * Use meta.role to determine if the current user has permission 124 | * @param menus 后台返回的菜单 125 | * @param route 前端定义好的异步路由中的项 126 | */ 127 | function hasPermission(menus, route) { 128 | // 进行匹配 129 | if (route.name) { // 前提是异步路由要存在name 130 | // 匹配的规则是,name要一致,只要匹配到就返回true,停止继续往下循环 131 | return menus.some(menu => route.name.includes(menu.name)) 132 | } else { 133 | return true 134 | } 135 | } 136 | 137 | export default permission 138 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | fixedHeader: fixedHeader, 8 | tagsView: tagsView, 9 | sidebarLogo: sidebarLogo 10 | } 11 | 12 | const mutations = { 13 | CHANGE_SETTING: (state, { key, value }) => { 14 | if (state.hasOwnProperty(key)) { 15 | state[key] = value 16 | } 17 | } 18 | } 19 | 20 | const actions = { 21 | changeSetting({ commit }, data) { 22 | commit('CHANGE_SETTING', data) 23 | } 24 | } 25 | 26 | export default { 27 | namespaced: true, 28 | state, 29 | mutations, 30 | actions 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/store/modules/tagsView.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | visitedViews: [], 3 | cachedViews: [] 4 | } 5 | 6 | const mutations = { 7 | ADD_VISITED_VIEW: (state, view) => { 8 | if (state.visitedViews.some(v => v.path === view.path)) return 9 | state.visitedViews.push( 10 | Object.assign({}, view, { 11 | title: view.meta.title || 'no-name' 12 | }) 13 | ) 14 | }, 15 | ADD_CACHED_VIEW: (state, view) => { 16 | if (state.cachedViews.includes(view.name)) return 17 | if (!view.meta.noCache) { 18 | state.cachedViews.push(view.name) 19 | } 20 | }, 21 | 22 | DEL_VISITED_VIEW: (state, view) => { 23 | for (const [i, v] of state.visitedViews.entries()) { 24 | if (v.path === view.path) { 25 | state.visitedViews.splice(i, 1) 26 | break 27 | } 28 | } 29 | }, 30 | DEL_CACHED_VIEW: (state, view) => { 31 | for (const i of state.cachedViews) { 32 | if (i === view.name) { 33 | const index = state.cachedViews.indexOf(i) 34 | state.cachedViews.splice(index, 1) 35 | break 36 | } 37 | } 38 | }, 39 | 40 | DEL_OTHERS_VISITED_VIEWS: (state, view) => { 41 | state.visitedViews = state.visitedViews.filter(v => { 42 | return v.meta.affix || v.path === view.path 43 | }) 44 | }, 45 | DEL_OTHERS_CACHED_VIEWS: (state, view) => { 46 | for (const i of state.cachedViews) { 47 | if (i === view.name) { 48 | const index = state.cachedViews.indexOf(i) 49 | state.cachedViews = state.cachedViews.slice(index, index + 1) 50 | break 51 | } 52 | } 53 | }, 54 | 55 | DEL_ALL_VISITED_VIEWS: state => { 56 | // keep affix tags 57 | const affixTags = state.visitedViews.filter(tag => tag.meta.affix) 58 | state.visitedViews = affixTags 59 | }, 60 | DEL_ALL_CACHED_VIEWS: state => { 61 | state.cachedViews = [] 62 | }, 63 | 64 | UPDATE_VISITED_VIEW: (state, view) => { 65 | for (let v of state.visitedViews) { 66 | if (v.path === view.path) { 67 | v = Object.assign(v, view) 68 | break 69 | } 70 | } 71 | } 72 | } 73 | 74 | const actions = { 75 | addView({ dispatch }, view) { 76 | dispatch('addVisitedView', view) 77 | dispatch('addCachedView', view) 78 | }, 79 | addVisitedView({ commit }, view) { 80 | commit('ADD_VISITED_VIEW', view) 81 | }, 82 | addCachedView({ commit }, view) { 83 | commit('ADD_CACHED_VIEW', view) 84 | }, 85 | 86 | delView({ dispatch, state }, view) { 87 | return new Promise(resolve => { 88 | dispatch('delVisitedView', view) 89 | dispatch('delCachedView', view) 90 | resolve({ 91 | visitedViews: [...state.visitedViews], 92 | cachedViews: [...state.cachedViews] 93 | }) 94 | }) 95 | }, 96 | delVisitedView({ commit, state }, view) { 97 | return new Promise(resolve => { 98 | commit('DEL_VISITED_VIEW', view) 99 | resolve([...state.visitedViews]) 100 | }) 101 | }, 102 | delCachedView({ commit, state }, view) { 103 | return new Promise(resolve => { 104 | commit('DEL_CACHED_VIEW', view) 105 | resolve([...state.cachedViews]) 106 | }) 107 | }, 108 | 109 | delOthersViews({ dispatch, state }, view) { 110 | return new Promise(resolve => { 111 | dispatch('delOthersVisitedViews', view) 112 | dispatch('delOthersCachedViews', view) 113 | resolve({ 114 | visitedViews: [...state.visitedViews], 115 | cachedViews: [...state.cachedViews] 116 | }) 117 | }) 118 | }, 119 | delOthersVisitedViews({ commit, state }, view) { 120 | return new Promise(resolve => { 121 | commit('DEL_OTHERS_VISITED_VIEWS', view) 122 | resolve([...state.visitedViews]) 123 | }) 124 | }, 125 | delOthersCachedViews({ commit, state }, view) { 126 | return new Promise(resolve => { 127 | commit('DEL_OTHERS_CACHED_VIEWS', view) 128 | resolve([...state.cachedViews]) 129 | }) 130 | }, 131 | 132 | delAllViews({ dispatch, state }, view) { 133 | return new Promise(resolve => { 134 | dispatch('delAllVisitedViews', view) 135 | dispatch('delAllCachedViews', view) 136 | resolve({ 137 | visitedViews: [...state.visitedViews], 138 | cachedViews: [...state.cachedViews] 139 | }) 140 | }) 141 | }, 142 | delAllVisitedViews({ commit, state }) { 143 | return new Promise(resolve => { 144 | commit('DEL_ALL_VISITED_VIEWS') 145 | resolve([...state.visitedViews]) 146 | }) 147 | }, 148 | delAllCachedViews({ commit, state }) { 149 | return new Promise(resolve => { 150 | commit('DEL_ALL_CACHED_VIEWS') 151 | resolve([...state.cachedViews]) 152 | }) 153 | }, 154 | 155 | updateVisitedView({ commit }, view) { 156 | commit('UPDATE_VISITED_VIEW', view) 157 | } 158 | } 159 | 160 | export default { 161 | namespaced: true, 162 | state, 163 | mutations, 164 | actions 165 | } 166 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import API from '@/assets/http/apiUrl' 2 | import Request from '@/assets/http' 3 | 4 | const user = { 5 | state: { 6 | token: '', 7 | name: '', 8 | roles: '' 9 | }, 10 | mutations: { 11 | SET_TOKEN: (state, data) => { 12 | state.token = data 13 | }, 14 | SET_USER_INFO: (state, data) => { 15 | state.name = data.name || '' 16 | state.roles = data.roles || [] 17 | } 18 | }, 19 | actions: { 20 | Login({ commit }, params) { 21 | return new Promise((resolve, reject) => { 22 | Request.httpRequest({ 23 | method: 'post', 24 | url: API.Login, 25 | params: params, 26 | success: data => { 27 | localStorage.setItem('ADMIN_TOKEN', data.token) 28 | commit('SET_USER_INFO', data.userInfo) 29 | resolve(data) 30 | }, 31 | error: err => { 32 | reject(err) 33 | } 34 | }) 35 | }) 36 | }, 37 | 38 | LoginByVin({ dispatch, commit }, params) { 39 | return dispatch('Login', params) 40 | }, 41 | 42 | ResetToken({ commit }) { 43 | return new Promise(resolve => { 44 | console.log(1111) 45 | commit('SET_TOKEN', '') 46 | commit('SET_USER_INFO', {}) 47 | localStorage.removeItem('ADMIN_TOKEN') 48 | resolve() 49 | }) 50 | }, 51 | 52 | // ?? 53 | LogOut({ dispatch, commit }) { 54 | return dispatch('ResetToken') 55 | } 56 | } 57 | } 58 | 59 | export default user 60 | 61 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | 67 | /* 去掉箭头 */ 68 | input::-webkit-outer-spin-button, 69 | input::-webkit-inner-spin-button { 70 | -webkit-appearance: none; 71 | } 72 | input[type="number"] { 73 | -moz-appearance: textfield; 74 | } 75 | 76 | // 改变Placeholder文字的颜色 77 | input::-moz-placeholder{color:rgb(204, 204, 204)} //Firefox 78 | input::-webkit-input-placeholder{color:rgb(204, 204, 204)} //Chrome,Safari 79 | input:-ms-input-placeholder{color:rgb(204, 204, 204)} // ie 80 | textarea::-moz-placeholder{color:rgb(204, 204, 204)} //Firefox 81 | textarea::-webkit-input-placeholder{color:rgb(204, 204, 204)} //Chrome,Safari 82 | textarea:-ms-input-placeholder{color:rgb(204, 204, 204)} // ie 83 | -------------------------------------------------------------------------------- /src/styles/login.scss: -------------------------------------------------------------------------------- 1 | $bg: rgb(245, 245, 245); 2 | $dark_gray: rgb(18, 29, 37); 3 | $light_gray: rgb(18, 29, 37); 4 | .login-container { 5 | position: fixed; 6 | height: 100%; 7 | width: 100%; 8 | background-color: $bg; 9 | .el-form-item { 10 | margin-bottom: 25px; 11 | } 12 | .login-card { 13 | border-radius: 8px; 14 | .el-card__body { 15 | padding: 40px; 16 | } 17 | } 18 | .login-form { 19 | position: absolute; 20 | left: 0; 21 | right: 0; 22 | width: 540px; 23 | max-width: 100%; 24 | padding: 35px 35px 15px 35px; 25 | margin: 120px auto; 26 | .login-card { 27 | .login-tab { 28 | display: flex; 29 | align-items: center; 30 | padding: 0 20px 20px; 31 | .tab-item { 32 | flex: 1; 33 | text-align: center; 34 | font-size: 16px; 35 | cursor: pointer; 36 | &:hover { 37 | color: #121d25 !important; 38 | } 39 | } 40 | } 41 | .login-input-item { 42 | border: 1px solid rgba(0, 0, 0, 0.1); 43 | border-radius: 5px; 44 | color: #454545; 45 | position: relative; 46 | .el-input { 47 | display: inline-block; 48 | width: 100%; 49 | padding-left: 15px; 50 | margin-bottom: 2px; 51 | input { 52 | background: transparent; 53 | border: 0px; 54 | -webkit-appearance: none; 55 | border-radius: 0px; 56 | color: $light_gray; 57 | padding-left: 0; 58 | &:-webkit-autofill { 59 | -webkit-box-shadow: 0 0 0px 1000px #fff inset !important; 60 | -webkit-text-fill-color: $light_gray !important; 61 | color: $light_gray !important; 62 | caret-color:$light_gray !important; 63 | } 64 | &::-moz-placeholder{color:rgb(204, 204, 204)} //Firefox 65 | &::-webkit-input-placeholder{color:rgb(204, 204, 204)} //Chrome,Safari 66 | &:-ms-input-placeholder{color:rgb(204, 204, 204)} // ie 67 | } 68 | } 69 | .svg-container { 70 | position: absolute; 71 | top: 0; 72 | left: 0; 73 | padding: 0 15px; 74 | color: $dark_gray; 75 | vertical-align: middle; 76 | display: inline-block; 77 | } 78 | .code { 79 | position: absolute; 80 | right: 10px; 81 | top: 2px; 82 | font-size: 14px; 83 | color: #121d25; 84 | cursor: pointer; 85 | user-select: none; 86 | } 87 | } 88 | .to-login { 89 | display: flex; 90 | justify-content: flex-end; 91 | font-size: 14px; 92 | margin-top: 8px; 93 | .btn { 94 | cursor: pointer; 95 | &:hover { 96 | color: #409EFF; 97 | } 98 | } 99 | } 100 | .accept-agreement { 101 | margin-bottom: 20px; 102 | font-size: 14px; 103 | .agreement { 104 | cursor: pointer; 105 | &:hover { 106 | color: #409EFF; 107 | } 108 | } 109 | } 110 | .psw { 111 | margin-bottom: 0 112 | } 113 | .forget-psw { 114 | display: flex; 115 | justify-content: flex-end; 116 | cursor: pointer; 117 | font-size: 14px; 118 | margin-top: 8px; 119 | margin-bottom: 22px; 120 | &:hover { 121 | color: #409EFF; 122 | } 123 | } 124 | } 125 | } 126 | .tips { 127 | font-size: 14px; 128 | color: #000; 129 | margin-bottom: 10px; 130 | span { 131 | &:first-of-type { 132 | margin-right: 16px; 133 | } 134 | } 135 | } 136 | .title { 137 | font-size: 32px; 138 | color: $light_gray; 139 | margin: 0px auto 40px auto; 140 | text-align: center; 141 | letter-spacing: 1px; 142 | } 143 | .show-pwd { 144 | position: absolute; 145 | right: 10px; 146 | top: 7px; 147 | font-size: 16px; 148 | color: $dark_gray; 149 | cursor: pointer; 150 | user-select: none; 151 | } 152 | } -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | /* 背景自适应容器大小 */ 31 | @mixin bgCover($url) { 32 | background-image: url($url); 33 | background-repeat: no-repeat; 34 | background-size: cover; 35 | background-position: 0 center; 36 | } 37 | 38 | @mixin noData($url) { 39 | width: 100%; 40 | font-size: 14px; 41 | text-align: center; 42 | color: #666; 43 | line-height: 60px; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: $sideBarWidth; 7 | position: relative; 8 | } 9 | 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: $sideBarWidth !important; 13 | background-color: $menuBg; 14 | height: 100%; 15 | position: fixed; 16 | font-size: 0px; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | z-index: 1001; 21 | overflow: hidden; 22 | 23 | // reset element-ui css 24 | .horizontal-collapse-transition { 25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 26 | } 27 | 28 | .scrollbar-wrapper { 29 | overflow-x: hidden !important; 30 | } 31 | 32 | .el-scrollbar__bar.is-vertical { 33 | right: 0px; 34 | } 35 | 36 | .el-scrollbar { 37 | height: 100%; 38 | } 39 | 40 | &.has-logo { 41 | .el-scrollbar { 42 | height: calc(100% - 50px); 43 | } 44 | } 45 | 46 | .is-horizontal { 47 | display: none; 48 | } 49 | 50 | a { 51 | display: inline-block; 52 | width: 100%; 53 | overflow: hidden; 54 | } 55 | 56 | .svg-icon { 57 | margin-right: 16px; 58 | } 59 | 60 | .el-menu { 61 | border: none; 62 | height: 100%; 63 | width: 100% !important; 64 | } 65 | 66 | // menu hover 67 | .submenu-title-noDropdown, 68 | .el-submenu__title { 69 | &:hover { 70 | background-color: $menuHover !important; 71 | } 72 | } 73 | 74 | .is-active>.el-submenu__title { 75 | color: $subMenuActiveText !important; 76 | } 77 | 78 | & .nest-menu .el-submenu>.el-submenu__title, 79 | & .el-submenu .el-menu-item { 80 | min-width: $sideBarWidth !important; 81 | background-color: $subMenuBg !important; 82 | 83 | &:hover { 84 | background-color: $subMenuHover !important; 85 | } 86 | } 87 | } 88 | 89 | .hideSidebar { 90 | .sidebar-container { 91 | width: 54px !important; 92 | } 93 | 94 | .main-container { 95 | margin-left: 54px; 96 | } 97 | 98 | .submenu-title-noDropdown { 99 | padding: 0 !important; 100 | position: relative; 101 | 102 | .el-tooltip { 103 | padding: 0 !important; 104 | 105 | .svg-icon { 106 | margin-left: 20px; 107 | } 108 | } 109 | } 110 | 111 | .el-submenu { 112 | overflow: hidden; 113 | 114 | &>.el-submenu__title { 115 | padding: 0 !important; 116 | 117 | .svg-icon { 118 | margin-left: 20px; 119 | } 120 | 121 | .el-submenu__icon-arrow { 122 | display: none; 123 | } 124 | } 125 | } 126 | 127 | .el-menu--collapse { 128 | .el-submenu { 129 | &>.el-submenu__title { 130 | &>span { 131 | height: 0; 132 | width: 0; 133 | overflow: hidden; 134 | visibility: hidden; 135 | display: inline-block; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | .el-menu--collapse .el-menu .el-submenu { 143 | min-width: $sideBarWidth !important; 144 | } 145 | 146 | // mobile responsive 147 | .mobile { 148 | .main-container { 149 | margin-left: 0px; 150 | } 151 | 152 | .sidebar-container { 153 | transition: transform .28s; 154 | width: $sideBarWidth !important; 155 | } 156 | 157 | &.hideSidebar { 158 | .sidebar-container { 159 | pointer-events: none; 160 | transition-duration: 0.3s; 161 | transform: translate3d(-$sideBarWidth, 0, 0); 162 | } 163 | } 164 | } 165 | 166 | .withoutAnimation { 167 | 168 | .main-container, 169 | .sidebar-container { 170 | transition: none; 171 | } 172 | } 173 | } 174 | 175 | // when menu collapsed 176 | .el-menu--vertical { 177 | &>.el-menu { 178 | .svg-icon { 179 | margin-right: 16px; 180 | } 181 | } 182 | 183 | .nest-menu .el-submenu>.el-submenu__title, 184 | .el-menu-item { 185 | &:hover { 186 | // you can use $subMenuHover 187 | background-color: $menuHover !important; 188 | } 189 | } 190 | 191 | // the scroll bar appears when the subMenu is too long 192 | >.el-menu--popup { 193 | max-height: 100vh; 194 | overflow-y: auto; 195 | 196 | &::-webkit-scrollbar-track-piece { 197 | background: #d3dce6; 198 | } 199 | 200 | &::-webkit-scrollbar { 201 | width: 6px; 202 | } 203 | 204 | &::-webkit-scrollbar-thumb { 205 | background: #99a9bf; 206 | border-radius: 20px; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/styles/table/demo.scss: -------------------------------------------------------------------------------- 1 | 2 | @import '~@/styles/mixin'; 3 | .el-card { 4 | border-radius: 8px; 5 | } 6 | 7 | .pagination { 8 | overflow: auto; 9 | text-align: right; 10 | padding: 20px 0; 11 | margin: 0 10px; 12 | } 13 | 14 | .list-content { 15 | background: #fff; 16 | border: 0; 17 | .el-card__body { 18 | padding: 0; 19 | .el-table thead th { 20 | background: #e6e6e6; 21 | color: #606266; 22 | } 23 | } 24 | .table-content { 25 | border-radius: 8px; 26 | .table-img { 27 | width: 120px; 28 | height: 120px; 29 | } 30 | .icon-btn { 31 | cursor: pointer; 32 | } 33 | .operation { 34 | text-overflow: unset; 35 | .cell { 36 | white-space: nowrap; 37 | text-overflow: unset; 38 | } 39 | .item { 40 | color: #409EFF; 41 | margin-right: 20px; 42 | &:last-child { 43 | margin-right: 0px; 44 | } 45 | &:hover { 46 | color: #606266; 47 | } 48 | } 49 | .item-gray { 50 | cursor: default; 51 | color: #aaa; 52 | margin-right: 20px; 53 | &:last-child { 54 | margin-right: 0px; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#bfcbd9; 3 | $menuActiveText:#409EFF; 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg:#304156; 7 | $menuHover:#263445; 8 | 9 | $subMenuBg:#1f2d3d; 10 | $subMenuHover:#001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // the :export directive is the magic sauce for webpack 15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 16 | :export { 17 | menuText: $menuText; 18 | menuActiveText: $menuActiveText; 19 | subMenuActiveText: $subMenuActiveText; 20 | menuBg: $menuBg; 21 | menuHover: $menuHover; 22 | subMenuBg: $subMenuBg; 23 | subMenuHover: $subMenuHover; 24 | sideBarWidth: $sideBarWidth; 25 | } 26 | -------------------------------------------------------------------------------- /src/views/404/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 56 | 57 | 251 | -------------------------------------------------------------------------------- /src/views/homepage/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 10 | 24 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | /* 登录 */ 2 | 116 | 117 | 288 | 289 | 293 | -------------------------------------------------------------------------------- /src/views/login/register.vue: -------------------------------------------------------------------------------- 1 | /* 注册 */ 2 | 85 | 86 | 255 | 256 | 260 | -------------------------------------------------------------------------------- /src/views/login/resetPsw.vue: -------------------------------------------------------------------------------- 1 | /* 重置密码 */ 2 | 75 | 76 | 224 | 225 | 263 | 264 | 343 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/views/table/demo.vue: -------------------------------------------------------------------------------- 1 | 61 | 121 | 127 | -------------------------------------------------------------------------------- /src/views/table/demoTest.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | function resolve(dir) { 4 | return path.join(__dirname, dir) 5 | } 6 | 7 | module.exports = { 8 | publicPath: '/', 9 | outputDir: 'dist', // 输出文件目录 10 | assetsDir: 'assets', // 静态资源文件夹 11 | productionSourceMap: false, 12 | devServer: { 13 | port: 9566, // 端口号 14 | open: true, 15 | proxy: null // 设置代理 16 | }, 17 | css: { 18 | loaderOptions: { 19 | sass: { // 如果用的是less就改成less 20 | javascriptEnabled: true 21 | }, 22 | postcss: { 23 | plugins: [ 24 | require('autoprefixer')({}), 25 | require('postcss-plugin-px2rem')({ 26 | rootValue: 54, // 换算基数,默认100,自行根据效果调整。 27 | mediaQuery: false, // (布尔值)允许在媒体查询中转换px。 28 | minPixelValue: 3 // 设置要替换的最小像素值默认0,这里表示大于3px会被转rem。 29 | }) 30 | ] 31 | } 32 | } 33 | }, 34 | chainWebpack: config => { 35 | config.module 36 | .rule('css') 37 | .test(/\.css$/) 38 | .oneOf('vue') 39 | .resourceQuery(/\?vue/) 40 | .use('px2rem') 41 | .loader('px2rem-loader') 42 | .options({ 43 | remUnit: 54 44 | }) 45 | 46 | config.module 47 | .rule('svg') 48 | .exclude.add(resolve('src/icons')) 49 | .end() 50 | 51 | config.module 52 | .rule('icons') 53 | .test(/\.svg$/) 54 | .include.add(resolve('src/icons')) 55 | .end() 56 | .use('svg-sprite-loader') 57 | .loader('svg-sprite-loader') 58 | .options({ 59 | symbolId: 'icon-[name]' 60 | }) 61 | 62 | config.entry.app = ['babel-polyfill', './src/main.js'] 63 | } 64 | } 65 | 66 | --------------------------------------------------------------------------------