├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .postcssrc.js ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── Easy.Front-End.njsproj ├── Easy.Front-End.sln ├── LICENSE ├── README.md ├── package.json ├── public └── index.html ├── src ├── App.vue ├── api │ ├── base.js │ ├── login.js │ └── route.js ├── apiSettings.js ├── assets │ ├── 401_images │ │ └── 401.gif │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ └── custom-theme │ │ ├── fonts │ │ ├── element-icons.ttf │ │ └── element-icons.woff │ │ └── index.css ├── components │ ├── Breadcrumb │ │ └── index.vue │ ├── ErrorLog │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── Pagination │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── ScrollPane │ │ └── index.vue │ ├── SizeSelect │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ └── ThemePicker │ │ └── index.vue ├── errorLog.js ├── icons │ ├── index.js │ ├── svg │ │ ├── 404.svg │ │ ├── bug.svg │ │ ├── chart.svg │ │ ├── clipboard.svg │ │ ├── component.svg │ │ ├── dashboard.svg │ │ ├── documentation.svg │ │ ├── drag.svg │ │ ├── edit.svg │ │ ├── email.svg │ │ ├── example.svg │ │ ├── excel.svg │ │ ├── eye.svg │ │ ├── form.svg │ │ ├── guide 2.svg │ │ ├── guide.svg │ │ ├── icon.svg │ │ ├── international.svg │ │ ├── language.svg │ │ ├── link.svg │ │ ├── list.svg │ │ ├── lock.svg │ │ ├── message.svg │ │ ├── money.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── people.svg │ │ ├── peoples.svg │ │ ├── qq.svg │ │ ├── shopping.svg │ │ ├── size.svg │ │ ├── star.svg │ │ ├── tab.svg │ │ ├── table.svg │ │ ├── theme.svg │ │ ├── tree.svg │ │ ├── user.svg │ │ ├── wechat.svg │ │ └── zip.svg │ └── svgo.yml ├── index.js ├── main.js ├── main1.js ├── permission.js ├── router │ └── index.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── apiSettings.js │ │ ├── app.js │ │ ├── errorLog.js │ │ ├── permission.js │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── btn.scss │ ├── element-ui.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── index.js │ ├── openWindow.js │ ├── permission.js │ ├── request.js │ ├── scrollTo.js │ ├── storage.js │ └── validate.js └── views │ ├── Menu │ └── index.vue │ ├── Role │ └── form.vue │ ├── common │ ├── formBase.vue │ └── tableBase.vue │ ├── dashboard │ └── index.vue │ ├── errorLog │ ├── errorTestA.vue │ ├── errorTestB.vue │ └── index.vue │ ├── errorPage │ ├── 401.vue │ └── 404.vue │ ├── guide │ ├── defineSteps.js │ └── index.vue │ ├── layout │ ├── Layout.vue │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ ├── TagsView.vue │ │ └── index.js │ └── mixin │ │ └── ResizeHandler.js │ ├── login │ ├── authredirect.vue │ ├── index.vue │ └── socialsignin.vue │ └── redirect │ └── index.vue ├── vue.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@vue/app" 4 | ] 5 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = crlf 3 | 4 | [*.xml] 5 | indent_style = space -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Easy.Front-End 2 | 3 | on: [create] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Setup Node.js for use with actions 13 | uses: actions/setup-node@v1.1.0 14 | with: 15 | version: 10.13.x 16 | - name: Install dependencies 17 | run: yarn install 18 | - name: Build 19 | run: yarn run build 20 | - name: Create .npmrc 21 | run: echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 24 | - name: Publish 25 | run: yarn publish --access=public 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | package-lock.json 63 | .vs/ 64 | /dist 65 | /*.user 66 | /obj 67 | /bin 68 | /lib -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "singleQuote": true, 4 | "semi": false 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "command": "yarn dev", 9 | "name": "Run yarn dev", 10 | "request": "launch", 11 | "type": "node-terminal" 12 | }, 13 | { 14 | "type": "chrome", 15 | "request": "launch", 16 | "name": "Launch Chrome", 17 | "url": "http://localhost:1337", 18 | "webRoot": "${workspaceFolder}", 19 | "breakOnLoad": true 20 | }, 21 | { 22 | "type": "node", 23 | "request": "launch", 24 | "name": "Launch via node(调试配置文件)", 25 | "runtimeExecutable": "node", 26 | "runtimeArgs": [ 27 | "--inspect-brk=9229", 28 | "./node_modules/@vue/cli-service/bin/vue-cli-service.js", 29 | "serve", 30 | "--port=1337" 31 | ], 32 | "autoAttachChildProcesses": true, 33 | "port": 9229 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#61021b", 4 | "activityBar.activeBorder": "#237d03", 5 | "activityBar.foreground": "#e7e7e7", 6 | "activityBar.inactiveForeground": "#e7e7e799", 7 | "activityBarBadge.background": "#237d03", 8 | "activityBarBadge.foreground": "#e7e7e7", 9 | "titleBar.activeBackground": "#2f010d", 10 | "titleBar.inactiveBackground": "#2f010d99", 11 | "titleBar.activeForeground": "#e7e7e7", 12 | "titleBar.inactiveForeground": "#e7e7e799", 13 | "statusBar.background": "#2f010d", 14 | "statusBarItem.hoverBackground": "#61021b", 15 | "statusBar.foreground": "#e7e7e7", 16 | "activityBar.activeBackground": "#61021b" 17 | }, 18 | "peacock.color": "#2f010d" 19 | } -------------------------------------------------------------------------------- /Easy.Front-End.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.102 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "Easy.Front-End", "Easy.Front-End.njsproj", "{E6B092F6-572E-4192-BB58-30A1D233D7B3}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {E6B092F6-572E-4192-BB58-30A1D233D7B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {E6B092F6-572E-4192-BB58-30A1D233D7B3}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {E6B092F6-572E-4192-BB58-30A1D233D7B3}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {E6B092F6-572E-4192-BB58-30A1D233D7B3}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {815E3C86-1362-4864-B127-09B0E0033421} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 xxred 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 | # Easy.Front-End使用介绍 2 | 3 | - 提供后台管理系统常用功能 4 | - 自动生成数据列表页,表单页,后端新建模型即可拥有对应页面 5 | - 后端采用asp.net core api 6 | - 基于element-ui,改造自https://github.com/PanJiaChen/vue-element-admin/ 7 | - [ ] jsx写法,方便调试 8 | 9 | api项目位于[Easy.Admin](https://github.com/xxred/Easy.Admin) 10 | 11 | ## 前言 12 | 13 | - 前端采用[vue](https://cn.vuejs.org/)框架,ui 使用[element](https://element.eleme.cn/#/zh-CN),框架参考[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin/)。 14 | - 本项目删除了`vue-element-admin`中的很多组件(本应基于[vue-admin-template](https://github.com/PanJiaChen/vue-admin-template),再慢慢添加功能),只保留用到的组件。在此基础添加了一些功能,菜单有后端提供,并基于权限自动过滤,提供所有页面的默认模板,可按开发需要替换任意模块。换句话说,新建项目,使用本项目,并做合适配置,不用写任何页面即可拥有所有 api 对应的操作页面,如果哪个页面不合心意,换! 15 | - 没错,这主要是为了后端开发人员方便,适合公司内部系统或者方便个人工作的小系统,对界面要求不高,而且提供定制化功能。绝对能让你做到快速完成,再根据意愿完善,将精力集中在业务,体现项目的价值。代码产生价值,才是最大的动力。 16 | - 注意:> 本项目是基于各位大佬们开源的框架组件完成的,新增的功能可以说仅有动态路由加载、默认页面模板(可覆盖)。希望可以给大家提供一个参考,觉得好用的话 copy 过去用,自己搭建框架吸收其它框架精华而不是直接基于本项目开发,这是本项目建议食用方式 17 | 18 | ## 准备 19 | 20 | - 为了照顾基础差的同学,这里就不说什么`您必须先点亮某某技能点`或者`假设您已具备某某技术基础`,而是手把手教你搭建环境,并列出需要学习的所有技术栈。 21 | - 首先你得具备 HTML、CSS 和 JavaScript 的相应知识(打脸)。 22 | - 接着是[vue](https://cn.vuejs.org/v2/guide/index.html),得了解它的基本原理和工作方式,后面的框架均基于此框架。类似于渲染一个字符串模板,只不过在 vue 中,数据和视图已经联动,数据修改会触发视图更新。 23 | - [element](https://element.eleme.cn/#/zh-CN/component/quickstart是)快速上手,element 是在 vue 的基础上进行封装和美化,类似于很多基于 jQuery 的组件,只不过它依赖的是 vue。也可以看成 bootstrap,可理解成 bootstrap 的基础上使用 vue,再将视图和数据进行关联,只不过 element 的样式是另一种风格。 24 | - [vue-element-admin](https://panjiachen.github.io/vue-element-admin)是后台前端解决方案,它基于 vue 和 element-ui 实现。将 element-ui 中多个组件组合起来构成完整的后台管理解决方案,其中用到了下列组件: 25 | - [vuex](https://vuex.vuejs.org/zh-cn/),状态管理,管理多个页面之间的公用数据,比如用户信息等。 26 | - [vue-router](https://router.vuejs.org/zh-cn/),路由系统,因为整个项目就是一个单页应用,不同页面切换也需要管理和记录。属于官方出品,与 Vue.js 的核心深度集成。 27 | - [axios](https://github.com/axios/axios),用于浏览器和 node.js 的基于 Promise 的 HTTP 客户端,类似于 jQuery ajax 28 | - [vue-cli](https://github.com/vuejs/vue-cli),3.x 版本起自带基础[webpack](https://webpack.docschina.org/)配置,基本够用了,告别了复杂打包配置,对此我只想说,干得漂亮!并且自带 ui 管理,`vue ui`开启新方式 29 | 30 | ## 环境搭建 31 | 32 | - [git 下载地址](https://npm.taobao.org/mirrors/git-for-windows/v2.24.1.windows.2/Git-2.24.1.2-64-bit.exe)、[nodejs v10.18.0 下载地址](https://nodejs.org/dist/v10.18.0/node-v10.18.0-x64.msi) 33 | - 设置国内镜像源: 34 | 35 | ```bash 36 | npm config set registry http://registry.npm.taobao.org/ 37 | ``` 38 | 39 | - 安装 yarn: 40 | 41 | ```bash 42 | npm i yarn -g 43 | ``` 44 | 45 | ## 开发 46 | 47 | - 克隆项目 48 | 49 | ```bash 50 | git clone https://github.com/xxred/Easy.Front-End.git 51 | ``` 52 | 53 | - 还原包 54 | 55 | ```bash 56 | # 进入项目目录 57 | cd Easy.Front-End 58 | 59 | # yarn 60 | ``` 61 | 62 | - 启动项目 63 | 64 | ```bash 65 | yarn start 66 | ``` 67 | 68 | ## 发布 69 | 70 | ```bash 71 | yarn build 72 | ``` 73 | 74 | ## 添加/覆盖页面 75 | 76 | - 覆盖一个页面,为什么是直接覆盖一个,因为有默认模板,只要检测到项目指定目录没有页面,即采用默认页面。例如,覆盖用户部分的table 页(数据列表)和 form 页(表单),直接新建`/src/views/User/index.vue`、`/src/views/User/form.vue` 77 | - 后台存储每个表的信息,因此 table 页(数据列表)和 form 页(表单)的字段都来自后端,遍历展示。每个表对应一个 table 页和一个 form 页,table 页的关键是 tableName 表名,根据表名请求后端拿到所有字段(如果有需要可以自建表维护字段信息并添加权限过滤字段啥的),form 页关键除了 tableName 还有 id,如果是编辑的话可在路由获取 id 78 | - 以下是实例: 79 | 80 | ```html 81 | 82 | 94 | 105 | 106 | 107 | 147 | 148 | 173 | ``` 174 | 175 | - 以上实例有删减,完全代码参考[这里](https://github.com/xxred/Easy.Front-End/tree/master/src/views),注意文件夹名称即表名,`index.vue`即 table 页,`form.vue`即 form 页 176 | - 如果不知道要修改的页面对应表名是什么,请看浏览器地址栏,路由规则(写法源于vue-router)是:table 页->`/${tableName}/index`、form 页->`/:tableName(${tableName})/:type(Edit|Add)/:id?`。 177 | - 如果是全部页面完全替换,则覆盖`项目目录/src/views/layout/Layout.vue`即可,完全自定义 ui,只使用项目的非 ui 功能 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xxred/easy-front-end", 3 | "version": "0.3.0", 4 | "main": "lib/index.js", 5 | "files": [ 6 | "lib" 7 | ], 8 | "scripts": { 9 | "dev": "vue-cli-service serve", 10 | "start": "vue-cli-service serve", 11 | "serve": "vue-cli-service serve", 12 | "build": "vue-cli-service build", 13 | "lib":"vue-cli-service build --target lib --dest lib --name EasyFE ./src/index.js", 14 | "lint": "vue-cli-service lint" 15 | }, 16 | "repository": "https://github.com/xxred/Easy.Front-End", 17 | "description": "Easy.Front-End,Admin,Back-end", 18 | "author": { 19 | "name": "xxred " 20 | }, 21 | "license": "MIT", 22 | "dependencies": { 23 | "axios": "^0.18.0", 24 | "element-ui": "^2.13.2", 25 | "font-awesome": "^4.7.0", 26 | "normalize.css": "^7.0.0", 27 | "nprogress": "^0.2.0", 28 | "screenfull": "^3.3.3", 29 | "vue": "^2.6.12", 30 | "vue-router": "^3.0.1", 31 | "vuex": "^3.0.1" 32 | }, 33 | "devDependencies": { 34 | "@vue/cli-plugin-babel": "^3.0.0", 35 | "@vue/cli-plugin-eslint": "^3.0.0", 36 | "@vue/cli-service": "^3.0.0", 37 | "babel-eslint": "^10.0.1", 38 | "eslint": "^5.6.0", 39 | "eslint-plugin-vue": "^4.7.1", 40 | "sass": "^1.26.11", 41 | "path-to-regexp": "^2.4.0", 42 | "sass-loader": "^10.0.2", 43 | "svg-sprite-loader": "^3.8.0", 44 | "vue-template-compiler": "^2.5.16" 45 | }, 46 | "eslintConfig": { 47 | "root": true, 48 | "env": { 49 | "node": true 50 | }, 51 | "extends": [ 52 | "plugin:vue/essential", 53 | "eslint:recommended" 54 | ], 55 | "rules": {}, 56 | "parserOptions": { 57 | "parser": "babel-eslint" 58 | } 59 | }, 60 | "postcss": { 61 | "plugins": { 62 | "autoprefixer": {} 63 | } 64 | }, 65 | "browserslist": [ 66 | "> 1%", 67 | "last 2 versions", 68 | "not ie <= 8" 69 | ], 70 | "engines": { 71 | "node": ">=0.10.3 <0.11" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | Easy.Front-End 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /src/api/base.js: -------------------------------------------------------------------------------- 1 | import request from 'src/utils/request' 2 | 3 | const baseUrl = '/api/{tableName}' 4 | 5 | export function searchData(tableName, page, search) { 6 | return request({ 7 | url: baseUrl.replace('{tableName}', tableName) + '/Search', 8 | method: 'post', 9 | data: search, 10 | params: page 11 | }) 12 | } 13 | 14 | export function queryData(tableName, id) { 15 | return request({ 16 | url: baseUrl.replace('{tableName}', tableName) + '/' + id, 17 | method: 'get' 18 | }) 19 | } 20 | 21 | export function createData(tableName, model) { 22 | return request({ 23 | url: baseUrl.replace('{tableName}', tableName), 24 | method: 'post', 25 | data: model 26 | }) 27 | } 28 | 29 | export function updateData(tableName, model) { 30 | return request({ 31 | url: baseUrl.replace('{tableName}', tableName), 32 | method: 'put', 33 | data: model 34 | }) 35 | } 36 | 37 | export function deletData(tableName, id) { 38 | return request({ 39 | url: baseUrl.replace('{tableName}', tableName) + '/' + id, 40 | method: 'delete' 41 | }) 42 | } 43 | 44 | export function getColumns(tableName) { 45 | return request({ 46 | url: baseUrl.replace('{tableName}', tableName) + '/GetColumns', 47 | method: 'get' 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /src/api/login.js: -------------------------------------------------------------------------------- 1 | import request from 'src/utils/request' 2 | import store from 'src/store' 3 | 4 | /* 此模块与store互相依赖,故使用方法返回结果 */ 5 | const api = () => store.getters.apiSettings.login 6 | 7 | export function loginByUsername(username, password) { 8 | // let data = new FormData(); 9 | // data.append("username", username); 10 | // data.append("password", password); 11 | // 改为get 12 | const data = { 13 | username, 14 | password 15 | } 16 | 17 | return request({ 18 | url: api().loginByUsername, 19 | method: 'get', 20 | // data, 21 | params: data 22 | }) 23 | } 24 | 25 | export function logout() { 26 | return request({ 27 | url: api().logout, 28 | method: 'post' 29 | }) 30 | } 31 | 32 | export function getUserInfo() { 33 | return request({ 34 | url: api().getUserInfo, 35 | method: 'get' 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/api/route.js: -------------------------------------------------------------------------------- 1 | import request from 'src/utils/request' 2 | import store from 'src/store' 3 | 4 | /* 此模块与store互相依赖,故使用方法返回结果 */ 5 | const api = () => store.getters.apiSettings.route 6 | 7 | export function getRoutes() { 8 | return request({ 9 | url: api().getRoutes, 10 | method: 'get' 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/apiSettings.js: -------------------------------------------------------------------------------- 1 | const login = { 2 | loginByUsername: '/api/Account/Login', 3 | logout: '/api/Account/Logout', 4 | getUserInfo: '/api/Account/GetUserInfo' 5 | } 6 | 7 | const route = { 8 | getRoutes: '/api/route' 9 | } 10 | 11 | export default settings => { 12 | settings.baseUrl = 13 | // '' 14 | 'http://localhost:32768/' 15 | // 'http://localhost:7877' 16 | settings.login = login 17 | settings.route = route 18 | return settings 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxred/Easy.Front-End/9e9d8f6512a0c785ea8e3cd9e439b28f9667d777/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxred/Easy.Front-End/9e9d8f6512a0c785ea8e3cd9e439b28f9667d777/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxred/Easy.Front-End/9e9d8f6512a0c785ea8e3cd9e439b28f9667d777/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxred/Easy.Front-End/9e9d8f6512a0c785ea8e3cd9e439b28f9667d777/src/assets/custom-theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxred/Easy.Front-End/9e9d8f6512a0c785ea8e3cd9e439b28f9667d777/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 82 | 83 | 95 | -------------------------------------------------------------------------------- /src/components/ErrorLog/index.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 68 | 69 | 87 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 48 | 49 | 64 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 94 | 95 | 104 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 68 | 69 | 79 | -------------------------------------------------------------------------------- /src/components/ScrollPane/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 76 | 77 | 93 | -------------------------------------------------------------------------------- /src/components/SizeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 47 | 48 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | 35 | 44 | -------------------------------------------------------------------------------- /src/components/ThemePicker/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 148 | 149 | 158 | -------------------------------------------------------------------------------- /src/errorLog.js: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue' 2 | import store from 'src/store' 3 | 4 | // you can set only in production env show the error-log 5 | if (process.env.NODE_ENV === 'production') { 6 | Vue.config.errorHandler = function(err, vm, info, a) { 7 | // Don't ask me why I use Vue.nextTick, it just a hack. 8 | // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500 9 | Vue.nextTick(() => { 10 | store.dispatch('addErrorLog', { 11 | err, 12 | vm, 13 | info, 14 | url: window.location.href 15 | }) 16 | console.error(err, info) 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue' 2 | import SvgIcon from 'src/components/SvgIcon' 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/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide 2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/qq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/index.js: -------------------------------------------------------------------------------- 1 | import Storage from './utils/storage' 2 | import Request from './utils/request' 3 | import Router from './router' 4 | import Store from './store' 5 | 6 | import FormBase from './views/common/formBase.vue' 7 | import TableBase from './views/common/tableBase.vue' 8 | import App from './App.vue' 9 | 10 | const components = [ 11 | FormBase, TableBase 12 | ] 13 | 14 | const install = (Vue, opts = {}) => { 15 | require('./icons') // svgicon 16 | require('./errorLog')// error log 17 | require('./permission') // permission control 18 | 19 | components.forEach(component => { 20 | Vue.component(component.name, component) 21 | }) 22 | 23 | Vue.prototype.$axios = Request 24 | } 25 | 26 | /** 27 | * 设置api地址 28 | * @param {*} apiSettings 29 | */ 30 | const setApiSettings = apiSettings => { 31 | // Store.dispatch('setApiSettings', apiSettings) 32 | } 33 | 34 | if (typeof window !== 'undefined' && window.Vue) { 35 | install(window.Vue) 36 | } 37 | 38 | export default { 39 | install, 40 | setApiSettings, 41 | Storage, 42 | Request, 43 | Router, 44 | Store, 45 | FormBase, 46 | TableBase, 47 | App 48 | } 49 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | // import VueRouter from 'vue-router' 3 | // import Vuex from 'vuex' 4 | // import Element from 'element-ui' 5 | import EasyFE from './index' 6 | import apiSettings from './apiSettings' 7 | 8 | /* A modern alternative to CSS resets */ 9 | import 'normalize.css/normalize.css' 10 | /* element-ui */ 11 | import 'element-ui/lib/theme-chalk/index.css' 12 | import './styles/index.scss' 13 | 14 | // Vue.use(VueRouter) 15 | // Vue.use(Vuex) 16 | 17 | const router = EasyFE.Router 18 | const store = EasyFE.Store 19 | const App = EasyFE.App 20 | // const Storage = EasyFE.Storage 21 | 22 | // Vue.use(Element, { 23 | // size: Storage.getItem('size') || 'medium' // set element-ui default size 24 | // // i18n: (key, value) => i18n.t(key, value) 25 | // }) 26 | Vue.use(EasyFE) 27 | 28 | EasyFE.setApiSettings(apiSettings) 29 | 30 | new Vue({ 31 | router, 32 | store, 33 | render: (h) => h(App) 34 | }).$mount('#app') 35 | -------------------------------------------------------------------------------- /src/main1.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Storage from 'src/utils/storage' 3 | import Request from 'src/utils/request' 4 | 5 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 6 | 7 | import Element from 'element-ui' 8 | import 'element-ui/lib/theme-chalk/index.css' 9 | 10 | import 'src/styles/index.scss' // global css 11 | 12 | import App from 'src/App.vue' 13 | import router from 'src/router' 14 | import store from 'src/store' 15 | 16 | import apiSettings from 'src/apiSettings' 17 | 18 | // 设置api地址 19 | store.dispatch('setApiSettings', apiSettings) 20 | 21 | import 'src/icons' // svgicon 22 | import 'src/errorLog' // error log 23 | import 'src/permission' // permission control 24 | 25 | Vue.use(Element, { 26 | size: Storage.getItem('size') || 'medium' // set element-ui default size 27 | // i18n: (key, value) => i18n.t(key, value) 28 | }) 29 | 30 | // 注册全局table基础组件 31 | Vue.component('table-base', () => import('src/views/common/tableBase.vue')) 32 | Vue.component('form-base', () => import('src/views/common/formBase.vue')) 33 | 34 | Vue.config.productionTip = false 35 | Vue.prototype.$axios = Request 36 | 37 | new Vue({ 38 | router, 39 | store, 40 | render: h => h(App) 41 | }).$mount('#app') 42 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from 'src/router' 2 | import store from 'src/store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from 'src/utils/auth' // getToken from cookie 7 | 8 | NProgress.configure({ 9 | showSpinner: false 10 | }) // NProgress Configuration 11 | 12 | // permission judge function 13 | function hasPermission(roles, permissionRoles) { 14 | if (roles.indexOf('admin') >= 0) return true // admin permission passed directly 15 | if (!permissionRoles) return true 16 | return roles.some(role => permissionRoles.indexOf(role) >= 0) 17 | } 18 | 19 | const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist 20 | 21 | router.beforeEach((to, from, next) => { 22 | NProgress.start() // start progress bar 23 | if (getToken()) { 24 | // determine if there has token 25 | /* has token*/ 26 | if (to.path === '/login') { 27 | next({ 28 | path: '/' 29 | }) 30 | NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it 31 | } else { 32 | if (store.getters.roles.length === 0) { 33 | // 判断当前用户是否已拉取完user_info信息 34 | store 35 | .dispatch('GetUserInfo') 36 | .then(res => { 37 | // 拉取user_info 38 | const roles = res.data.Data.Roles // note: roles must be a array! such as: ['editor','develop'] 39 | store 40 | .dispatch('GenerateRoutes', { 41 | roles 42 | }) 43 | .then(() => { 44 | // 根据roles权限生成可访问的路由表 45 | router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表 46 | next({ 47 | ...to, 48 | replace: true 49 | }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record 50 | }) 51 | }) 52 | .catch(err => { 53 | store.dispatch('FedLogOut').then(() => { 54 | Message.error(err || 'Verification failed, please login again') 55 | next({ 56 | path: '/' 57 | }) 58 | }) 59 | }) 60 | } else { 61 | // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ 62 | if (hasPermission(store.getters.roles, to.meta.roles)) { 63 | next() 64 | } else { 65 | next({ 66 | path: '/401', 67 | replace: true, 68 | query: { 69 | noGoBack: true 70 | } 71 | }) 72 | } 73 | // 可删 ↑ 74 | } 75 | } 76 | } else { 77 | /* has no token*/ 78 | if (whiteList.indexOf(to.path) !== -1) { 79 | // 在免登录白名单,直接进入 80 | next() 81 | } else { 82 | next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页 83 | NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it 84 | } 85 | } 86 | }) 87 | 88 | router.afterEach(() => { 89 | NProgress.done() // finish progress bar 90 | }) 91 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue' 2 | // import Router from 'vue-router' 3 | 4 | /* Layout */ 5 | const Layout = () => import('src/views/layout/Layout') 6 | 7 | /** note: Submenu only appear when children.length>=1 8 | * detail see https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html 9 | **/ 10 | 11 | /** 12 | * hidden: true if `hidden:true` will not show in the sidebar(default is false) 13 | * alwaysShow: true if set true, will always show the root menu, whatever its child routes length 14 | * if not set alwaysShow, only more than one route under the children 15 | * it will becomes nested mode, otherwise not show the root menu 16 | * redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb 17 | * name:'router-name' the name is used by (must set!!!) 18 | * meta : { 19 | roles: ['admin','editor'] will control the page roles (you can set multiple roles) 20 | title: 'title' the name show in submenu and breadcrumb (recommend set) 21 | icon: 'svg-name' the icon show in the sidebar, 22 | noCache: true if true ,the page will no be cached(default is false) 23 | } 24 | **/ 25 | export const constantRouterMap = [ 26 | { 27 | path: '/redirect', 28 | component: Layout, 29 | hidden: true, 30 | children: [ 31 | { 32 | path: '/redirect/:path*', 33 | component: () => import('src/views/redirect/index') 34 | } 35 | ] 36 | }, 37 | { 38 | path: '/login', 39 | component: () => import('src/views/login/index'), 40 | hidden: true 41 | }, 42 | { 43 | path: '/auth-redirect', 44 | component: () => import('src/views/login/authredirect'), 45 | hidden: true 46 | }, 47 | { 48 | path: '/404', 49 | component: () => import('src/views/errorPage/404'), 50 | hidden: true 51 | }, 52 | { 53 | path: '/401', 54 | component: () => import('src/views/errorPage/401'), 55 | hidden: true 56 | }, 57 | { 58 | path: '', 59 | component: Layout, 60 | redirect: 'dashboard', 61 | children: [ 62 | { 63 | path: 'dashboard', 64 | component: () => import('src/views/dashboard/index'), 65 | name: 'Dashboard', 66 | meta: { 67 | title: '首页', 68 | icon: 'dashboard', 69 | noCache: true 70 | } 71 | } 72 | ] 73 | }, 74 | { 75 | path: '/guide', 76 | component: Layout, 77 | redirect: '/guide/index', 78 | children: [ 79 | { 80 | path: 'index', 81 | component: () => import('src/views/guide/index'), 82 | name: 'Guide', 83 | meta: { 84 | title: '引导页', 85 | icon: 'guide', 86 | noCache: true 87 | } 88 | } 89 | ] 90 | } 91 | ] 92 | 93 | export default new VueRouter({ 94 | mode: 'history', // require service support 95 | scrollBehavior: () => ({ 96 | y: 0 97 | }), 98 | routes: constantRouterMap 99 | }) 100 | 101 | export const asyncRouterMap = [ 102 | { 103 | path: '/error-log', 104 | component: Layout, 105 | redirect: 'noredirect', 106 | children: [ 107 | { 108 | path: 'log', 109 | component: () => import('src/views/errorLog/index'), 110 | name: 'ErrorLog', 111 | meta: { 112 | title: 'errorLog', 113 | icon: 'bug' 114 | } 115 | } 116 | ] 117 | }, 118 | { 119 | path: '*', 120 | redirect: '/404', 121 | hidden: true 122 | } 123 | ] 124 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | apiSettings: state => state.apiSettings.settings, 3 | sidebar: state => state.app.sidebar, 4 | language: state => state.app.language, 5 | size: state => state.app.size, 6 | device: state => state.app.device, 7 | baseUrl: state => state.apiSettings.settings.baseUrl, 8 | visitedViews: state => state.tagsView.visitedViews, 9 | cachedViews: state => state.tagsView.cachedViews, 10 | token: state => state.user.token, 11 | avatar: state => state.user.avatar, 12 | name: state => state.user.name, 13 | introduction: state => state.user.introduction, 14 | status: state => state.user.status, 15 | roles: state => state.user.roles, 16 | setting: state => state.user.setting, 17 | permission_routers: state => state.permission.routers, 18 | addRouters: state => state.permission.addRouters, 19 | errorLogs: state => state.errorLog.logs 20 | } 21 | export default getters 22 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue' 2 | // import Vuex from 'vuex' 3 | import apiSettings from './modules/apiSettings' 4 | import app from './modules/app' 5 | import errorLog from './modules/errorLog' 6 | import permission from './modules/permission' 7 | import tagsView from './modules/tagsView' 8 | import user from './modules/user' 9 | import getters from './getters' 10 | 11 | const store = new Vuex.Store({ 12 | modules: { 13 | apiSettings, 14 | app, 15 | errorLog, 16 | permission, 17 | tagsView, 18 | user 19 | }, 20 | getters 21 | }) 22 | 23 | export default store 24 | -------------------------------------------------------------------------------- /src/store/modules/apiSettings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * api路径所有配置,包括所有请求路径和跟url 3 | */ 4 | 5 | import deafultApiSettings from '../../apiSettings.js' 6 | 7 | const apiSettings = { 8 | state: { 9 | settings: { 10 | baseUrl: '' 11 | } 12 | }, 13 | mutations: { 14 | Set_ApiSettings: (state, apiSettings) => { 15 | if (typeof apiSettings === 'function') { 16 | state.settings = apiSettings(state.settings) 17 | } else { 18 | state.settings = apiSettings 19 | } 20 | } 21 | }, 22 | actions: { 23 | /** 设置api设置 */ 24 | setApiSettings({ commit }, apiSettings) { 25 | commit('Set_ApiSettings', apiSettings) 26 | } 27 | } 28 | } 29 | 30 | apiSettings.state.settings = deafultApiSettings(apiSettings.state.settings) 31 | 32 | export default apiSettings 33 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Storage from 'src/utils/storage' 2 | 3 | const app = { 4 | state: { 5 | sidebar: { 6 | opened: !+Storage.getItem('sidebarStatus'), 7 | withoutAnimation: false 8 | }, 9 | device: 'desktop', 10 | language: Storage.getItem('language') || 'en', 11 | size: Storage.getItem('size') || 'medium', 12 | // baseUrl: "https://localhost:44336" 13 | baseUrl: '' 14 | }, 15 | mutations: { 16 | TOGGLE_SIDEBAR: state => { 17 | if (state.sidebar.opened) { 18 | Storage.setItem('sidebarStatus', 1) 19 | } else { 20 | Storage.setItem('sidebarStatus', 0) 21 | } 22 | state.sidebar.opened = !state.sidebar.opened 23 | state.sidebar.withoutAnimation = false 24 | }, 25 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 26 | Storage.setItem('sidebarStatus', 1) 27 | state.sidebar.opened = false 28 | state.sidebar.withoutAnimation = withoutAnimation 29 | }, 30 | TOGGLE_DEVICE: (state, device) => { 31 | state.device = device 32 | }, 33 | SET_LANGUAGE: (state, language) => { 34 | state.language = language 35 | Storage.setItem('language', language) 36 | }, 37 | SET_SIZE: (state, size) => { 38 | state.size = size 39 | Storage.setItem('size', size) 40 | } 41 | }, 42 | actions: { 43 | toggleSideBar({ commit }) { 44 | commit('TOGGLE_SIDEBAR') 45 | }, 46 | closeSideBar({ commit }, { withoutAnimation }) { 47 | commit('CLOSE_SIDEBAR', withoutAnimation) 48 | }, 49 | toggleDevice({ commit }, device) { 50 | commit('TOGGLE_DEVICE', device) 51 | }, 52 | setLanguage({ commit }, language) { 53 | commit('SET_LANGUAGE', language) 54 | }, 55 | setSize({ commit }, size) { 56 | commit('SET_SIZE', size) 57 | } 58 | } 59 | } 60 | 61 | export default app 62 | -------------------------------------------------------------------------------- /src/store/modules/errorLog.js: -------------------------------------------------------------------------------- 1 | const errorLog = { 2 | state: { 3 | logs: [] 4 | }, 5 | mutations: { 6 | ADD_ERROR_LOG: (state, log) => { 7 | state.logs.push(log) 8 | } 9 | }, 10 | actions: { 11 | addErrorLog({ commit }, log) { 12 | commit('ADD_ERROR_LOG', log) 13 | } 14 | } 15 | } 16 | 17 | export default errorLog 18 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { asyncRouterMap, constantRouterMap } from 'src/router' 2 | import { getRoutes } from 'src/api/route' 3 | 4 | // 添加/编辑页的路由,不需要加到菜单显示 5 | const addRouters = [] 6 | 7 | /** 8 | * 通过meta.role判断是否与当前用户权限匹配 9 | * @param roles 10 | * @param route 11 | */ 12 | function hasPermission(roles, route) { 13 | if (route.meta && route.meta.roles) { 14 | return roles.some(role => route.meta.roles.includes(role)) 15 | } else { 16 | return true 17 | } 18 | } 19 | 20 | /** 21 | * 递归过滤异步路由表,返回符合用户角色权限的路由表 22 | * @param routes asyncRouterMap 23 | * @param roles 24 | */ 25 | function filterAsyncRouter(routes, roles) { 26 | const res = [] 27 | 28 | routes.forEach(route => { 29 | const tmp = { 30 | ...route 31 | } 32 | if (hasPermission(roles, tmp)) { 33 | if (tmp.children) { 34 | tmp.children = filterAsyncRouter(tmp.children, roles) 35 | } 36 | res.push(tmp) 37 | } 38 | }) 39 | 40 | return res 41 | } 42 | 43 | function requestRoutes() { 44 | return getRoutes() 45 | } 46 | 47 | function formatRoutes(routes) { 48 | const fmRoutes = [] 49 | routes.forEach(router => { 50 | if (router.component) { 51 | const component = router.component 52 | router.component = resolve => { 53 | // 动态加载组件会编译加载项目所有组件 54 | // 这里不能全写变量,写开头确定起始地址,写结尾确定文件名 55 | // 这样就相当于编译'src/**/*.vue',编译之后模块列表才会有所有的模块,传模块路径匹配才会命中 56 | require(['src/' + component + '.vue'], resolve) 57 | } 58 | } else if (router.template) { 59 | router.component = resolve => { 60 | resolve({ 61 | template: router.template 62 | }) 63 | } 64 | } else { 65 | const component = `${router.name}/index` 66 | router.component = async resolve => { 67 | // 加载失败,不存在此模块,尝试加载本项目的视图 68 | try { 69 | // 尝试加载模块 70 | await require(['src/views/' + component + '.vue'], resolve) 71 | } catch { 72 | // 加载失败,不存在此模块,使用默认模板 73 | console.log( 74 | '(@|../..)/views/' + component + '.vue不存在,加载默认模板' 75 | ) 76 | resolve({ 77 | template: `` 78 | }) 79 | } 80 | } 81 | } 82 | 83 | let children = router.children 84 | if (children && children instanceof Array) { 85 | children = formatRoutes(children) 86 | } 87 | 88 | router.children = children 89 | 90 | fmRoutes.push(router) 91 | 92 | // 添加/编辑页路由 93 | const r = { 94 | path: router.path, 95 | component: resolve => { 96 | require(['src/views/layout/Layout.vue'], resolve) 97 | }, 98 | children: [ 99 | { 100 | path: `/:tableName(${router.name})/:type(Edit|Add)/:id?`, 101 | component: async resolve => { 102 | try { 103 | // 尝试加载模块 104 | await require(['src/views/' + router.name + '/form.vue'], resolve) 105 | } catch { 106 | // 加载失败,不存在此模块,使用默认模板 107 | // 如果本项目被引用,尝试加载本项目已有视图 108 | // console.log( 109 | // 'src/views/' + router.name + '/form.vue不存在,加载默认模板' 110 | // ) 111 | try { 112 | await require([ 113 | 'src/views/' + router.name + '/form.vue' 114 | ], resolve) 115 | } catch { 116 | resolve({ 117 | template: `` 118 | }) 119 | } 120 | } 121 | } 122 | } 123 | ] 124 | } 125 | addRouters.push(r) 126 | }) 127 | 128 | return fmRoutes 129 | } 130 | 131 | const permission = { 132 | state: { 133 | // 将展示在侧边栏的菜单 134 | routers: constantRouterMap, 135 | // 将要添加到路由系统中的新路由 136 | addRouters: [] 137 | }, 138 | mutations: { 139 | SET_ROUTERS: (state, routers) => { 140 | state.addRouters = routers 141 | state.routers = constantRouterMap.concat(routers) 142 | }, 143 | ADD_ROUTERS: (state, routers) => { 144 | state.addRouters = state.addRouters.concat(routers) 145 | } 146 | }, 147 | actions: { 148 | GenerateRoutes({ commit }, data) { 149 | return new Promise(async resolve => { 150 | const { roles } = data 151 | let accessedRouters 152 | const routeRes = await requestRoutes() 153 | const asyncRouters = asyncRouterMap.concat(formatRoutes(routeRes.data)) 154 | if (roles.includes('admin')) { 155 | accessedRouters = asyncRouters 156 | } else { 157 | accessedRouters = filterAsyncRouter(asyncRouters, roles) 158 | } 159 | commit('SET_ROUTERS', accessedRouters) 160 | commit('ADD_ROUTERS', addRouters) 161 | resolve() 162 | }) 163 | } 164 | } 165 | } 166 | 167 | export default permission 168 | -------------------------------------------------------------------------------- /src/store/modules/tagsView.js: -------------------------------------------------------------------------------- 1 | const tagsView = { 2 | state: { 3 | visitedViews: [], 4 | cachedViews: [] 5 | }, 6 | 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 | for (const [i, v] of state.visitedViews.entries()) { 42 | if (v.path === view.path) { 43 | state.visitedViews = state.visitedViews.slice(i, i + 1) 44 | break 45 | } 46 | } 47 | }, 48 | DEL_OTHERS_CACHED_VIEWS: (state, view) => { 49 | for (const i of state.cachedViews) { 50 | if (i === view.name) { 51 | const index = state.cachedViews.indexOf(i) 52 | state.cachedViews = state.cachedViews.slice(index, index + 1) 53 | break 54 | } 55 | } 56 | }, 57 | 58 | DEL_ALL_VISITED_VIEWS: state => { 59 | state.visitedViews = [] 60 | }, 61 | DEL_ALL_CACHED_VIEWS: state => { 62 | state.cachedViews = [] 63 | }, 64 | 65 | UPDATE_VISITED_VIEW: (state, view) => { 66 | for (let v of state.visitedViews) { 67 | if (v.path === view.path) { 68 | v = Object.assign(v, view) 69 | break 70 | } 71 | } 72 | } 73 | 74 | }, 75 | actions: { 76 | addView({ dispatch }, view) { 77 | dispatch('addVisitedView', view) 78 | dispatch('addCachedView', view) 79 | }, 80 | addVisitedView({ commit }, view) { 81 | commit('ADD_VISITED_VIEW', view) 82 | }, 83 | addCachedView({ commit }, view) { 84 | commit('ADD_CACHED_VIEW', view) 85 | }, 86 | 87 | delView({ dispatch, state }, view) { 88 | return new Promise(resolve => { 89 | dispatch('delVisitedView', view) 90 | dispatch('delCachedView', view) 91 | resolve({ 92 | visitedViews: [...state.visitedViews], 93 | cachedViews: [...state.cachedViews] 94 | }) 95 | }) 96 | }, 97 | delVisitedView({ commit, state }, view) { 98 | return new Promise(resolve => { 99 | commit('DEL_VISITED_VIEW', view) 100 | resolve([...state.visitedViews]) 101 | }) 102 | }, 103 | delCachedView({ commit, state }, view) { 104 | return new Promise(resolve => { 105 | commit('DEL_CACHED_VIEW', view) 106 | resolve([...state.cachedViews]) 107 | }) 108 | }, 109 | 110 | delOthersViews({ dispatch, state }, view) { 111 | return new Promise(resolve => { 112 | dispatch('delOthersVisitedViews', view) 113 | dispatch('delOthersCachedViews', view) 114 | resolve({ 115 | visitedViews: [...state.visitedViews], 116 | cachedViews: [...state.cachedViews] 117 | }) 118 | }) 119 | }, 120 | delOthersVisitedViews({ commit, state }, view) { 121 | return new Promise(resolve => { 122 | commit('DEL_OTHERS_VISITED_VIEWS', view) 123 | resolve([...state.visitedViews]) 124 | }) 125 | }, 126 | delOthersCachedViews({ commit, state }, view) { 127 | return new Promise(resolve => { 128 | commit('DEL_OTHERS_CACHED_VIEWS', view) 129 | resolve([...state.cachedViews]) 130 | }) 131 | }, 132 | 133 | delAllViews({ dispatch, state }, view) { 134 | return new Promise(resolve => { 135 | dispatch('delAllVisitedViews', view) 136 | dispatch('delAllCachedViews', view) 137 | resolve({ 138 | visitedViews: [...state.visitedViews], 139 | cachedViews: [...state.cachedViews] 140 | }) 141 | }) 142 | }, 143 | delAllVisitedViews({ commit, state }) { 144 | return new Promise(resolve => { 145 | commit('DEL_ALL_VISITED_VIEWS') 146 | resolve([...state.visitedViews]) 147 | }) 148 | }, 149 | delAllCachedViews({ commit, state }) { 150 | return new Promise(resolve => { 151 | commit('DEL_ALL_CACHED_VIEWS') 152 | resolve([...state.cachedViews]) 153 | }) 154 | }, 155 | 156 | updateVisitedView({ commit }, view) { 157 | commit('UPDATE_VISITED_VIEW', view) 158 | } 159 | } 160 | } 161 | 162 | export default tagsView 163 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { loginByUsername, logout, getUserInfo } from 'src/api/login' 2 | import { getToken, setToken, removeToken } from 'src/utils/auth' 3 | 4 | const user = { 5 | state: { 6 | user: '', 7 | status: '', 8 | code: '', 9 | token: getToken(), 10 | name: '', 11 | avatar: '', 12 | introduction: '', 13 | roles: [], 14 | setting: { 15 | articlePlatform: [] 16 | } 17 | }, 18 | 19 | mutations: { 20 | SET_CODE: (state, code) => { 21 | state.code = code 22 | }, 23 | SET_TOKEN: (state, token) => { 24 | state.token = token 25 | }, 26 | SET_INTRODUCTION: (state, introduction) => { 27 | state.introduction = introduction 28 | }, 29 | SET_SETTING: (state, setting) => { 30 | state.setting = setting 31 | }, 32 | SET_STATUS: (state, status) => { 33 | state.status = status 34 | }, 35 | SET_NAME: (state, name) => { 36 | state.name = name 37 | }, 38 | SET_AVATAR: (state, avatar) => { 39 | state.avatar = avatar 40 | }, 41 | SET_ROLES: (state, roles) => { 42 | state.roles = roles 43 | } 44 | }, 45 | 46 | actions: { 47 | // 用户名登录 48 | LoginByUsername({ commit }, userInfo) { 49 | const username = userInfo.username.trim() 50 | return new Promise((resolve, reject) => { 51 | loginByUsername(username, userInfo.password) 52 | .then(response => { 53 | const data = response.data.Data 54 | commit('SET_TOKEN', data.Token) 55 | setToken(data.Token) 56 | resolve() 57 | }) 58 | .catch(error => { 59 | reject(error) 60 | }) 61 | }) 62 | }, 63 | 64 | // 获取用户信息 65 | GetUserInfo({ commit, state }) { 66 | return new Promise((resolve, reject) => { 67 | getUserInfo() 68 | .then(response => { 69 | if (!response.data) { 70 | // 由于mockjs 不支持自定义状态码只能这样hack 71 | reject('error') 72 | } 73 | 74 | const data = response.data.Data 75 | 76 | if (data.Roles && data.Roles.length > 0) { 77 | // 验证返回的roles是否是一个非空数组 78 | commit('SET_ROLES', data.Roles) 79 | } else { 80 | reject('getInfo: roles must be a non-null array !') 81 | } 82 | 83 | commit('SET_NAME', data.Name) 84 | commit('SET_AVATAR', data.Avatar) 85 | commit('SET_INTRODUCTION', data.Introduction) 86 | resolve(response) 87 | }) 88 | .catch(error => { 89 | reject(error) 90 | }) 91 | }) 92 | }, 93 | 94 | // 第三方验证登录 95 | LoginByThirdparty({ commit, state }, token) { 96 | return new Promise((resolve, reject) => { 97 | // commit('SET_CODE', code) 98 | // loginByThirdparty(state.status, state.email, state.code).then(response => { 99 | commit('SET_TOKEN', token) 100 | setToken(token) 101 | resolve() 102 | // }).catch(error => { 103 | // reject(error) 104 | // }) 105 | }) 106 | }, 107 | 108 | // 登出 109 | LogOut({ commit, state }) { 110 | return new Promise((resolve, reject) => { 111 | logout(state.token) 112 | .then(() => { 113 | commit('SET_TOKEN', '') 114 | commit('SET_ROLES', []) 115 | removeToken() 116 | resolve() 117 | }) 118 | .catch(error => { 119 | reject(error) 120 | }) 121 | }) 122 | }, 123 | 124 | // 前端 登出 125 | FedLogOut({ commit }) { 126 | return new Promise(resolve => { 127 | commit('SET_TOKEN', '') 128 | removeToken() 129 | resolve() 130 | }) 131 | }, 132 | 133 | // 动态修改权限 134 | ChangeRoles({ commit, dispatch }, role) { 135 | return new Promise(resolve => { 136 | commit('SET_TOKEN', role) 137 | setToken(role) 138 | getUserInfo(role).then(response => { 139 | const data = response.data.Data 140 | commit('SET_ROLES', data.Roles) 141 | commit('SET_NAME', data.Name) 142 | commit('SET_AVATAR', data.Avatar) 143 | commit('SET_INTRODUCTION', data.Introduction) 144 | dispatch('GenerateRoutes', data) // 动态修改权限后 重绘侧边菜单 145 | resolve() 146 | }) 147 | }) 148 | } 149 | } 150 | } 151 | 152 | export default user 153 | -------------------------------------------------------------------------------- /src/styles/btn.scss: -------------------------------------------------------------------------------- 1 | @import 'src/styles/variables.scss'; 2 | 3 | @mixin colorBtn($color) { 4 | background: $color; 5 | 6 | &:hover { 7 | color: $color; 8 | 9 | &:before, 10 | &:after { 11 | background: $color; 12 | } 13 | } 14 | } 15 | 16 | .blue-btn { 17 | @include colorBtn($blue) 18 | } 19 | 20 | .light-blue-btn { 21 | @include colorBtn($light-blue) 22 | } 23 | 24 | .red-btn { 25 | @include colorBtn($red) 26 | } 27 | 28 | .pink-btn { 29 | @include colorBtn($pink) 30 | } 31 | 32 | .green-btn { 33 | @include colorBtn($green) 34 | } 35 | 36 | .tiffany-btn { 37 | @include colorBtn($tiffany) 38 | } 39 | 40 | .yellow-btn { 41 | @include colorBtn($yellow) 42 | } 43 | 44 | .pan-btn { 45 | font-size: 14px; 46 | color: #fff; 47 | padding: 14px 36px; 48 | border-radius: 8px; 49 | border: none; 50 | outline: none; 51 | transition: 600ms ease all; 52 | position: relative; 53 | display: inline-block; 54 | 55 | &:hover { 56 | background: #fff; 57 | 58 | &:before, 59 | &:after { 60 | width: 100%; 61 | transition: 600ms ease all; 62 | } 63 | } 64 | 65 | &:before, 66 | &:after { 67 | content: ''; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | height: 2px; 72 | width: 0; 73 | transition: 400ms ease all; 74 | } 75 | 76 | &::after { 77 | right: inherit; 78 | top: inherit; 79 | left: 0; 80 | bottom: 0; 81 | } 82 | } 83 | 84 | .custom-button { 85 | display: inline-block; 86 | line-height: 1; 87 | white-space: nowrap; 88 | cursor: pointer; 89 | background: #fff; 90 | color: #fff; 91 | -webkit-appearance: none; 92 | text-align: center; 93 | box-sizing: border-box; 94 | outline: 0; 95 | margin: 0; 96 | padding: 10px 15px; 97 | font-size: 14px; 98 | border-radius: 4px; 99 | } -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | //覆盖一些element-ui样式 2 | 3 | .el-breadcrumb__inner, .el-breadcrumb__inner a{ 4 | font-weight: 400!important; 5 | } 6 | 7 | .el-upload { 8 | input[type="file"] { 9 | display: none !important; 10 | } 11 | } 12 | 13 | .el-upload__input { 14 | display: none; 15 | } 16 | 17 | .cell { 18 | .el-tag { 19 | margin-right: 0px; 20 | } 21 | } 22 | 23 | .small-padding { 24 | .cell { 25 | padding-left: 5px; 26 | padding-right: 5px; 27 | } 28 | } 29 | 30 | .fixed-width{ 31 | .el-button--mini{ 32 | padding: 7px 10px; 33 | width: 60px; 34 | } 35 | } 36 | 37 | .status-col { 38 | .cell { 39 | padding: 0 10px; 40 | text-align: center; 41 | .el-tag { 42 | margin-right: 0px; 43 | } 44 | } 45 | } 46 | 47 | //暂时性解决dialog 问题 https://github.com/ElemeFE/element/issues/2461 48 | .el-dialog { 49 | transform: none; 50 | left: 0; 51 | position: relative; 52 | margin: 0 auto; 53 | } 54 | 55 | //文章页textarea修改样式 56 | .article-textarea { 57 | textarea { 58 | padding-right: 40px; 59 | resize: none; 60 | border: none; 61 | border-radius: 0px; 62 | border-bottom: 1px solid #bfcbd9; 63 | } 64 | } 65 | 66 | //element ui upload 67 | .upload-container { 68 | .el-upload { 69 | width: 100%; 70 | .el-upload-dragger { 71 | width: 100%; 72 | height: 200px; 73 | } 74 | } 75 | } 76 | 77 | //dropdown 78 | .el-dropdown-menu{ 79 | a{ 80 | display: block 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import 'src/styles/variables.scss'; 2 | @import 'src/styles/mixin.scss'; 3 | @import 'src/styles/transition.scss'; 4 | @import 'src/styles/element-ui.scss'; 5 | @import 'src/styles/sidebar.scss'; 6 | @import 'src/styles/btn.scss'; 7 | 8 | body { 9 | height: 100%; 10 | -moz-osx-font-smoothing: grayscale; 11 | -webkit-font-smoothing: antialiased; 12 | text-rendering: optimizeLegibility; 13 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 14 | } 15 | 16 | label { 17 | font-weight: 700; 18 | } 19 | 20 | html { 21 | height: 100%; 22 | box-sizing: border-box; 23 | } 24 | 25 | #app { 26 | height: 100%; 27 | } 28 | 29 | *, 30 | *:before, 31 | *:after { 32 | box-sizing: inherit; 33 | } 34 | 35 | .no-padding { 36 | padding: 0px !important; 37 | } 38 | 39 | .padding-content { 40 | padding: 4px 0; 41 | } 42 | 43 | a:focus, 44 | a:active { 45 | outline: none; 46 | } 47 | 48 | a, 49 | a:focus, 50 | a:hover { 51 | cursor: pointer; 52 | color: inherit; 53 | text-decoration: none; 54 | } 55 | 56 | div:focus { 57 | outline: none; 58 | } 59 | 60 | .fr { 61 | float: right; 62 | } 63 | 64 | .fl { 65 | float: left; 66 | } 67 | 68 | .pr-5 { 69 | padding-right: 5px; 70 | } 71 | 72 | .pl-5 { 73 | padding-left: 5px; 74 | } 75 | 76 | .block { 77 | display: block; 78 | } 79 | 80 | .pointer { 81 | cursor: pointer; 82 | } 83 | 84 | .inlineBlock { 85 | display: block; 86 | } 87 | 88 | .clearfix { 89 | &:after { 90 | visibility: hidden; 91 | display: block; 92 | font-size: 0; 93 | content: " "; 94 | clear: both; 95 | height: 0; 96 | } 97 | } 98 | 99 | code { 100 | background: #eef1f6; 101 | padding: 15px 16px; 102 | margin-bottom: 20px; 103 | display: block; 104 | line-height: 36px; 105 | font-size: 15px; 106 | font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; 107 | 108 | a { 109 | color: #337ab7; 110 | cursor: pointer; 111 | 112 | &:hover { 113 | color: rgb(32, 160, 255); 114 | } 115 | } 116 | } 117 | 118 | .warn-content { 119 | background: rgba(66, 185, 131, .1); 120 | border-radius: 2px; 121 | padding: 16px; 122 | padding: 1rem; 123 | line-height: 1.6rem; 124 | word-spacing: .05rem; 125 | 126 | a { 127 | color: #42b983; 128 | font-weight: 600; 129 | } 130 | } 131 | 132 | //main-container全局样式 133 | .app-container { 134 | padding: 20px; 135 | } 136 | 137 | .components-container { 138 | margin: 30px 50px; 139 | position: relative; 140 | } 141 | 142 | .pagination-container { 143 | margin-top: 30px; 144 | } 145 | 146 | .text-center { 147 | text-align: center 148 | } 149 | 150 | .sub-navbar { 151 | height: 50px; 152 | line-height: 50px; 153 | position: relative; 154 | width: 100%; 155 | text-align: right; 156 | padding-right: 20px; 157 | transition: 600ms ease position; 158 | background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); 159 | 160 | .subtitle { 161 | font-size: 20px; 162 | color: #fff; 163 | } 164 | 165 | &.draft { 166 | background: #d0d0d0; 167 | } 168 | 169 | &.deleted { 170 | background: #d0d0d0; 171 | } 172 | } 173 | 174 | .link-type, 175 | .link-type:focus { 176 | color: #337ab7; 177 | cursor: pointer; 178 | 179 | &:hover { 180 | color: rgb(32, 160, 255); 181 | } 182 | } 183 | 184 | .filter-container { 185 | padding-bottom: 10px; 186 | 187 | .filter-item { 188 | display: inline-block; 189 | vertical-align: middle; 190 | margin-bottom: 10px; 191 | } 192 | } 193 | 194 | //refine vue-multiselect plugin 195 | .multiselect { 196 | line-height: 16px; 197 | } 198 | 199 | .multiselect--active { 200 | z-index: 1000 !important; 201 | } -------------------------------------------------------------------------------- /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 | &::-webkit-scrollbar { 14 | width: 6px; 15 | } 16 | &::-webkit-scrollbar-thumb { 17 | background: #99a9bf; 18 | border-radius: 20px; 19 | } 20 | } 21 | 22 | @mixin relative { 23 | position: relative; 24 | width: 100%; 25 | height: 100%; 26 | } 27 | 28 | @mixin pct($pct) { 29 | width: #{$pct}; 30 | position: relative; 31 | margin: 0 auto; 32 | } 33 | 34 | @mixin triangle($width, $height, $color, $direction) { 35 | $width: $width/2; 36 | $color-border-style: $height solid $color; 37 | $transparent-border-style: $width solid transparent; 38 | height: 0; 39 | width: 0; 40 | @if $direction==up { 41 | border-bottom: $color-border-style; 42 | border-left: $transparent-border-style; 43 | border-right: $transparent-border-style; 44 | } 45 | @else if $direction==right { 46 | border-left: $color-border-style; 47 | border-top: $transparent-border-style; 48 | border-bottom: $transparent-border-style; 49 | } 50 | @else if $direction==down { 51 | border-top: $color-border-style; 52 | border-left: $transparent-border-style; 53 | border-right: $transparent-border-style; 54 | } 55 | @else if $direction==left { 56 | border-right: $color-border-style; 57 | border-top: $transparent-border-style; 58 | border-bottom: $transparent-border-style; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | // 主体区域 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: 180px; 7 | position: relative; 8 | } 9 | // 侧边栏 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: 180px !important; 13 | height: 100%; 14 | position: fixed; 15 | font-size: 0px; 16 | top: 0; 17 | bottom: 0; 18 | left: 0; 19 | z-index: 1001; 20 | overflow: hidden; 21 | //reset element-ui css 22 | .horizontal-collapse-transition { 23 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 24 | } 25 | .scrollbar-wrapper { 26 | overflow-x: hidden!important; 27 | .el-scrollbar__view { 28 | height: 100%; 29 | } 30 | } 31 | .el-scrollbar__bar.is-vertical{ 32 | right: 0px; 33 | } 34 | .is-horizontal { 35 | display: none; 36 | } 37 | a { 38 | display: inline-block; 39 | width: 100%; 40 | overflow: hidden; 41 | } 42 | .svg-icon { 43 | margin-right: 16px; 44 | } 45 | .el-menu { 46 | border: none; 47 | height: 100%; 48 | width: 100% !important; 49 | } 50 | .is-active > .el-submenu__title{ 51 | color: #f4f4f5!important; 52 | } 53 | } 54 | .hideSidebar { 55 | .sidebar-container { 56 | width: 36px !important; 57 | } 58 | .main-container { 59 | margin-left: 36px; 60 | } 61 | .submenu-title-noDropdown { 62 | padding-left: 10px !important; 63 | position: relative; 64 | .el-tooltip { 65 | padding: 0 10px !important; 66 | } 67 | } 68 | .el-submenu { 69 | overflow: hidden; 70 | &>.el-submenu__title { 71 | padding-left: 10px !important; 72 | .el-submenu__icon-arrow { 73 | display: none; 74 | } 75 | } 76 | } 77 | .el-menu--collapse { 78 | .el-submenu { 79 | &>.el-submenu__title { 80 | &>span { 81 | height: 0; 82 | width: 0; 83 | overflow: hidden; 84 | visibility: hidden; 85 | display: inline-block; 86 | } 87 | } 88 | } 89 | } 90 | } 91 | .sidebar-container .nest-menu .el-submenu>.el-submenu__title, 92 | .sidebar-container .el-submenu .el-menu-item { 93 | min-width: 180px !important; 94 | background-color: $subMenuBg !important; 95 | &:hover { 96 | background-color: $menuHover !important; 97 | } 98 | } 99 | .el-menu--collapse .el-menu .el-submenu { 100 | min-width: 180px !important; 101 | } 102 | 103 | //适配移动端 104 | .mobile { 105 | .main-container { 106 | margin-left: 0px; 107 | } 108 | .sidebar-container { 109 | transition: transform .28s; 110 | width: 180px !important; 111 | } 112 | &.hideSidebar { 113 | .sidebar-container { 114 | transition-duration: 0.3s; 115 | transform: translate3d(-180px, 0, 0); 116 | } 117 | } 118 | } 119 | .withoutAnimation { 120 | .main-container, 121 | .sidebar-container { 122 | transition: none; 123 | } 124 | } 125 | } 126 | 127 | .el-menu--vertical{ 128 | & >.el-menu{ 129 | .svg-icon{ 130 | margin-right: 16px; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | //globl 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 | .fade-transform-enter { 20 | opacity: 0; 21 | transform: translateX(-30px); 22 | } 23 | .fade-transform-leave-to { 24 | opacity: 0; 25 | transform: translateX(30px); 26 | } 27 | 28 | /*breadcrumb transition*/ 29 | .breadcrumb-enter-active, 30 | .breadcrumb-leave-active { 31 | transition: all .5s; 32 | } 33 | 34 | .breadcrumb-enter, 35 | .breadcrumb-leave-active { 36 | opacity: 0; 37 | transform: translateX(20px); 38 | } 39 | 40 | .breadcrumb-move { 41 | transition: all .5s; 42 | } 43 | 44 | .breadcrumb-leave-active { 45 | position: absolute; 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $blue:#324157; 2 | $light-blue:#3A71A8; 3 | $red:#C03639; 4 | $pink: #E65D6E; 5 | $green: #30B08F; 6 | $tiffany: #4AB7BD; 7 | $yellow:#FEC171; 8 | $panGreen: #30B08F; 9 | 10 | //sidebar 11 | $menuBg:#304156; 12 | $subMenuBg:#1f2d3d; 13 | $menuHover:#001528; 14 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Storage from 'src/utils/storage' 2 | 3 | const TokenKey = 'Admin-Token' 4 | 5 | export function getToken() { 6 | return Storage.getItem(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Storage.setItem(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Storage.removeItem(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jiachenpan on 16/11/18. 3 | */ 4 | 5 | export function parseTime(time, cFormat) { 6 | if (arguments.length === 0) { 7 | return null 8 | } 9 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 10 | let date 11 | if (typeof time === 'object') { 12 | date = time 13 | } else { 14 | if (('' + time).length === 10) time = parseInt(time) * 1000 15 | date = new Date(time) 16 | } 17 | const formatObj = { 18 | y: date.getFullYear(), 19 | m: date.getMonth() + 1, 20 | d: date.getDate(), 21 | h: date.getHours(), 22 | i: date.getMinutes(), 23 | s: date.getSeconds(), 24 | a: date.getDay() 25 | } 26 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 27 | let value = formatObj[key] 28 | // Note: getDay() returns 0 on Sunday 29 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 30 | if (result.length > 0 && value < 10) { 31 | value = '0' + value 32 | } 33 | return value || 0 34 | }) 35 | return time_str 36 | } 37 | 38 | export function formatTime(time, option) { 39 | time = +time * 1000 40 | const d = new Date(time) 41 | const now = Date.now() 42 | 43 | const diff = (now - d) / 1000 44 | 45 | if (diff < 30) { 46 | return '刚刚' 47 | } else if (diff < 3600) { 48 | // less 1 hour 49 | return Math.ceil(diff / 60) + '分钟前' 50 | } else if (diff < 3600 * 24) { 51 | return Math.ceil(diff / 3600) + '小时前' 52 | } else if (diff < 3600 * 24 * 2) { 53 | return '1天前' 54 | } 55 | if (option) { 56 | return parseTime(time, option) 57 | } else { 58 | return ( 59 | d.getMonth() + 60 | 1 + 61 | '月' + 62 | d.getDate() + 63 | '日' + 64 | d.getHours() + 65 | '时' + 66 | d.getMinutes() + 67 | '分' 68 | ) 69 | } 70 | } 71 | 72 | // 格式化时间 73 | export function getQueryObject(url) { 74 | url = url == null ? window.location.href : url 75 | const search = url.substring(url.lastIndexOf('?') + 1) 76 | const obj = {} 77 | const reg = /([^?&=]+)=([^?&=]*)/g 78 | search.replace(reg, (rs, $1, $2) => { 79 | const name = decodeURIComponent($1) 80 | let val = decodeURIComponent($2) 81 | val = String(val) 82 | obj[name] = val 83 | return rs 84 | }) 85 | return obj 86 | } 87 | 88 | /** 89 | *get getByteLen 90 | * @param {Sting} val input value 91 | * @returns {number} output value 92 | */ 93 | export function getByteLen(val) { 94 | let len = 0 95 | for (let i = 0; i < val.length; i++) { 96 | if (val[i].match(/[^\x00-\xff]/gi) != null) { 97 | len += 1 98 | } else { 99 | len += 0.5 100 | } 101 | } 102 | return Math.floor(len) 103 | } 104 | 105 | export function cleanArray(actual) { 106 | const newArray = [] 107 | for (let i = 0; i < actual.length; i++) { 108 | if (actual[i]) { 109 | newArray.push(actual[i]) 110 | } 111 | } 112 | return newArray 113 | } 114 | 115 | export function param(json) { 116 | if (!json) return '' 117 | return cleanArray( 118 | Object.keys(json).map(key => { 119 | if (json[key] === undefined) return '' 120 | return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]) 121 | }) 122 | ).join('&') 123 | } 124 | 125 | export function param2Obj(url) { 126 | const search = url.split('?')[1] 127 | if (!search) { 128 | return {} 129 | } 130 | return JSON.parse( 131 | '{"' + 132 | decodeURIComponent(search) 133 | .replace(/"/g, '\\"') 134 | .replace(/&/g, '","') 135 | .replace(/=/g, '":"') + 136 | '"}' 137 | ) 138 | } 139 | 140 | export function html2Text(val) { 141 | const div = document.createElement('div') 142 | div.innerHTML = val 143 | return div.textContent || div.innerText 144 | } 145 | 146 | export function objectMerge(target, source) { 147 | /* Merges two objects, 148 | giving the last one precedence */ 149 | 150 | if (typeof target !== 'object') { 151 | target = {} 152 | } 153 | if (Array.isArray(source)) { 154 | return source.slice() 155 | } 156 | Object.keys(source).forEach(property => { 157 | const sourceProperty = source[property] 158 | if (typeof sourceProperty === 'object') { 159 | target[property] = objectMerge(target[property], sourceProperty) 160 | } else { 161 | target[property] = sourceProperty 162 | } 163 | }) 164 | return target 165 | } 166 | 167 | export function toggleClass(element, className) { 168 | if (!element || !className) { 169 | return 170 | } 171 | let classString = element.className 172 | const nameIndex = classString.indexOf(className) 173 | if (nameIndex === -1) { 174 | classString += '' + className 175 | } else { 176 | classString = 177 | classString.substr(0, nameIndex) + 178 | classString.substr(nameIndex + className.length) 179 | } 180 | element.className = classString 181 | } 182 | 183 | export const pickerOptions = [ 184 | { 185 | text: '今天', 186 | onClick(picker) { 187 | const end = new Date() 188 | const start = new Date(new Date().toDateString()) 189 | end.setTime(start.getTime()) 190 | picker.$emit('pick', [start, end]) 191 | } 192 | }, 193 | { 194 | text: '最近一周', 195 | onClick(picker) { 196 | const end = new Date(new Date().toDateString()) 197 | const start = new Date() 198 | start.setTime(end.getTime() - 3600 * 1000 * 24 * 7) 199 | picker.$emit('pick', [start, end]) 200 | } 201 | }, 202 | { 203 | text: '最近一个月', 204 | onClick(picker) { 205 | const end = new Date(new Date().toDateString()) 206 | const start = new Date() 207 | start.setTime(start.getTime() - 3600 * 1000 * 24 * 30) 208 | picker.$emit('pick', [start, end]) 209 | } 210 | }, 211 | { 212 | text: '最近三个月', 213 | onClick(picker) { 214 | const end = new Date(new Date().toDateString()) 215 | const start = new Date() 216 | start.setTime(start.getTime() - 3600 * 1000 * 24 * 90) 217 | picker.$emit('pick', [start, end]) 218 | } 219 | } 220 | ] 221 | 222 | export function getTime(type) { 223 | if (type === 'start') { 224 | return new Date().getTime() - 3600 * 1000 * 24 * 90 225 | } else { 226 | return new Date(new Date().toDateString()) 227 | } 228 | } 229 | 230 | export function debounce(func, wait, immediate) { 231 | let timeout, args, context, timestamp, result 232 | 233 | const later = function() { 234 | // 据上一次触发时间间隔 235 | const last = +new Date() - timestamp 236 | 237 | // 上次被包装函数被调用时间间隔last小于设定时间间隔wait 238 | if (last < wait && last > 0) { 239 | timeout = setTimeout(later, wait - last) 240 | } else { 241 | timeout = null 242 | // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用 243 | if (!immediate) { 244 | result = func.apply(context, args) 245 | if (!timeout) context = args = null 246 | } 247 | } 248 | } 249 | 250 | return function(...args) { 251 | context = this 252 | timestamp = +new Date() 253 | const callNow = immediate && !timeout 254 | // 如果延时不存在,重新设定延时 255 | if (!timeout) timeout = setTimeout(later, wait) 256 | if (callNow) { 257 | result = func.apply(context, args) 258 | context = args = null 259 | } 260 | 261 | return result 262 | } 263 | } 264 | 265 | /** 266 | * This is just a simple version of deep copy 267 | * Has a lot of edge cases bug 268 | * If you want to use a perfect deep copy, use lodash's _.cloneDeep 269 | */ 270 | export function deepClone(source) { 271 | if (!source && typeof source !== 'object') { 272 | throw new Error('error arguments', 'shallowClone') 273 | } 274 | const targetObj = source.constructor === Array ? [] : {} 275 | Object.keys(source).forEach(keys => { 276 | if (source[keys] && typeof source[keys] === 'object') { 277 | targetObj[keys] = deepClone(source[keys]) 278 | } else { 279 | targetObj[keys] = source[keys] 280 | } 281 | }) 282 | return targetObj 283 | } 284 | 285 | export function uniqueArr(arr) { 286 | return Array.from(new Set(arr)) 287 | } 288 | 289 | export function isExternal(path) { 290 | return /^(https?:|mailto:|tel:)/.test(path) 291 | } 292 | -------------------------------------------------------------------------------- /src/utils/openWindow.js: -------------------------------------------------------------------------------- 1 | /** 2 | *Created by jiachenpan on 16/11/29. 3 | * @param {Sting} url 4 | * @param {Sting} title 5 | * @param {Number} w 6 | * @param {Number} h 7 | */ 8 | 9 | export default function openWindow(url, title, w, h) { 10 | // Fixes dual-screen position Most browsers Firefox 11 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left 12 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top 13 | 14 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width 15 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height 16 | 17 | const left = ((width / 2) - (w / 2)) + dualScreenLeft 18 | const top = ((height / 2) - (h / 2)) + dualScreenTop 19 | const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left) 20 | 21 | // Puts focus on the newWindow 22 | if (window.focus) { 23 | newWindow.focus() 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import store from 'src/store' 2 | 3 | /** 4 | * @param {Array} value 5 | * @returns {Boolean} 6 | * @example see @/views/permission/directive.vue 7 | */ 8 | export default function checkPermission(value) { 9 | if (value && value instanceof Array && value.length > 0) { 10 | const roles = store.getters && store.getters.roles 11 | const permissionRoles = value 12 | 13 | const hasPermission = roles.some(role => { 14 | return permissionRoles.includes(role) 15 | }) 16 | 17 | if (!hasPermission) { 18 | return false 19 | } 20 | return true 21 | } else { 22 | console.error(`need roles! Like v-permission="['admin','editor']"`) 23 | return false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | // import axios from 'axios' 2 | import { Message, MessageBox } from 'element-ui' 3 | import store from '../store' 4 | 5 | // create an axios instance 6 | const service = axios.create({ 7 | timeout: 50000 // request timeout 8 | }) 9 | 10 | // 是否正在弹窗 11 | let isLoginTimeout = false 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // api 的 base_url 17 | // 在此处设置baseURL,避免直接依赖store,如果放在上面create方法,store为空 18 | config.baseURL = store.getters.baseUrl 19 | 20 | // 所有请求默认是json格式,除了上传文件,不用表单 21 | config.headers['Content-Type'] = 'application/json; charset=UTF-8' 22 | 23 | // Do something before request is sent 24 | if (store.getters.token) { 25 | // 让每个请求携带token-- ['Authorization']为自定义key 请根据实际情况自行修改 26 | config.headers['Authorization'] = store.getters.token 27 | } 28 | return config 29 | }, 30 | error => { 31 | // Do something with request error 32 | // console.log(error) // for debug 33 | Message({ 34 | message: error, 35 | type: 'error', 36 | duration: 5 * 1000 37 | }) 38 | Promise.reject(error) 39 | } 40 | ) 41 | 42 | // response interceptor 43 | service.interceptors.response.use( 44 | // response => response, 45 | /** 46 | * 下面的注释为通过在response里,自定义code来标示请求状态 47 | * 当code返回如下情况则说明权限有问题,登出并返回到登录页 48 | * 如想通过 xmlhttprequest 来状态码标识 逻辑可写在下面error中 49 | * 以下代码均为样例,请结合自生需求加以修改,若不需要,则可删除 50 | */ 51 | response => { 52 | const res = response.data 53 | if (res.Status && res.Status !== 0) { 54 | if (res.Status >= 500) { 55 | Message({ 56 | message: '后端服务错误', 57 | type: 'error', 58 | duration: 5 * 1000 59 | }) 60 | return Promise.reject('后端服务错误') 61 | } else if (res.Status === 203) { 62 | // 请自行在引入 MessageBox 63 | // import { Message, MessageBox } from 'element-ui' 64 | // 如果已弹窗,不重复弹窗 65 | if (!isLoginTimeout) { 66 | isLoginTimeout = true 67 | // 如果当前就是login、页面则不作处理 68 | if (location.pathname.indexOf('/login') > 0) { 69 | return response 70 | } 71 | MessageBox.confirm( 72 | '你已被登出或者登陆失效,可以取消继续留在该页面,或者重新登录', 73 | '确定登出', 74 | { 75 | confirmButtonText: '重新登录', 76 | cancelButtonText: '取消', 77 | type: 'warning' 78 | } 79 | ).then(() => { 80 | isLoginTimeout = false 81 | store.dispatch('FedLogOut').then(() => { 82 | location.reload() // 为了重新实例化vue-router对象 避免bug 83 | }) 84 | }) 85 | } else { 86 | return response 87 | } 88 | } else { 89 | Message({ 90 | message: res.Msg, 91 | type: 'error', 92 | duration: 5 * 1000 93 | }) 94 | return Promise.reject(res.Msg) 95 | } 96 | } else { 97 | return response 98 | } 99 | }, 100 | error => { 101 | // console.log('err' + error) // for debug 102 | Message({ 103 | message: '请求错误', 104 | type: 'error', 105 | duration: 5 * 1000 106 | }) 107 | return Promise.reject(error) 108 | } 109 | ) 110 | 111 | export default service 112 | -------------------------------------------------------------------------------- /src/utils/scrollTo.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | // because it's so fucking difficult to detect the scrolling element, just move them all 16 | function move(amount) { 17 | document.documentElement.scrollTop = amount 18 | document.body.parentNode.scrollTop = amount 19 | document.body.scrollTop = amount 20 | } 21 | 22 | function position() { 23 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 24 | } 25 | 26 | export function scrollTo(to, duration, callback) { 27 | const start = position() 28 | const change = to - start 29 | const increment = 20 30 | let currentTime = 0 31 | duration = (typeof (duration) === 'undefined') ? 500 : duration 32 | var animateScroll = function() { 33 | // increment the time 34 | currentTime += increment 35 | // find the value with the quadratic in-out easing function 36 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 37 | // move the document.body 38 | move(val) 39 | // do the animation unless its over 40 | if (currentTime < duration) { 41 | requestAnimFrame(animateScroll) 42 | } else { 43 | if (callback && typeof (callback) === 'function') { 44 | // the animation is done so lets callback 45 | callback() 46 | } 47 | } 48 | } 49 | animateScroll() 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | export function getItem(key) { 2 | return localStorage.getItem(key) 3 | } 4 | 5 | export function setItem(key, token) { 6 | return localStorage.setItem(key, token) 7 | } 8 | 9 | export function removeItem(key) { 10 | return localStorage.removeItem(key) 11 | } 12 | 13 | export default { 14 | getItem, 15 | setItem, 16 | removeItem 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by jiachenpan on 16/11/18. 3 | */ 4 | 5 | export function isvalidUsername(str) { 6 | return true 7 | } 8 | 9 | /* 合法uri*/ 10 | export function validateURL(textval) { 11 | 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.,?'\\+&%$#=~_-]+))*$/ 12 | return urlregex.test(textval) 13 | } 14 | 15 | /* 小写字母*/ 16 | export function validateLowerCase(str) { 17 | const reg = /^[a-z]+$/ 18 | return reg.test(str) 19 | } 20 | 21 | /* 大写字母*/ 22 | export function validateUpperCase(str) { 23 | const reg = /^[A-Z]+$/ 24 | return reg.test(str) 25 | } 26 | 27 | /* 大小写字母*/ 28 | export function validateAlphabets(str) { 29 | const reg = /^[A-Za-z]+$/ 30 | return reg.test(str) 31 | } 32 | 33 | /** 34 | * validate email 35 | * @param email 36 | * @returns {boolean} 37 | */ 38 | export function validateEmail(email) { 39 | const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 40 | return re.test(email) 41 | } 42 | -------------------------------------------------------------------------------- /src/views/Menu/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 44 | -------------------------------------------------------------------------------- /src/views/Role/form.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 216 | 229 | 230 | 231 | 240 | -------------------------------------------------------------------------------- /src/views/common/formBase.vue: -------------------------------------------------------------------------------- 1 | 70 | 167 | -------------------------------------------------------------------------------- /src/views/common/tableBase.vue: -------------------------------------------------------------------------------- 1 | 94 | 285 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | -------------------------------------------------------------------------------- /src/views/errorLog/errorTestA.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/views/errorLog/errorTestB.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/views/errorLog/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /src/views/errorPage/401.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 74 | 75 | 114 | -------------------------------------------------------------------------------- /src/views/errorPage/404.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 40 | 41 | 235 | -------------------------------------------------------------------------------- /src/views/guide/defineSteps.js: -------------------------------------------------------------------------------- 1 | const steps = [ 2 | { 3 | element: '.hamburger-container', 4 | popover: { 5 | title: 'Hamburger', 6 | description: 'Open && Close sidebar', 7 | position: 'bottom' 8 | } 9 | }, 10 | { 11 | element: '.breadcrumb-container', 12 | popover: { 13 | title: 'Breadcrumb', 14 | description: 'Indicate the current page location', 15 | position: 'bottom' 16 | } 17 | }, 18 | { 19 | element: '.screenfull', 20 | popover: { 21 | title: 'Screenfull', 22 | description: 'Bring the page into fullscreen', 23 | position: 'left' 24 | } 25 | }, 26 | { 27 | element: '.international-icon', 28 | popover: { 29 | title: 'Switch language', 30 | description: 'Switch the system language', 31 | position: 'left' 32 | } 33 | }, 34 | { 35 | element: '.theme-switch', 36 | popover: { 37 | title: 'Theme Switch', 38 | description: 'Custom switch system theme', 39 | position: 'left' 40 | } 41 | }, 42 | { 43 | element: '.tags-view-container', 44 | popover: { 45 | title: 'Tags view', 46 | description: 'The history of the page you visited', 47 | position: 'bottom' 48 | } 49 | } 50 | ] 51 | 52 | export default steps 53 | -------------------------------------------------------------------------------- /src/views/guide/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 42 | -------------------------------------------------------------------------------- /src/views/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 56 | 57 | 81 | -------------------------------------------------------------------------------- /src/views/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 34 | 35 | -------------------------------------------------------------------------------- /src/views/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 116 | 117 | 178 | -------------------------------------------------------------------------------- /src/views/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 mouseeleave 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/views/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /src/views/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 40 | -------------------------------------------------------------------------------- /src/views/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 135 | -------------------------------------------------------------------------------- /src/views/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | -------------------------------------------------------------------------------- /src/views/layout/components/TagsView.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 155 | 156 | 223 | 224 | 249 | -------------------------------------------------------------------------------- /src/views/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from 'src/views/layout/components/Navbar' 2 | export { default as Sidebar } from 'src/views/layout/components/Sidebar/index.vue' 3 | export { default as TagsView } from 'src/views/layout/components/TagsView' 4 | export { default as AppMain } from 'src/views/layout/components/AppMain' 5 | -------------------------------------------------------------------------------- /src/views/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from 'src/store' 2 | 3 | const { body } = document 4 | const WIDTH = 1024 5 | const RATIO = 3 6 | 7 | export default { 8 | watch: { 9 | $route(route) { 10 | if (this.device === 'mobile' && this.sidebar.opened) { 11 | store.dispatch('closeSideBar', { withoutAnimation: false }) 12 | } 13 | } 14 | }, 15 | beforeMount() { 16 | window.addEventListener('resize', this.resizeHandler) 17 | }, 18 | mounted() { 19 | const isMobile = this.isMobile() 20 | if (isMobile) { 21 | store.dispatch('toggleDevice', 'mobile') 22 | store.dispatch('closeSideBar', { withoutAnimation: true }) 23 | } 24 | }, 25 | methods: { 26 | isMobile() { 27 | const rect = body.getBoundingClientRect() 28 | return rect.width - RATIO < WIDTH 29 | }, 30 | resizeHandler() { 31 | if (!document.hidden) { 32 | const isMobile = this.isMobile() 33 | store.dispatch('toggleDevice', isMobile ? 'mobile' : 'desktop') 34 | 35 | if (isMobile) { 36 | store.dispatch('closeSideBar', { withoutAnimation: true }) 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/views/login/authredirect.vue: -------------------------------------------------------------------------------- 1 |  33 | -------------------------------------------------------------------------------- /src/views/login/socialsignin.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 66 | 67 | 105 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | function resolve(dir) { 4 | return path.join(__dirname, '.', dir) 5 | } 6 | 7 | // development、production。生产模式使用dist路径 8 | // 不再做此设置,一切由服务端决定 9 | // const publicPath = process.env.NODE_ENV === 'production' ? '/dist' : '/' 10 | 11 | module.exports = { 12 | chainWebpack: config => { 13 | config.module.rules.delete('svg') // 重点:删除默认配置中处理svg 14 | // svg配置 15 | config.module 16 | .rule('svg-sprite-loader') 17 | .test(/\.svg$/) 18 | .include.add(resolve('src/icons')) // 处理svg目录 19 | .end() 20 | .use('svg-sprite-loader') 21 | .loader('svg-sprite-loader') 22 | .options({ 23 | symbolId: 'icon-[name]' 24 | }) 25 | 26 | // 配置模块搜索目录 27 | config.resolve.modules.add(resolve('./')) 28 | 29 | config.optimization.splitChunks({ 30 | // chunks: 'all' 31 | // minSize: 4000000, 32 | // enforceSizeThreshold: 400000000, 33 | // cacheGroups: { 34 | // default: { 35 | // test: /[\\/]node_modules[\\/]/, 36 | // chunks: 'all' 37 | // } 38 | // } 39 | // maxSize: 3000000 40 | }) 41 | // config.optimization.runtimeChunk('single') 42 | 43 | // config.optimization.minimize(true) 44 | 45 | // 用cdn方式引入 46 | config.externals({ 47 | 'vue': 'Vue', 48 | 'vuex': 'Vuex', 49 | 'vue-router': 'VueRouter', 50 | 'axios': 'axios' 51 | }) 52 | }, 53 | configureWebpack: config => { 54 | // 编译多个文件 55 | const entry = config.entry 56 | entry['index'] = './src/index.js' 57 | entry['elementui'] = 'element-ui' 58 | }, 59 | // devServer: { 60 | // proxy: 'http://localhost:44336/', 61 | // https: false 62 | // }, 63 | // outputDir: '../Easy.Admin/wwwroot/dist', 64 | // publicPath: publicPath, 65 | // 导入vue包含编译器 https://cli.vuejs.org/zh/config/#runtimecompiler 66 | runtimeCompiler: true, 67 | css: { 68 | extract: true 69 | } 70 | } 71 | --------------------------------------------------------------------------------