| @ -0,0 +1,14 @@ | ||||
| # http://editorconfig.org | ||||
| root = true | ||||
| 
 | ||||
| [*] | ||||
| charset = utf-8 | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
| end_of_line = lf | ||||
| insert_final_newline = true | ||||
| trim_trailing_whitespace = true | ||||
| 
 | ||||
| [*.md] | ||||
| insert_final_newline = false | ||||
| trim_trailing_whitespace = false | ||||
| @ -0,0 +1,14 @@ | ||||
| # just a flag | ||||
| ENV = 'development' | ||||
| 
 | ||||
| # base api | ||||
| VUE_APP_BASE_API = 'http://localhost:8082/api' | ||||
| 
 | ||||
| # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, | ||||
| # to control whether the babel-plugin-dynamic-import-node plugin is enabled. | ||||
| # It only does one thing by converting all import() to require(). | ||||
| # This configuration can significantly increase the speed of hot updates, | ||||
| # when you have a large number of pages. | ||||
| # Detail:  https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js | ||||
| 
 | ||||
| VUE_CLI_BABEL_TRANSPILE_MODULES = true | ||||
| @ -0,0 +1,6 @@ | ||||
| # just a flag | ||||
| ENV = 'production' | ||||
| 
 | ||||
| # base api | ||||
| VUE_APP_BASE_API = 'http://localhost:8082/api' | ||||
| 
 | ||||
| @ -0,0 +1,8 @@ | ||||
| NODE_ENV = production | ||||
| 
 | ||||
| # just a flag | ||||
| ENV = 'staging' | ||||
| 
 | ||||
| # base api | ||||
| VUE_APP_BASE_API = '/stage-api' | ||||
| 
 | ||||
| @ -0,0 +1,4 @@ | ||||
| build/*.js | ||||
| src/assets | ||||
| public | ||||
| dist | ||||
| @ -0,0 +1,198 @@ | ||||
| module.exports = { | ||||
|   root: true, | ||||
|   parserOptions: { | ||||
|     parser: 'babel-eslint', | ||||
|     sourceType: 'module' | ||||
|   }, | ||||
|   env: { | ||||
|     browser: true, | ||||
|     node: true, | ||||
|     es6: true, | ||||
|   }, | ||||
|   extends: ['plugin:vue/recommended', 'eslint:recommended'], | ||||
| 
 | ||||
|   // add your custom rules here
 | ||||
|   //it is base on https://github.com/vuejs/eslint-config-vue
 | ||||
|   rules: { | ||||
|     "vue/max-attributes-per-line": [2, { | ||||
|       "singleline": 10, | ||||
|       "multiline": { | ||||
|         "max": 1, | ||||
|         "allowFirstLine": false | ||||
|       } | ||||
|     }], | ||||
|     "vue/singleline-html-element-content-newline": "off", | ||||
|     "vue/multiline-html-element-content-newline":"off", | ||||
|     "vue/name-property-casing": ["error", "PascalCase"], | ||||
|     "vue/no-v-html": "off", | ||||
|     'accessor-pairs': 2, | ||||
|     'arrow-spacing': [2, { | ||||
|       'before': true, | ||||
|       'after': true | ||||
|     }], | ||||
|     'block-spacing': [2, 'always'], | ||||
|     'brace-style': [2, '1tbs', { | ||||
|       'allowSingleLine': true | ||||
|     }], | ||||
|     'camelcase': [0, { | ||||
|       'properties': 'always' | ||||
|     }], | ||||
|     'comma-dangle': [2, 'never'], | ||||
|     'comma-spacing': [2, { | ||||
|       'before': false, | ||||
|       'after': true | ||||
|     }], | ||||
|     'comma-style': [2, 'last'], | ||||
|     'constructor-super': 2, | ||||
|     'curly': [2, 'multi-line'], | ||||
|     'dot-location': [2, 'property'], | ||||
|     'eol-last': 2, | ||||
|     'eqeqeq': ["error", "always", {"null": "ignore"}], | ||||
|     'generator-star-spacing': [2, { | ||||
|       'before': true, | ||||
|       'after': true | ||||
|     }], | ||||
|     'handle-callback-err': [2, '^(err|error)$'], | ||||
|     'indent': [2, 2, { | ||||
|       'SwitchCase': 1 | ||||
|     }], | ||||
|     'jsx-quotes': [2, 'prefer-single'], | ||||
|     'key-spacing': [2, { | ||||
|       'beforeColon': false, | ||||
|       'afterColon': true | ||||
|     }], | ||||
|     'keyword-spacing': [2, { | ||||
|       'before': true, | ||||
|       'after': true | ||||
|     }], | ||||
|     'new-cap': [2, { | ||||
|       'newIsCap': true, | ||||
|       'capIsNew': false | ||||
|     }], | ||||
|     'new-parens': 2, | ||||
|     'no-array-constructor': 2, | ||||
|     'no-caller': 2, | ||||
|     'no-console': 'off', | ||||
|     'no-class-assign': 2, | ||||
|     'no-cond-assign': 2, | ||||
|     'no-const-assign': 2, | ||||
|     'no-control-regex': 0, | ||||
|     'no-delete-var': 2, | ||||
|     'no-dupe-args': 2, | ||||
|     'no-dupe-class-members': 2, | ||||
|     'no-dupe-keys': 2, | ||||
|     'no-duplicate-case': 2, | ||||
|     'no-empty-character-class': 2, | ||||
|     'no-empty-pattern': 2, | ||||
|     'no-eval': 2, | ||||
|     'no-ex-assign': 2, | ||||
|     'no-extend-native': 2, | ||||
|     'no-extra-bind': 2, | ||||
|     'no-extra-boolean-cast': 2, | ||||
|     'no-extra-parens': [2, 'functions'], | ||||
|     'no-fallthrough': 2, | ||||
|     'no-floating-decimal': 2, | ||||
|     'no-func-assign': 2, | ||||
|     'no-implied-eval': 2, | ||||
|     'no-inner-declarations': [2, 'functions'], | ||||
|     'no-invalid-regexp': 2, | ||||
|     'no-irregular-whitespace': 2, | ||||
|     'no-iterator': 2, | ||||
|     'no-label-var': 2, | ||||
|     'no-labels': [2, { | ||||
|       'allowLoop': false, | ||||
|       'allowSwitch': false | ||||
|     }], | ||||
|     'no-lone-blocks': 2, | ||||
|     'no-mixed-spaces-and-tabs': 2, | ||||
|     'no-multi-spaces': 2, | ||||
|     'no-multi-str': 2, | ||||
|     'no-multiple-empty-lines': [2, { | ||||
|       'max': 1 | ||||
|     }], | ||||
|     'no-native-reassign': 2, | ||||
|     'no-negated-in-lhs': 2, | ||||
|     'no-new-object': 2, | ||||
|     'no-new-require': 2, | ||||
|     'no-new-symbol': 2, | ||||
|     'no-new-wrappers': 2, | ||||
|     'no-obj-calls': 2, | ||||
|     'no-octal': 2, | ||||
|     'no-octal-escape': 2, | ||||
|     'no-path-concat': 2, | ||||
|     'no-proto': 2, | ||||
|     'no-redeclare': 2, | ||||
|     'no-regex-spaces': 2, | ||||
|     'no-return-assign': [2, 'except-parens'], | ||||
|     'no-self-assign': 2, | ||||
|     'no-self-compare': 2, | ||||
|     'no-sequences': 2, | ||||
|     'no-shadow-restricted-names': 2, | ||||
|     'no-spaced-func': 2, | ||||
|     'no-sparse-arrays': 2, | ||||
|     'no-this-before-super': 2, | ||||
|     'no-throw-literal': 2, | ||||
|     'no-trailing-spaces': 2, | ||||
|     'no-undef': 2, | ||||
|     'no-undef-init': 2, | ||||
|     'no-unexpected-multiline': 2, | ||||
|     'no-unmodified-loop-condition': 2, | ||||
|     'no-unneeded-ternary': [2, { | ||||
|       'defaultAssignment': false | ||||
|     }], | ||||
|     'no-unreachable': 2, | ||||
|     'no-unsafe-finally': 2, | ||||
|     'no-unused-vars': [2, { | ||||
|       'vars': 'all', | ||||
|       'args': 'none' | ||||
|     }], | ||||
|     'no-useless-call': 2, | ||||
|     'no-useless-computed-key': 2, | ||||
|     'no-useless-constructor': 2, | ||||
|     'no-useless-escape': 0, | ||||
|     'no-whitespace-before-property': 2, | ||||
|     'no-with': 2, | ||||
|     'one-var': [2, { | ||||
|       'initialized': 'never' | ||||
|     }], | ||||
|     'operator-linebreak': [2, 'after', { | ||||
|       'overrides': { | ||||
|         '?': 'before', | ||||
|         ':': 'before' | ||||
|       } | ||||
|     }], | ||||
|     'padded-blocks': [2, 'never'], | ||||
|     'quotes': [2, 'single', { | ||||
|       'avoidEscape': true, | ||||
|       'allowTemplateLiterals': true | ||||
|     }], | ||||
|     'semi': [2, 'never'], | ||||
|     'semi-spacing': [2, { | ||||
|       'before': false, | ||||
|       'after': true | ||||
|     }], | ||||
|     'space-before-blocks': [2, 'always'], | ||||
|     'space-before-function-paren': [2, 'never'], | ||||
|     'space-in-parens': [2, 'never'], | ||||
|     'space-infix-ops': 2, | ||||
|     'space-unary-ops': [2, { | ||||
|       'words': true, | ||||
|       'nonwords': false | ||||
|     }], | ||||
|     'spaced-comment': [2, 'always', { | ||||
|       'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] | ||||
|     }], | ||||
|     'template-curly-spacing': [2, 'never'], | ||||
|     'use-isnan': 2, | ||||
|     'valid-typeof': 2, | ||||
|     'wrap-iife': [2, 'any'], | ||||
|     'yield-star-spacing': [2, 'both'], | ||||
|     'yoda': [2, 'never'], | ||||
|     'prefer-const': 2, | ||||
|     'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, | ||||
|     'object-curly-spacing': [2, 'always', { | ||||
|       objectsInObjects: false | ||||
|     }], | ||||
|     'array-bracket-spacing': [2, 'never'] | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,16 @@ | ||||
| .DS_Store | ||||
| node_modules/ | ||||
| dist/ | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| package-lock.json | ||||
| tests/**/coverage/ | ||||
| 
 | ||||
| # Editor directories and files | ||||
| .idea | ||||
| .vscode | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| @ -0,0 +1,8 @@ | ||||
| // https://github.com/michael-ciniawsky/postcss-load-config
 | ||||
| 
 | ||||
| module.exports = { | ||||
|   'plugins': { | ||||
|     // to edit target browsers: use "browserslist" field in package.json
 | ||||
|     'autoprefixer': {} | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,5 @@ | ||||
| language: node_js | ||||
| node_js: 10 | ||||
| script: npm run test | ||||
| notifications: | ||||
|   email: false | ||||
| @ -0,0 +1,21 @@ | ||||
| MIT License | ||||
| 
 | ||||
| Copyright (c) 2017-present PanJiaChen | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
| @ -0,0 +1,33 @@ | ||||
| # SOP Admin 前端vue实现 | ||||
| 
 | ||||
|   前提:先安装好npm,[npm安装教程](https://blog.csdn.net/zhangwenwu2/article/details/52778521) | ||||
| 
 | ||||
| 1. 启动服务端程序,运行`SopAdminServerApplication.java` | ||||
| 2. `cd sop-admin-vue` | ||||
| 3. 执行`npm install --registry=https://registry.npm.taobao.org` | ||||
| 4. 执行`npm run dev`,访问`http://localhost:9528/`,用户名密码:`admin/123456` | ||||
| 
 | ||||
| ``` | ||||
| # 建议不要用cnpm  安装有各种诡异的bug 可以通过如下操作解决npm速度慢的问题 | ||||
| npm install --registry=https://registry.npm.taobao.org | ||||
| 
 | ||||
| # 开发调试 localhost:9528 | ||||
| npm run dev | ||||
| 
 | ||||
| # 打包发布 | ||||
| npm run build | ||||
| 
 | ||||
| # Build for production and view the bundle analyzer report | ||||
| npm run build --report | ||||
| ``` | ||||
| 
 | ||||
| - 修改端口号:打开`vue.config.js`,找到`port`属性 | ||||
| 
 | ||||
| ## 打包放入到服务端步骤 | ||||
| 
 | ||||
| 如果想要把vue打包放到服务端,步骤如下: | ||||
| 
 | ||||
| - 打开`vue.config.js`,找到`build`下的`assetsPublicPath`参数,设置成'./' | ||||
| - 打开`.env.production`,配置`VUE_APP_BASE_API`参数,设置成'/api' | ||||
| - 执行`npm run build`进行打包,结果在dest下 | ||||
| - 打包完成后,把dest中的所有文件,放到`sop-admin-server/src/main/resources/public`下 | ||||
| @ -0,0 +1,5 @@ | ||||
| module.exports = { | ||||
|   presets: [ | ||||
|     '@vue/app' | ||||
|   ] | ||||
| } | ||||
| @ -0,0 +1,35 @@ | ||||
| const { run } = require('runjs') | ||||
| const chalk = require('chalk') | ||||
| const config = require('../vue.config.js') | ||||
| const rawArgv = process.argv.slice(2) | ||||
| const args = rawArgv.join(' ') | ||||
| 
 | ||||
| if (process.env.npm_config_preview || rawArgv.includes('--preview')) { | ||||
|   const report = rawArgv.includes('--report') | ||||
| 
 | ||||
|   run(`vue-cli-service build ${args}`) | ||||
| 
 | ||||
|   const port = 9526 | ||||
|   const publicPath = config.publicPath | ||||
| 
 | ||||
|   var connect = require('connect') | ||||
|   var serveStatic = require('serve-static') | ||||
|   const app = connect() | ||||
| 
 | ||||
|   app.use( | ||||
|     publicPath, | ||||
|     serveStatic('./dist', { | ||||
|       index: ['index.html', '/'] | ||||
|     }) | ||||
|   ) | ||||
| 
 | ||||
|   app.listen(port, function () { | ||||
|     console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`)) | ||||
|     if (report) { | ||||
|       console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`)) | ||||
|     } | ||||
| 
 | ||||
|   }) | ||||
| } else { | ||||
|   run(`vue-cli-service build ${args}`) | ||||
| } | ||||
| @ -0,0 +1,24 @@ | ||||
| module.exports = { | ||||
|   moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], | ||||
|   transform: { | ||||
|     '^.+\\.vue$': 'vue-jest', | ||||
|     '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': | ||||
|       'jest-transform-stub', | ||||
|     '^.+\\.jsx?$': 'babel-jest' | ||||
|   }, | ||||
|   moduleNameMapper: { | ||||
|     '^@/(.*)$': '<rootDir>/src/$1' | ||||
|   }, | ||||
|   snapshotSerializers: ['jest-serializer-vue'], | ||||
|   testMatch: [ | ||||
|     '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' | ||||
|   ], | ||||
|   collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], | ||||
|   coverageDirectory: '<rootDir>/tests/unit/coverage', | ||||
|   // 'collectCoverage': true,
 | ||||
|   'coverageReporters': [ | ||||
|     'lcov', | ||||
|     'text-summary' | ||||
|   ], | ||||
|   testURL: 'http://localhost/' | ||||
| } | ||||
| @ -0,0 +1,66 @@ | ||||
| import Mock from 'mockjs' | ||||
| import { param2Obj } from '../src/utils' | ||||
| 
 | ||||
| import user from './user' | ||||
| import table from './table' | ||||
| 
 | ||||
| const mocks = [ | ||||
|   ...user, | ||||
|   ...table | ||||
| ] | ||||
| 
 | ||||
| // for front mock
 | ||||
| // please use it cautiously, it will redefine XMLHttpRequest,
 | ||||
| // which will cause many of your third-party libraries to be invalidated(like progress event).
 | ||||
| export function mockXHR() { | ||||
|   // mock patch
 | ||||
|   // https://github.com/nuysoft/Mock/issues/300
 | ||||
|   Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send | ||||
|   Mock.XHR.prototype.send = function() { | ||||
|     if (this.custom.xhr) { | ||||
|       this.custom.xhr.withCredentials = this.withCredentials || false | ||||
| 
 | ||||
|       if (this.responseType) { | ||||
|         this.custom.xhr.responseType = this.responseType | ||||
|       } | ||||
|     } | ||||
|     this.proxy_send(...arguments) | ||||
|   } | ||||
| 
 | ||||
|   function XHR2ExpressReqWrap(respond) { | ||||
|     return function(options) { | ||||
|       let result = null | ||||
|       if (respond instanceof Function) { | ||||
|         const { body, type, url } = options | ||||
|         // https://expressjs.com/en/4x/api.html#req
 | ||||
|         result = respond({ | ||||
|           method: type, | ||||
|           body: JSON.parse(body), | ||||
|           query: param2Obj(url) | ||||
|         }) | ||||
|       } else { | ||||
|         result = respond | ||||
|       } | ||||
|       return Mock.mock(result) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   for (const i of mocks) { | ||||
|     Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // for mock server
 | ||||
| const responseFake = (url, type, respond) => { | ||||
|   return { | ||||
|     url: new RegExp(`/mock${url}`), | ||||
|     type: type || 'get', | ||||
|     response(req, res) { | ||||
|       res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default mocks.map(route => { | ||||
|   return responseFake(route.url, route.type, route.response) | ||||
| }) | ||||
| @ -0,0 +1,68 @@ | ||||
| const chokidar = require('chokidar') | ||||
| const bodyParser = require('body-parser') | ||||
| const chalk = require('chalk') | ||||
| const path = require('path') | ||||
| 
 | ||||
| const mockDir = path.join(process.cwd(), 'mock') | ||||
| 
 | ||||
| function registerRoutes(app) { | ||||
|   let mockLastIndex | ||||
|   const { default: mocks } = require('./index.js') | ||||
|   for (const mock of mocks) { | ||||
|     app[mock.type](mock.url, mock.response) | ||||
|     mockLastIndex = app._router.stack.length | ||||
|   } | ||||
|   const mockRoutesLength = Object.keys(mocks).length | ||||
|   return { | ||||
|     mockRoutesLength: mockRoutesLength, | ||||
|     mockStartIndex: mockLastIndex - mockRoutesLength | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function unregisterRoutes() { | ||||
|   Object.keys(require.cache).forEach(i => { | ||||
|     if (i.includes(mockDir)) { | ||||
|       delete require.cache[require.resolve(i)] | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| module.exports = app => { | ||||
|   // es6 polyfill
 | ||||
|   require('@babel/register') | ||||
| 
 | ||||
|   // parse app.body
 | ||||
|   // https://expressjs.com/en/4x/api.html#req.body
 | ||||
|   app.use(bodyParser.json()) | ||||
|   app.use(bodyParser.urlencoded({ | ||||
|     extended: true | ||||
|   })) | ||||
| 
 | ||||
|   const mockRoutes = registerRoutes(app) | ||||
|   var mockRoutesLength = mockRoutes.mockRoutesLength | ||||
|   var mockStartIndex = mockRoutes.mockStartIndex | ||||
| 
 | ||||
|   // watch files, hot reload mock server
 | ||||
|   chokidar.watch(mockDir, { | ||||
|     ignored: /mock-server/, | ||||
|     ignoreInitial: true | ||||
|   }).on('all', (event, path) => { | ||||
|     if (event === 'change' || event === 'add') { | ||||
|       try { | ||||
|         // remove mock routes stack
 | ||||
|         app._router.stack.splice(mockStartIndex, mockRoutesLength) | ||||
| 
 | ||||
|         // clear routes cache
 | ||||
|         unregisterRoutes() | ||||
| 
 | ||||
|         const mockRoutes = registerRoutes(app) | ||||
|         mockRoutesLength = mockRoutes.mockRoutesLength | ||||
|         mockStartIndex = mockRoutes.mockStartIndex | ||||
| 
 | ||||
|         console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`)) | ||||
|       } catch (error) { | ||||
|         console.log(chalk.redBright(error)) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| @ -0,0 +1,29 @@ | ||||
| import Mock from 'mockjs' | ||||
| 
 | ||||
| const data = Mock.mock({ | ||||
|   'items|30': [{ | ||||
|     id: '@id', | ||||
|     title: '@sentence(10, 20)', | ||||
|     'status|1': ['published', 'draft', 'deleted'], | ||||
|     author: 'name', | ||||
|     display_time: '@datetime', | ||||
|     pageviews: '@integer(300, 5000)' | ||||
|   }] | ||||
| }) | ||||
| 
 | ||||
| export default [ | ||||
|   { | ||||
|     url: '/table/list', | ||||
|     type: 'get', | ||||
|     response: config => { | ||||
|       const items = data.items | ||||
|       return { | ||||
|         code: 20000, | ||||
|         data: { | ||||
|           total: items.length, | ||||
|           items: items | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| ] | ||||
| @ -0,0 +1,84 @@ | ||||
| 
 | ||||
| const tokens = { | ||||
|   admin: { | ||||
|     token: 'admin-token' | ||||
|   }, | ||||
|   editor: { | ||||
|     token: 'editor-token' | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const users = { | ||||
|   'admin-token': { | ||||
|     roles: ['admin'], | ||||
|     introduction: 'I am a super administrator', | ||||
|     avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', | ||||
|     name: 'Super Admin' | ||||
|   }, | ||||
|   'editor-token': { | ||||
|     roles: ['editor'], | ||||
|     introduction: 'I am an editor', | ||||
|     avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', | ||||
|     name: 'Normal Editor' | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default [ | ||||
|   // user login
 | ||||
|   { | ||||
|     url: '/user/login', | ||||
|     type: 'post', | ||||
|     response: config => { | ||||
|       const { username } = config.body | ||||
|       const token = tokens[username] | ||||
| 
 | ||||
|       // mock error
 | ||||
|       if (!token) { | ||||
|         return { | ||||
|           code: 60204, | ||||
|           message: 'Account and password are incorrect.' | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return { | ||||
|         code: 20000, | ||||
|         data: token | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   // get user info
 | ||||
|   { | ||||
|     url: '/user/info\.*', | ||||
|     type: 'get', | ||||
|     response: config => { | ||||
|       const { token } = config.query | ||||
|       const info = users[token] | ||||
| 
 | ||||
|       // mock error
 | ||||
|       if (!info) { | ||||
|         return { | ||||
|           code: 50008, | ||||
|           message: 'Login failed, unable to get user details.' | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return { | ||||
|         code: 20000, | ||||
|         data: info | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   // user logout
 | ||||
|   { | ||||
|     url: '/user/logout', | ||||
|     type: 'post', | ||||
|     response: _ => { | ||||
|       return { | ||||
|         code: 20000, | ||||
|         data: 'success' | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| ] | ||||
| @ -0,0 +1,65 @@ | ||||
| { | ||||
|   "name": "sop-admin", | ||||
|   "version": "4.1.0", | ||||
|   "description": "sop admin", | ||||
|   "author": "tanghc", | ||||
|   "license": "MIT", | ||||
|   "scripts": { | ||||
|     "dev": "vue-cli-service serve", | ||||
|     "build:prod": "vue-cli-service build", | ||||
|     "build:stage": "vue-cli-service build --mode staging", | ||||
|     "preview": "node build/index.js --preview", | ||||
|     "lint": "eslint --ext .js,.vue src", | ||||
|     "test:unit": "jest --clearCache && vue-cli-service test:unit", | ||||
|     "test:ci": "npm run lint && npm run test:unit", | ||||
|     "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "axios": "0.18.0", | ||||
|     "element-ui": "2.7.2", | ||||
|     "js-cookie": "2.2.0", | ||||
|     "js-md5": "^0.7.3", | ||||
|     "normalize.css": "7.0.0", | ||||
|     "nprogress": "0.2.0", | ||||
|     "path-to-regexp": "2.4.0", | ||||
|     "vue": "2.6.10", | ||||
|     "vue-router": "3.0.6", | ||||
|     "vuex": "3.1.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "7.0.0", | ||||
|     "@babel/register": "7.0.0", | ||||
|     "@vue/cli-plugin-babel": "3.6.0", | ||||
|     "@vue/cli-plugin-eslint": "3.6.0", | ||||
|     "@vue/cli-plugin-unit-jest": "3.6.3", | ||||
|     "@vue/cli-service": "3.6.0", | ||||
|     "@vue/test-utils": "1.0.0-beta.29", | ||||
|     "babel-core": "7.0.0-bridge.0", | ||||
|     "babel-eslint": "10.0.1", | ||||
|     "babel-jest": "23.6.0", | ||||
|     "chalk": "2.4.2", | ||||
|     "connect": "3.6.6", | ||||
|     "eslint": "5.15.3", | ||||
|     "eslint-plugin-vue": "5.2.2", | ||||
|     "html-webpack-plugin": "3.2.0", | ||||
|     "mockjs": "1.0.1-beta3", | ||||
|     "node-sass": "^4.9.0", | ||||
|     "runjs": "^4.3.2", | ||||
|     "sass-loader": "^7.1.0", | ||||
|     "script-ext-html-webpack-plugin": "2.1.3", | ||||
|     "script-loader": "0.7.2", | ||||
|     "serve-static": "^1.13.2", | ||||
|     "svg-sprite-loader": "4.1.3", | ||||
|     "svgo": "1.2.2", | ||||
|     "vue-template-compiler": "2.6.10" | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": ">=8.9", | ||||
|     "npm": ">= 3.0.0" | ||||
|   }, | ||||
|   "browserslist": [ | ||||
|     "> 1%", | ||||
|     "last 2 versions", | ||||
|     "not ie <= 8" | ||||
|   ] | ||||
| } | ||||
| After Width: | Height: | Size: 66 KiB | 
| @ -0,0 +1,17 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> | ||||
|     <link rel="icon" href="<%= BASE_URL %>favicon.ico"> | ||||
|     <title><%= webpackConfig.name %></title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <noscript> | ||||
|       <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> | ||||
|     </noscript> | ||||
|     <div id="app"></div> | ||||
|     <!-- built files will be auto injected --> | ||||
|   </body> | ||||
| </html> | ||||
| @ -0,0 +1,11 @@ | ||||
| <template> | ||||
|   <div id="app"> | ||||
|     <router-view /> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   name: 'App' | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,9 @@ | ||||
| import request from '@/utils/request' | ||||
| 
 | ||||
| export function getList(params) { | ||||
|   return request({ | ||||
|     url: '/table/list', | ||||
|     method: 'get', | ||||
|     params | ||||
|   }) | ||||
| } | ||||
| @ -0,0 +1,24 @@ | ||||
| import request from '@/utils/request' | ||||
| 
 | ||||
| export function login(data) { | ||||
|   return request({ | ||||
|     url: '/user/login', | ||||
|     method: 'post', | ||||
|     data | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| export function getInfo(token) { | ||||
|   return request({ | ||||
|     url: '/user/info', | ||||
|     method: 'get', | ||||
|     params: { token } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| export function logout() { | ||||
|   return request({ | ||||
|     url: '/user/logout', | ||||
|     method: 'post' | ||||
|   }) | ||||
| } | ||||
| After Width: | Height: | Size: 96 KiB | 
| After Width: | Height: | Size: 4.7 KiB | 
| @ -0,0 +1,78 @@ | ||||
| <template> | ||||
|   <el-breadcrumb class="app-breadcrumb" separator="/"> | ||||
|     <transition-group name="breadcrumb"> | ||||
|       <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path"> | ||||
|         <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span> | ||||
|         <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a> | ||||
|       </el-breadcrumb-item> | ||||
|     </transition-group> | ||||
|   </el-breadcrumb> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import pathToRegexp from 'path-to-regexp' | ||||
| 
 | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       levelList: null | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     $route() { | ||||
|       this.getBreadcrumb() | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     this.getBreadcrumb() | ||||
|   }, | ||||
|   methods: { | ||||
|     getBreadcrumb() { | ||||
|       // only show routes with meta.title | ||||
|       let matched = this.$route.matched.filter(item => item.meta && item.meta.title) | ||||
|       const first = matched[0] | ||||
| 
 | ||||
|       if (!this.isDashboard(first)) { | ||||
|         matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched) | ||||
|       } | ||||
| 
 | ||||
|       this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false) | ||||
|     }, | ||||
|     isDashboard(route) { | ||||
|       const name = route && route.name | ||||
|       if (!name) { | ||||
|         return false | ||||
|       } | ||||
|       return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase() | ||||
|     }, | ||||
|     pathCompile(path) { | ||||
|       // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561 | ||||
|       const { params } = this.$route | ||||
|       var toPath = pathToRegexp.compile(path) | ||||
|       return toPath(params) | ||||
|     }, | ||||
|     handleLink(item) { | ||||
|       const { redirect, path } = item | ||||
|       if (redirect) { | ||||
|         this.$router.push(redirect) | ||||
|         return | ||||
|       } | ||||
|       this.$router.push(this.pathCompile(path)) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .app-breadcrumb.el-breadcrumb { | ||||
|   display: inline-block; | ||||
|   font-size: 14px; | ||||
|   line-height: 50px; | ||||
|   margin-left: 8px; | ||||
| 
 | ||||
|   .no-redirect { | ||||
|     color: #97a8be; | ||||
|     cursor: text; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @ -0,0 +1,44 @@ | ||||
| <template> | ||||
|   <div style="padding: 0 15px;" @click="toggleClick"> | ||||
|     <svg | ||||
|       :class="{'is-active':isActive}" | ||||
|       class="hamburger" | ||||
|       viewBox="0 0 1024 1024" | ||||
|       xmlns="http://www.w3.org/2000/svg" | ||||
|       width="64" | ||||
|       height="64" | ||||
|     > | ||||
|       <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" /> | ||||
|     </svg> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   name: 'Hamburger', | ||||
|   props: { | ||||
|     isActive: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     toggleClick() { | ||||
|       this.$emit('toggleClick') | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .hamburger { | ||||
|   display: inline-block; | ||||
|   vertical-align: middle; | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
| } | ||||
| 
 | ||||
| .hamburger.is-active { | ||||
|   transform: rotate(180deg); | ||||
| } | ||||
| </style> | ||||
| @ -0,0 +1,43 @@ | ||||
| <template> | ||||
|   <svg :class="svgClass" aria-hidden="true" v-on="$listeners"> | ||||
|     <use :xlink:href="iconName" /> | ||||
|   </svg> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   name: 'SvgIcon', | ||||
|   props: { | ||||
|     iconClass: { | ||||
|       type: String, | ||||
|       required: true | ||||
|     }, | ||||
|     className: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     iconName() { | ||||
|       return `#icon-${this.iconClass}` | ||||
|     }, | ||||
|     svgClass() { | ||||
|       if (this.className) { | ||||
|         return 'svg-icon ' + this.className | ||||
|       } else { | ||||
|         return 'svg-icon' | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .svg-icon { | ||||
|   width: 1em; | ||||
|   height: 1em; | ||||
|   vertical-align: -0.15em; | ||||
|   fill: currentColor; | ||||
|   overflow: hidden; | ||||
| } | ||||
| </style> | ||||
| @ -0,0 +1,9 @@ | ||||
| import Vue from 'vue' | ||||
| import SvgIcon from '@/components/SvgIcon'// svg component
 | ||||
| 
 | ||||
| // register globally
 | ||||
| Vue.component('svg-icon', SvgIcon) | ||||
| 
 | ||||
| const req = require.context('./svg', false, /\.svg$/) | ||||
| const requireAll = requireContext => requireContext.keys().map(requireContext) | ||||
| requireAll(req) | ||||
| After Width: | Height: | Size: 2.3 KiB | 
| After Width: | Height: | Size: 497 B | 
| After Width: | Height: | Size: 1.3 KiB | 
| After Width: | Height: | Size: 944 B | 
| After Width: | Height: | Size: 2.4 KiB | 
| After Width: | Height: | Size: 285 B | 
| After Width: | Height: | Size: 821 B | 
| After Width: | Height: | Size: 623 B | 
| After Width: | Height: | Size: 597 B | 
| After Width: | Height: | Size: 1.8 KiB | 
| After Width: | Height: | Size: 440 B | 
| @ -0,0 +1,22 @@ | ||||
| # replace default config | ||||
| 
 | ||||
| # multipass: true | ||||
| # full: true | ||||
| 
 | ||||
| plugins: | ||||
| 
 | ||||
|   # - name | ||||
|   # | ||||
|   # or: | ||||
|   # - name: false | ||||
|   # - name: true | ||||
|   # | ||||
|   # or: | ||||
|   # - name: | ||||
|   #     param1: 1 | ||||
|   #     param2: 2 | ||||
| 
 | ||||
| - removeAttrs: | ||||
|     attrs: | ||||
|       - 'fill' | ||||
|       - 'fill-rule' | ||||
| @ -0,0 +1,31 @@ | ||||
| <template> | ||||
|   <section class="app-main"> | ||||
|     <transition name="fade-transform" mode="out-in"> | ||||
|       <router-view :key="key" /> | ||||
|     </transition> | ||||
|   </section> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   name: 'AppMain', | ||||
|   computed: { | ||||
|     key() { | ||||
|       return this.$route.fullPath | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .app-main { | ||||
|   /*50 = navbar  */ | ||||
|   min-height: calc(100vh - 50px); | ||||
|   width: 100%; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
| } | ||||
| .fixed-header+.app-main { | ||||
|   padding-top: 50px; | ||||
| } | ||||
| </style> | ||||
| @ -0,0 +1,129 @@ | ||||
| <template> | ||||
|   <div class="navbar"> | ||||
|     <hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> | ||||
| 
 | ||||
|     <breadcrumb class="breadcrumb-container" /> | ||||
| 
 | ||||
|     <div class="right-menu"> | ||||
|       <el-button type="text" style="margin-right: 10px" @click="doLogout">退出</el-button> | ||||
|       <!--<el-dropdown class="avatar-container" trigger="click">--> | ||||
|         <!--<div class="avatar-wrapper">--> | ||||
|           <!--<img src="https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80'" class="user-avatar">--> | ||||
|           <!--<i class="user-avatar el-icon-s-custom"></i>--> | ||||
|         <!--</div>--> | ||||
|         <!--<el-dropdown-menu slot="dropdown" class="user-dropdown">--> | ||||
|           <!--<el-dropdown-item>--> | ||||
|             <!--<span style="display:block;" @click="logout">退出</span>--> | ||||
|           <!--</el-dropdown-item>--> | ||||
|         <!--</el-dropdown-menu>--> | ||||
|       <!--</el-dropdown>--> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { mapGetters } from 'vuex' | ||||
| import Breadcrumb from '@/components/Breadcrumb' | ||||
| import Hamburger from '@/components/Hamburger' | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     Breadcrumb, | ||||
|     Hamburger | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters([ | ||||
|       'sidebar', | ||||
|       'avatar' | ||||
|     ]) | ||||
|   }, | ||||
|   methods: { | ||||
|     toggleSideBar() { | ||||
|       this.$store.dispatch('app/toggleSideBar') | ||||
|     }, | ||||
|     doLogout() { | ||||
|       this.logout() | ||||
|       // this.$router.push(`/login?redirect=${this.$route.fullPath}`) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .navbar { | ||||
|   height: 50px; | ||||
|   overflow: hidden; | ||||
|   position: relative; | ||||
|   background: #fff; | ||||
|   box-shadow: 0 1px 4px rgba(0,21,41,.08); | ||||
| 
 | ||||
|   .hamburger-container { | ||||
|     line-height: 46px; | ||||
|     height: 100%; | ||||
|     float: left; | ||||
|     cursor: pointer; | ||||
|     transition: background .3s; | ||||
|     -webkit-tap-highlight-color:transparent; | ||||
| 
 | ||||
|     &:hover { | ||||
|       background: rgba(0, 0, 0, .025) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .breadcrumb-container { | ||||
|     float: left; | ||||
|   } | ||||
| 
 | ||||
|   .right-menu { | ||||
|     float: right; | ||||
|     height: 100%; | ||||
|     line-height: 50px; | ||||
| 
 | ||||
|     &:focus { | ||||
|       outline: none; | ||||
|     } | ||||
| 
 | ||||
|     .right-menu-item { | ||||
|       display: inline-block; | ||||
|       padding: 0 8px; | ||||
|       height: 100%; | ||||
|       font-size: 18px; | ||||
|       color: #5a5e66; | ||||
|       vertical-align: text-bottom; | ||||
| 
 | ||||
|       &.hover-effect { | ||||
|         cursor: pointer; | ||||
|         transition: background .3s; | ||||
| 
 | ||||
|         &:hover { | ||||
|           background: rgba(0, 0, 0, .025) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .avatar-container { | ||||
|       margin-right: 30px; | ||||
| 
 | ||||
|       .avatar-wrapper { | ||||
|         margin-top: 5px; | ||||
|         position: relative; | ||||
| 
 | ||||
|         .user-avatar { | ||||
|           cursor: pointer; | ||||
|           width: 40px; | ||||
|           height: 40px; | ||||
|           border-radius: 10px; | ||||
|         } | ||||
| 
 | ||||
|         .el-icon-caret-bottom { | ||||
|           cursor: pointer; | ||||
|           position: absolute; | ||||
|           right: -20px; | ||||
|           top: 25px; | ||||
|           font-size: 12px; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @ -0,0 +1,26 @@ | ||||
| export default { | ||||
|   computed: { | ||||
|     device() { | ||||
|       return this.$store.state.app.device | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     // In order to fix the click on menu on the ios device will trigger the mouseleave bug
 | ||||
|     // https://github.com/PanJiaChen/vue-element-admin/issues/1135
 | ||||
|     this.fixBugIniOS() | ||||
|   }, | ||||
|   methods: { | ||||
|     fixBugIniOS() { | ||||
|       const $subMenu = this.$refs.subMenu | ||||
|       if ($subMenu) { | ||||
|         const handleMouseleave = $subMenu.handleMouseleave | ||||
|         $subMenu.handleMouseleave = (e) => { | ||||
|           if (this.device === 'mobile') { | ||||
|             return | ||||
|           } | ||||
|           handleMouseleave(e) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,29 @@ | ||||
| <script> | ||||
| export default { | ||||
|   name: 'MenuItem', | ||||
|   functional: true, | ||||
|   props: { | ||||
|     icon: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     }, | ||||
|     title: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     } | ||||
|   }, | ||||
|   render(h, context) { | ||||
|     const { icon, title } = context.props | ||||
|     const vnodes = [] | ||||
| 
 | ||||
|     if (icon) { | ||||
|       vnodes.push(<svg-icon icon-class={icon}/>) | ||||
|     } | ||||
| 
 | ||||
|     if (title) { | ||||
|       vnodes.push(<span slot='title'>{(title)}</span>) | ||||
|     } | ||||
|     return vnodes | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,36 @@ | ||||
| 
 | ||||
| <template> | ||||
|   <!-- eslint-disable vue/require-component-is --> | ||||
|   <component v-bind="linkProps(to)"> | ||||
|     <slot /> | ||||
|   </component> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { isExternal } from '@/utils/validate' | ||||
| 
 | ||||
| export default { | ||||
|   props: { | ||||
|     to: { | ||||
|       type: String, | ||||
|       required: true | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     linkProps(url) { | ||||
|       if (isExternal(url)) { | ||||
|         return { | ||||
|           is: 'a', | ||||
|           href: url, | ||||
|           target: '_blank', | ||||
|           rel: 'noopener' | ||||
|         } | ||||
|       } | ||||
|       return { | ||||
|         is: 'router-link', | ||||
|         to: url | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,83 @@ | ||||
| <template> | ||||
|   <div class="sidebar-logo-container" :class="{'collapse':collapse}"> | ||||
|     <transition name="sidebarLogoFade"> | ||||
|       <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> | ||||
|         <img v-if="logo" :src="logo" class="sidebar-logo"> | ||||
|         <h1 v-else class="sidebar-title">{{ title }} </h1> | ||||
|       </router-link> | ||||
|       <router-link v-else key="expand" class="sidebar-logo-link" to="/"> | ||||
|         <img v-if="logo" :src="logo" class="sidebar-logo"> | ||||
|         <h1 class="sidebar-title">{{ title }} </h1> | ||||
|       </router-link> | ||||
|     </transition> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   name: 'SidebarLogo', | ||||
|   props: { | ||||
|     collapse: { | ||||
|       type: Boolean, | ||||
|       required: true | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       title: 'SOP Admin', | ||||
|       logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .sidebarLogoFade-enter-active { | ||||
|   transition: opacity 1.5s; | ||||
| } | ||||
| 
 | ||||
| .sidebarLogoFade-enter, | ||||
| .sidebarLogoFade-leave-to { | ||||
|   opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .sidebar-logo-container { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   height: 50px; | ||||
|   line-height: 50px; | ||||
|   background: #2b2f3a; | ||||
|   /*text-align: center;*/ | ||||
|   padding-left: 10px; | ||||
|   overflow: hidden; | ||||
| 
 | ||||
|   & .sidebar-logo-link { | ||||
|     height: 100%; | ||||
|     width: 100%; | ||||
| 
 | ||||
|     & .sidebar-logo { | ||||
|       width: 32px; | ||||
|       height: 32px; | ||||
|       vertical-align: middle; | ||||
|       margin-right: 12px; | ||||
|     } | ||||
| 
 | ||||
|     & .sidebar-title { | ||||
|       display: inline-block; | ||||
|       margin: 0; | ||||
|       color: #fff; | ||||
|       font-weight: 600; | ||||
|       line-height: 50px; | ||||
|       font-size: 14px; | ||||
|       font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; | ||||
|       vertical-align: middle; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   &.collapse { | ||||
|     .sidebar-logo { | ||||
|       margin-right: 0px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @ -0,0 +1,95 @@ | ||||
| <template> | ||||
|   <div v-if="!item.hidden" class="menu-wrapper"> | ||||
|     <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> | ||||
|       <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> | ||||
|         <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> | ||||
|           <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> | ||||
|         </el-menu-item> | ||||
|       </app-link> | ||||
|     </template> | ||||
| 
 | ||||
|     <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> | ||||
|       <template slot="title"> | ||||
|         <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> | ||||
|       </template> | ||||
|       <sidebar-item | ||||
|         v-for="child in item.children" | ||||
|         :key="child.path" | ||||
|         :is-nest="true" | ||||
|         :item="child" | ||||
|         :base-path="resolvePath(child.path)" | ||||
|         class="nest-menu" | ||||
|       /> | ||||
|     </el-submenu> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import path from 'path' | ||||
| import { isExternal } from '@/utils/validate' | ||||
| import Item from './Item' | ||||
| import AppLink from './Link' | ||||
| import FixiOSBug from './FixiOSBug' | ||||
| 
 | ||||
| export default { | ||||
|   name: 'SidebarItem', | ||||
|   components: { Item, AppLink }, | ||||
|   mixins: [FixiOSBug], | ||||
|   props: { | ||||
|     // route object | ||||
|     item: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|     }, | ||||
|     isNest: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     basePath: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237 | ||||
|     // TODO: refactor with render function | ||||
|     this.onlyOneChild = null | ||||
|     return {} | ||||
|   }, | ||||
|   methods: { | ||||
|     hasOneShowingChild(children = [], parent) { | ||||
|       const showingChildren = children.filter(item => { | ||||
|         if (item.hidden) { | ||||
|           return false | ||||
|         } else { | ||||
|           // Temp set(will be used if only has one showing child) | ||||
|           this.onlyOneChild = item | ||||
|           return true | ||||
|         } | ||||
|       }) | ||||
| 
 | ||||
|       // When there is only one child router, the child router is displayed by default | ||||
|       if (showingChildren.length === 1) { | ||||
|         return true | ||||
|       } | ||||
| 
 | ||||
|       // Show parent if there are no child router to display | ||||
|       if (showingChildren.length === 0) { | ||||
|         this.onlyOneChild = { ... parent, path: '', noShowingChildren: true } | ||||
|         return true | ||||
|       } | ||||
| 
 | ||||
|       return false | ||||
|     }, | ||||
|     resolvePath(routePath) { | ||||
|       if (isExternal(routePath)) { | ||||
|         return routePath | ||||
|       } | ||||
|       if (isExternal(this.basePath)) { | ||||
|         return this.basePath | ||||
|       } | ||||
|       return path.resolve(this.basePath, routePath) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,56 @@ | ||||
| <template> | ||||
|   <div :class="{'has-logo':showLogo}"> | ||||
|     <logo v-if="showLogo" :collapse="isCollapse" /> | ||||
|     <el-scrollbar wrap-class="scrollbar-wrapper"> | ||||
|       <el-menu | ||||
|         :default-active="activeMenu" | ||||
|         :collapse="isCollapse" | ||||
|         :background-color="variables.menuBg" | ||||
|         :text-color="variables.menuText" | ||||
|         :unique-opened="false" | ||||
|         :active-text-color="variables.menuActiveText" | ||||
|         :collapse-transition="false" | ||||
|         mode="vertical" | ||||
|       > | ||||
|         <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" /> | ||||
|       </el-menu> | ||||
|     </el-scrollbar> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { mapGetters } from 'vuex' | ||||
| import Logo from './Logo' | ||||
| import SidebarItem from './SidebarItem' | ||||
| import variables from '@/styles/variables.scss' | ||||
| 
 | ||||
| export default { | ||||
|   components: { SidebarItem, Logo }, | ||||
|   computed: { | ||||
|     ...mapGetters([ | ||||
|       'sidebar' | ||||
|     ]), | ||||
|     routes() { | ||||
|       return this.$router.options.routes | ||||
|     }, | ||||
|     activeMenu() { | ||||
|       const route = this.$route | ||||
|       const { meta, path } = route | ||||
|       // if set path, the sidebar will highlight the path you set | ||||
|       if (meta.activeMenu) { | ||||
|         return meta.activeMenu | ||||
|       } | ||||
|       return path | ||||
|     }, | ||||
|     showLogo() { | ||||
|       return this.$store.state.settings.sidebarLogo | ||||
|     }, | ||||
|     variables() { | ||||
|       return variables | ||||
|     }, | ||||
|     isCollapse() { | ||||
|       return !this.sidebar.opened | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,3 @@ | ||||
| export { default as Navbar } from './Navbar' | ||||
| export { default as Sidebar } from './Sidebar' | ||||
| export { default as AppMain } from './AppMain' | ||||
| @ -0,0 +1,93 @@ | ||||
| <template> | ||||
|   <div :class="classObj" class="app-wrapper"> | ||||
|     <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> | ||||
|     <sidebar class="sidebar-container" /> | ||||
|     <div class="main-container"> | ||||
|       <div :class="{'fixed-header':fixedHeader}"> | ||||
|         <navbar /> | ||||
|       </div> | ||||
|       <app-main /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { Navbar, Sidebar, AppMain } from './components' | ||||
| import ResizeMixin from './mixin/ResizeHandler' | ||||
| 
 | ||||
| export default { | ||||
|   name: 'Layout', | ||||
|   components: { | ||||
|     Navbar, | ||||
|     Sidebar, | ||||
|     AppMain | ||||
|   }, | ||||
|   mixins: [ResizeMixin], | ||||
|   computed: { | ||||
|     sidebar() { | ||||
|       return this.$store.state.app.sidebar | ||||
|     }, | ||||
|     device() { | ||||
|       return this.$store.state.app.device | ||||
|     }, | ||||
|     fixedHeader() { | ||||
|       return this.$store.state.settings.fixedHeader | ||||
|     }, | ||||
|     classObj() { | ||||
|       return { | ||||
|         hideSidebar: !this.sidebar.opened, | ||||
|         openSidebar: this.sidebar.opened, | ||||
|         withoutAnimation: this.sidebar.withoutAnimation, | ||||
|         mobile: this.device === 'mobile' | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     handleClickOutside() { | ||||
|       this.$store.dispatch('app/closeSideBar', { withoutAnimation: false }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
|   @import "~@/styles/mixin.scss"; | ||||
|   @import "~@/styles/variables.scss"; | ||||
| 
 | ||||
|   .app-wrapper { | ||||
|     @include clearfix; | ||||
|     position: relative; | ||||
|     height: 100%; | ||||
|     width: 100%; | ||||
|     &.mobile.openSidebar{ | ||||
|       position: fixed; | ||||
|       top: 0; | ||||
|     } | ||||
|   } | ||||
|   .drawer-bg { | ||||
|     background: #000; | ||||
|     opacity: 0.3; | ||||
|     width: 100%; | ||||
|     top: 0; | ||||
|     height: 100%; | ||||
|     position: absolute; | ||||
|     z-index: 999; | ||||
|   } | ||||
| 
 | ||||
|   .fixed-header { | ||||
|     position: fixed; | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     z-index: 9; | ||||
|     width: calc(100% - #{$sideBarWidth}); | ||||
|     transition: width 0.28s; | ||||
|   } | ||||
| 
 | ||||
|   .hideSidebar .fixed-header { | ||||
|     width: calc(100% - 54px) | ||||
|   } | ||||
| 
 | ||||
|   .mobile .fixed-header { | ||||
|     width: 100%; | ||||
|   } | ||||
| </style> | ||||
| @ -0,0 +1,45 @@ | ||||
| import store from '@/store' | ||||
| 
 | ||||
| const { body } = document | ||||
| const WIDTH = 992 // refer to Bootstrap's responsive design
 | ||||
| 
 | ||||
| export default { | ||||
|   watch: { | ||||
|     $route(route) { | ||||
|       if (this.device === 'mobile' && this.sidebar.opened) { | ||||
|         store.dispatch('app/closeSideBar', { withoutAnimation: false }) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   beforeMount() { | ||||
|     window.addEventListener('resize', this.$_resizeHandler) | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     window.removeEventListener('resize', this.$_resizeHandler) | ||||
|   }, | ||||
|   mounted() { | ||||
|     const isMobile = this.$_isMobile() | ||||
|     if (isMobile) { | ||||
|       store.dispatch('app/toggleDevice', 'mobile') | ||||
|       store.dispatch('app/closeSideBar', { withoutAnimation: true }) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     // use $_ for mixins properties
 | ||||
|     // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
 | ||||
|     $_isMobile() { | ||||
|       const rect = body.getBoundingClientRect() | ||||
|       return rect.width - 1 < WIDTH | ||||
|     }, | ||||
|     $_resizeHandler() { | ||||
|       if (!document.hidden) { | ||||
|         const isMobile = this.$_isMobile() | ||||
|         store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') | ||||
| 
 | ||||
|         if (isMobile) { | ||||
|           store.dispatch('app/closeSideBar', { withoutAnimation: true }) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,38 @@ | ||||
| import Vue from 'vue' | ||||
| 
 | ||||
| import 'normalize.css/normalize.css' // A modern alternative to CSS resets
 | ||||
| 
 | ||||
| import ElementUI from 'element-ui' | ||||
| import 'element-ui/lib/theme-chalk/index.css' | ||||
| import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
 | ||||
| 
 | ||||
| import '@/styles/index.scss' // global css
 | ||||
| 
 | ||||
| import App from './App' | ||||
| import store from './store' | ||||
| import router from './router' | ||||
| 
 | ||||
| import '@/icons' // icon
 | ||||
| import '@/permission' // permission control
 | ||||
| import '@/utils/global' // 自定义全局js
 | ||||
| 
 | ||||
| /** | ||||
|  * If you don't want to use mock-server | ||||
|  * you want to use mockjs for request interception | ||||
|  * you can execute: | ||||
|  * | ||||
|  * import { mockXHR } from '../mock' | ||||
|  * mockXHR() | ||||
|  */ | ||||
| 
 | ||||
| // set ElementUI lang to EN
 | ||||
| Vue.use(ElementUI, { locale }) | ||||
| 
 | ||||
| Vue.config.productionTip = false | ||||
| 
 | ||||
| new Vue({ | ||||
|   el: '#app', | ||||
|   router, | ||||
|   store, | ||||
|   render: h => h(App) | ||||
| }) | ||||
| @ -0,0 +1,65 @@ | ||||
| import router from './router' | ||||
| // import store from './store'
 | ||||
| // import { Message } from 'element-ui'
 | ||||
| import NProgress from 'nprogress' // progress bar
 | ||||
| import 'nprogress/nprogress.css' // progress bar style
 | ||||
| import { getToken } from '@/utils/auth' // get token from cookie
 | ||||
| import getPageTitle from '@/utils/get-page-title' | ||||
| 
 | ||||
| NProgress.configure({ showSpinner: false }) // NProgress Configuration
 | ||||
| 
 | ||||
| const whiteList = ['/login'] // no redirect whitelist
 | ||||
| 
 | ||||
| router.beforeEach(async(to, from, next) => { | ||||
|   // start progress bar
 | ||||
|   NProgress.start() | ||||
| 
 | ||||
|   // set page title
 | ||||
|   document.title = getPageTitle(to.meta.title) | ||||
| 
 | ||||
|   // determine whether the user has logged in
 | ||||
|   const hasToken = getToken() | ||||
| 
 | ||||
|   if (hasToken) { | ||||
|     if (to.path === '/login') { | ||||
|       // if is logged in, redirect to the home page
 | ||||
|       next({ path: '/' }) | ||||
|       NProgress.done() | ||||
|     } else { | ||||
|       next() | ||||
|       // const hasGetUserInfo = store.getters.name
 | ||||
|       // if (hasGetUserInfo) {
 | ||||
|       //   next()
 | ||||
|       // } else {
 | ||||
|       //   try {
 | ||||
|       //     // get user info
 | ||||
|       //     await store.dispatch('user/getInfo')
 | ||||
|       //
 | ||||
|       //     next()
 | ||||
|       //   } catch (error) {
 | ||||
|       //     // remove token and go to login page to re-login
 | ||||
|       //     await store.dispatch('user/resetToken')
 | ||||
|       //     Message.error(error || 'Has Error')
 | ||||
|       //     next(`/login?redirect=${to.path}`)
 | ||||
|       //     NProgress.done()
 | ||||
|       //   }
 | ||||
|       // }
 | ||||
|     } | ||||
|   } else { | ||||
|     /* has no token*/ | ||||
| 
 | ||||
|     if (whiteList.indexOf(to.path) !== -1) { | ||||
|       // in the free login whitelist, go directly
 | ||||
|       next() | ||||
|     } else { | ||||
|       // other pages that do not have permission to access are redirected to the login page.
 | ||||
|       next(`/login?redirect=${to.path}`) | ||||
|       NProgress.done() | ||||
|     } | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| router.afterEach(() => { | ||||
|   // finish progress bar
 | ||||
|   NProgress.done() | ||||
| }) | ||||
| @ -0,0 +1,116 @@ | ||||
| import Vue from 'vue' | ||||
| import Router from 'vue-router' | ||||
| 
 | ||||
| Vue.use(Router) | ||||
| 
 | ||||
| /* Layout */ | ||||
| import Layout from '@/layout' | ||||
| 
 | ||||
| /** | ||||
|  * Note: sub-menu only appear when route children.length >= 1 | ||||
|  * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
 | ||||
|  * | ||||
|  * hidden: true                   if set true, item will not show in the sidebar(default is false) | ||||
|  * alwaysShow: true               if set true, will always show the root menu | ||||
|  *                                if not set alwaysShow, when item has more than one children route, | ||||
|  *                                it will becomes nested mode, otherwise not show the root menu | ||||
|  * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb | ||||
|  * name:'router-name'             the name is used by <keep-alive> (must set!!!) | ||||
|  * meta : { | ||||
|     roles: ['admin','editor']    control the page roles (you can set multiple roles) | ||||
|     title: 'title'               the name show in sidebar and breadcrumb (recommend set) | ||||
|     icon: 'svg-name'             the icon show in the sidebar | ||||
|     breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true) | ||||
|     activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set | ||||
|   } | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * constantRoutes | ||||
|  * a base page that does not have permission requirements | ||||
|  * all roles can be accessed | ||||
|  */ | ||||
| export const constantRoutes = [ | ||||
|   { | ||||
|     path: '/login', | ||||
|     component: () => import('@/views/login/index'), | ||||
|     hidden: true | ||||
|   }, | ||||
| 
 | ||||
|   { | ||||
|     path: '/404', | ||||
|     component: () => import('@/views/404'), | ||||
|     hidden: true | ||||
|   }, | ||||
| 
 | ||||
|   { | ||||
|     path: '/', | ||||
|     component: Layout, | ||||
|     redirect: '/dashboard', | ||||
|     children: [{ | ||||
|       path: 'dashboard', | ||||
|       name: 'Dashboard', | ||||
|       component: () => import('@/views/dashboard/index'), | ||||
|       meta: { title: '首页', icon: 'dashboard' } | ||||
|     }] | ||||
|   }, | ||||
| 
 | ||||
|   { | ||||
|     path: '/service', | ||||
|     component: Layout, | ||||
|     name: 'Service', | ||||
|     meta: { title: '服务管理', icon: 'example' }, | ||||
|     children: [ | ||||
|       { | ||||
|         path: 'list', | ||||
|         name: 'ServiceList', | ||||
|         component: () => import('@/views/service/list/index'), | ||||
|         meta: { title: '服务列表' } | ||||
|       }, | ||||
|       { | ||||
|         path: 'route', | ||||
|         name: 'Route', | ||||
|         component: () => import('@/views/service/route/index'), | ||||
|         meta: { title: '路由管理' } | ||||
|       }, | ||||
|       { | ||||
|         path: 'limit', | ||||
|         name: 'Limit', | ||||
|         component: () => import('@/views/service/limit/index'), | ||||
|         meta: { title: '限流管理' } | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
| 
 | ||||
|   { | ||||
|     path: '/isv', | ||||
|     component: Layout, | ||||
|     meta: { title: 'ISV管理', icon: 'user' }, | ||||
|     children: [ | ||||
|       { | ||||
|         path: 'list', | ||||
|         name: 'IsvList', | ||||
|         component: () => import('@/views/isv/index'), | ||||
|         meta: { title: 'ISV列表' } | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   // 404 page must be placed at the end !!!
 | ||||
|   { path: '*', redirect: '/404', hidden: true } | ||||
| ] | ||||
| 
 | ||||
| const createRouter = () => new Router({ | ||||
|   // mode: 'history', // require service support
 | ||||
|   scrollBehavior: () => ({ y: 0 }), | ||||
|   routes: constantRoutes | ||||
| }) | ||||
| 
 | ||||
| const router = createRouter() | ||||
| 
 | ||||
| // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
 | ||||
| export function resetRouter() { | ||||
|   const newRouter = createRouter() | ||||
|   router.matcher = newRouter.matcher // reset router
 | ||||
| } | ||||
| 
 | ||||
| export default router | ||||
| @ -0,0 +1,16 @@ | ||||
| module.exports = { | ||||
| 
 | ||||
|   title: 'SOP Admin', | ||||
| 
 | ||||
|   /** | ||||
|    * @type {boolean} true | false | ||||
|    * @description Whether fix the header | ||||
|    */ | ||||
|   fixedHeader: false, | ||||
| 
 | ||||
|   /** | ||||
|    * @type {boolean} true | false | ||||
|    * @description Whether show the logo in sidebar | ||||
|    */ | ||||
|   sidebarLogo: true | ||||
| } | ||||
| @ -0,0 +1,8 @@ | ||||
| const getters = { | ||||
|   sidebar: state => state.app.sidebar, | ||||
|   device: state => state.app.device, | ||||
|   token: state => state.user.token, | ||||
|   avatar: state => state.user.avatar, | ||||
|   name: state => state.user.name | ||||
| } | ||||
| export default getters | ||||
| @ -0,0 +1,19 @@ | ||||
| import Vue from 'vue' | ||||
| import Vuex from 'vuex' | ||||
| import getters from './getters' | ||||
| import app from './modules/app' | ||||
| import settings from './modules/settings' | ||||
| import user from './modules/user' | ||||
| 
 | ||||
| Vue.use(Vuex) | ||||
| 
 | ||||
| const store = new Vuex.Store({ | ||||
|   modules: { | ||||
|     app, | ||||
|     settings, | ||||
|     user | ||||
|   }, | ||||
|   getters | ||||
| }) | ||||
| 
 | ||||
| export default store | ||||
| @ -0,0 +1,48 @@ | ||||
| import Cookies from 'js-cookie' | ||||
| 
 | ||||
| const state = { | ||||
|   sidebar: { | ||||
|     opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, | ||||
|     withoutAnimation: false | ||||
|   }, | ||||
|   device: 'desktop' | ||||
| } | ||||
| 
 | ||||
| const mutations = { | ||||
|   TOGGLE_SIDEBAR: state => { | ||||
|     state.sidebar.opened = !state.sidebar.opened | ||||
|     state.sidebar.withoutAnimation = false | ||||
|     if (state.sidebar.opened) { | ||||
|       Cookies.set('sidebarStatus', 1) | ||||
|     } else { | ||||
|       Cookies.set('sidebarStatus', 0) | ||||
|     } | ||||
|   }, | ||||
|   CLOSE_SIDEBAR: (state, withoutAnimation) => { | ||||
|     Cookies.set('sidebarStatus', 0) | ||||
|     state.sidebar.opened = false | ||||
|     state.sidebar.withoutAnimation = withoutAnimation | ||||
|   }, | ||||
|   TOGGLE_DEVICE: (state, device) => { | ||||
|     state.device = device | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const actions = { | ||||
|   toggleSideBar({ commit }) { | ||||
|     commit('TOGGLE_SIDEBAR') | ||||
|   }, | ||||
|   closeSideBar({ commit }, { withoutAnimation }) { | ||||
|     commit('CLOSE_SIDEBAR', withoutAnimation) | ||||
|   }, | ||||
|   toggleDevice({ commit }, device) { | ||||
|     commit('TOGGLE_DEVICE', device) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|   namespaced: true, | ||||
|   state, | ||||
|   mutations, | ||||
|   actions | ||||
| } | ||||
| @ -0,0 +1,31 @@ | ||||
| import defaultSettings from '@/settings' | ||||
| 
 | ||||
| const { showSettings, fixedHeader, sidebarLogo } = defaultSettings | ||||
| 
 | ||||
| const state = { | ||||
|   showSettings: showSettings, | ||||
|   fixedHeader: fixedHeader, | ||||
|   sidebarLogo: sidebarLogo | ||||
| } | ||||
| 
 | ||||
| const mutations = { | ||||
|   CHANGE_SETTING: (state, { key, value }) => { | ||||
|     if (state.hasOwnProperty(key)) { | ||||
|       state[key] = value | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const actions = { | ||||
|   changeSetting({ commit }, data) { | ||||
|     commit('CHANGE_SETTING', data) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|   namespaced: true, | ||||
|   state, | ||||
|   mutations, | ||||
|   actions | ||||
| } | ||||
| 
 | ||||
| @ -0,0 +1,90 @@ | ||||
| import { login, logout, getInfo } from '@/api/user' | ||||
| import { getToken, setToken, removeToken } from '@/utils/auth' | ||||
| import { resetRouter } from '@/router' | ||||
| 
 | ||||
| const state = { | ||||
|   token: getToken(), | ||||
|   name: '', | ||||
|   avatar: '' | ||||
| } | ||||
| 
 | ||||
| const mutations = { | ||||
|   SET_TOKEN: (state, token) => { | ||||
|     state.token = token | ||||
|   }, | ||||
|   SET_NAME: (state, name) => { | ||||
|     state.name = name | ||||
|   }, | ||||
|   SET_AVATAR: (state, avatar) => { | ||||
|     state.avatar = avatar | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const actions = { | ||||
|   // user login
 | ||||
|   login({ commit }, userInfo) { | ||||
|     const { username, password } = userInfo | ||||
|     return new Promise((resolve, reject) => { | ||||
|       login({ username: username.trim(), password: password }).then(response => { | ||||
|         const { data } = response | ||||
|         commit('SET_TOKEN', data.token) | ||||
|         setToken(data.token) | ||||
|         resolve() | ||||
|       }).catch(error => { | ||||
|         reject(error) | ||||
|       }) | ||||
|     }) | ||||
|   }, | ||||
| 
 | ||||
|   // get user info
 | ||||
|   getInfo({ commit, state }) { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       getInfo(state.token).then(response => { | ||||
|         const { data } = response | ||||
| 
 | ||||
|         if (!data) { | ||||
|           reject('Verification failed, please Login again.') | ||||
|         } | ||||
| 
 | ||||
|         const { name, avatar } = data | ||||
| 
 | ||||
|         commit('SET_NAME', name) | ||||
|         commit('SET_AVATAR', avatar) | ||||
|         resolve(data) | ||||
|       }).catch(error => { | ||||
|         reject(error) | ||||
|       }) | ||||
|     }) | ||||
|   }, | ||||
| 
 | ||||
|   // user logout
 | ||||
|   logout({ commit, state }) { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       logout(state.token).then(() => { | ||||
|         commit('SET_TOKEN', '') | ||||
|         removeToken() | ||||
|         resetRouter() | ||||
|         resolve() | ||||
|       }).catch(error => { | ||||
|         reject(error) | ||||
|       }) | ||||
|     }) | ||||
|   }, | ||||
| 
 | ||||
|   // remove token
 | ||||
|   resetToken({ commit }) { | ||||
|     return new Promise(resolve => { | ||||
|       commit('SET_TOKEN', '') | ||||
|       removeToken() | ||||
|       resolve() | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|   namespaced: true, | ||||
|   state, | ||||
|   mutations, | ||||
|   actions | ||||
| } | ||||
| 
 | ||||
| @ -0,0 +1,44 @@ | ||||
| // cover some element-ui styles | ||||
| 
 | ||||
| .el-breadcrumb__inner, | ||||
| .el-breadcrumb__inner a { | ||||
|   font-weight: 400 !important; | ||||
| } | ||||
| 
 | ||||
| .el-upload { | ||||
|   input[type="file"] { | ||||
|     display: none !important; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .el-upload__input { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| // to fixed https://github.com/ElemeFE/element/issues/2461 | ||||
| .el-dialog { | ||||
|   transform: none; | ||||
|   left: 0; | ||||
|   position: relative; | ||||
|   margin: 0 auto; | ||||
| } | ||||
| 
 | ||||
| // refine element ui upload | ||||
| .upload-container { | ||||
|   .el-upload { | ||||
|     width: 100%; | ||||
| 
 | ||||
|     .el-upload-dragger { | ||||
|       width: 100%; | ||||
|       height: 200px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // dropdown | ||||
| .el-dropdown-menu { | ||||
|   a { | ||||
|     display: block | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,65 @@ | ||||
| @import './variables.scss'; | ||||
| @import './mixin.scss'; | ||||
| @import './transition.scss'; | ||||
| @import './element-ui.scss'; | ||||
| @import './sidebar.scss'; | ||||
| 
 | ||||
| body { | ||||
|   height: 100%; | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   text-rendering: optimizeLegibility; | ||||
|   font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; | ||||
| } | ||||
| 
 | ||||
| label { | ||||
|   font-weight: 700; | ||||
| } | ||||
| 
 | ||||
| html { | ||||
|   height: 100%; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| #app { | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| *, | ||||
| *:before, | ||||
| *:after { | ||||
|   box-sizing: inherit; | ||||
| } | ||||
| 
 | ||||
| a:focus, | ||||
| a:active { | ||||
|   outline: none; | ||||
| } | ||||
| 
 | ||||
| a, | ||||
| a:focus, | ||||
| a:hover { | ||||
|   cursor: pointer; | ||||
|   color: inherit; | ||||
|   text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| div:focus { | ||||
|   outline: none; | ||||
| } | ||||
| 
 | ||||
| .clearfix { | ||||
|   &:after { | ||||
|     visibility: hidden; | ||||
|     display: block; | ||||
|     font-size: 0; | ||||
|     content: " "; | ||||
|     clear: both; | ||||
|     height: 0; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // main-container global css | ||||
| .app-container { | ||||
|   padding: 20px; | ||||
| } | ||||
| @ -0,0 +1,28 @@ | ||||
| @mixin clearfix { | ||||
|   &:after { | ||||
|     content: ""; | ||||
|     display: table; | ||||
|     clear: both; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @mixin scrollBar { | ||||
|   &::-webkit-scrollbar-track-piece { | ||||
|     background: #d3dce6; | ||||
|   } | ||||
| 
 | ||||
|   &::-webkit-scrollbar { | ||||
|     width: 6px; | ||||
|   } | ||||
| 
 | ||||
|   &::-webkit-scrollbar-thumb { | ||||
|     background: #99a9bf; | ||||
|     border-radius: 20px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @mixin relative { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| @ -0,0 +1,213 @@ | ||||
| #app { | ||||
| 
 | ||||
|   .main-container { | ||||
|     min-height: 100%; | ||||
|     transition: margin-left .28s; | ||||
|     margin-left: $sideBarWidth; | ||||
|     position: relative; | ||||
|   } | ||||
| 
 | ||||
|   .sidebar-container { | ||||
|     transition: width 0.28s; | ||||
|     width: $sideBarWidth !important; | ||||
|     background-color: $menuBg; | ||||
|     height: 100%; | ||||
|     position: fixed; | ||||
|     font-size: 0px; | ||||
|     top: 0; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|     z-index: 1001; | ||||
|     overflow: hidden; | ||||
| 
 | ||||
|     // reset element-ui css | ||||
|     .horizontal-collapse-transition { | ||||
|       transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; | ||||
|     } | ||||
| 
 | ||||
|     .scrollbar-wrapper { | ||||
|       overflow-x: hidden !important; | ||||
|     } | ||||
| 
 | ||||
|     .el-scrollbar__bar.is-vertical { | ||||
|       right: 0px; | ||||
|     } | ||||
| 
 | ||||
|     .el-scrollbar { | ||||
|       height: 100%; | ||||
|     } | ||||
| 
 | ||||
|     &.has-logo { | ||||
|       .el-scrollbar { | ||||
|         height: calc(100% - 50px); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .is-horizontal { | ||||
|       display: none; | ||||
|     } | ||||
| 
 | ||||
|     a { | ||||
|       display: inline-block; | ||||
|       width: 100%; | ||||
|       overflow: hidden; | ||||
|     } | ||||
| 
 | ||||
|     .svg-icon { | ||||
|       margin-right: 16px; | ||||
|     } | ||||
| 
 | ||||
|     .el-menu { | ||||
|       border: none; | ||||
|       height: 100%; | ||||
|       width: 100% !important; | ||||
|     } | ||||
| 
 | ||||
|     // menu hover | ||||
|     .submenu-title-noDropdown, | ||||
|     .el-submenu__title { | ||||
|       &:hover { | ||||
|         background-color: $menuHover !important; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .is-active>.el-submenu__title { | ||||
|       color: $subMenuActiveText !important; | ||||
|     } | ||||
| 
 | ||||
|     & .nest-menu .el-submenu>.el-submenu__title, | ||||
|     & .el-submenu .el-menu-item { | ||||
|       min-width: $sideBarWidth !important; | ||||
|       background-color: $subMenuBg !important; | ||||
| 
 | ||||
|       &:hover { | ||||
|         background-color: $subMenuHover !important; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .hideSidebar { | ||||
|     .sidebar-container { | ||||
|       width: 54px !important; | ||||
|     } | ||||
| 
 | ||||
|     .main-container { | ||||
|       margin-left: 54px; | ||||
|     } | ||||
| 
 | ||||
|     .svg-icon { | ||||
|       margin-right: 0px; | ||||
|     } | ||||
| 
 | ||||
|     .submenu-title-noDropdown { | ||||
|       padding: 0 !important; | ||||
|       position: relative; | ||||
| 
 | ||||
|       .el-tooltip { | ||||
|         padding: 0 !important; | ||||
| 
 | ||||
|         .svg-icon { | ||||
|           margin-left: 20px; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .el-submenu { | ||||
|       overflow: hidden; | ||||
| 
 | ||||
|       &>.el-submenu__title { | ||||
|         padding: 0 !important; | ||||
| 
 | ||||
|         .svg-icon { | ||||
|           margin-left: 20px; | ||||
|         } | ||||
| 
 | ||||
|         .el-submenu__icon-arrow { | ||||
|           display: none; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .el-menu--collapse { | ||||
|       .el-submenu { | ||||
|         &>.el-submenu__title { | ||||
|           &>span { | ||||
|             height: 0; | ||||
|             width: 0; | ||||
|             overflow: hidden; | ||||
|             visibility: hidden; | ||||
|             display: inline-block; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .el-menu--collapse .el-menu .el-submenu { | ||||
|     min-width: $sideBarWidth !important; | ||||
|   } | ||||
| 
 | ||||
|   // mobile responsive | ||||
|   .mobile { | ||||
|     .main-container { | ||||
|       margin-left: 0px; | ||||
|     } | ||||
| 
 | ||||
|     .sidebar-container { | ||||
|       transition: transform .28s; | ||||
|       width: $sideBarWidth !important; | ||||
|     } | ||||
| 
 | ||||
|     &.hideSidebar { | ||||
|       .sidebar-container { | ||||
|         pointer-events: none; | ||||
|         transition-duration: 0.3s; | ||||
|         transform: translate3d(-$sideBarWidth, 0, 0); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .withoutAnimation { | ||||
| 
 | ||||
|     .main-container, | ||||
|     .sidebar-container { | ||||
|       transition: none; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // when menu collapsed | ||||
| .el-menu--vertical { | ||||
|   &>.el-menu { | ||||
|     .svg-icon { | ||||
|       margin-right: 16px; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .nest-menu .el-submenu>.el-submenu__title, | ||||
|   .el-menu-item { | ||||
|     &:hover { | ||||
|       // you can use $subMenuHover | ||||
|       background-color: $menuHover !important; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // the scroll bar appears when the subMenu is too long | ||||
|   >.el-menu--popup { | ||||
|     max-height: 100vh; | ||||
|     overflow-y: auto; | ||||
| 
 | ||||
|     &::-webkit-scrollbar-track-piece { | ||||
|       background: #d3dce6; | ||||
|     } | ||||
| 
 | ||||
|     &::-webkit-scrollbar { | ||||
|       width: 6px; | ||||
|     } | ||||
| 
 | ||||
|     &::-webkit-scrollbar-thumb { | ||||
|       background: #99a9bf; | ||||
|       border-radius: 20px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,48 @@ | ||||
| // global transition css | ||||
| 
 | ||||
| /* fade */ | ||||
| .fade-enter-active, | ||||
| .fade-leave-active { | ||||
|   transition: opacity 0.28s; | ||||
| } | ||||
| 
 | ||||
| .fade-enter, | ||||
| .fade-leave-active { | ||||
|   opacity: 0; | ||||
| } | ||||
| 
 | ||||
| /* fade-transform */ | ||||
| .fade-transform-leave-active, | ||||
| .fade-transform-enter-active { | ||||
|   transition: all .5s; | ||||
| } | ||||
| 
 | ||||
| .fade-transform-enter { | ||||
|   opacity: 0; | ||||
|   transform: translateX(-30px); | ||||
| } | ||||
| 
 | ||||
| .fade-transform-leave-to { | ||||
|   opacity: 0; | ||||
|   transform: translateX(30px); | ||||
| } | ||||
| 
 | ||||
| /* breadcrumb transition */ | ||||
| .breadcrumb-enter-active, | ||||
| .breadcrumb-leave-active { | ||||
|   transition: all .5s; | ||||
| } | ||||
| 
 | ||||
| .breadcrumb-enter, | ||||
| .breadcrumb-leave-active { | ||||
|   opacity: 0; | ||||
|   transform: translateX(20px); | ||||
| } | ||||
| 
 | ||||
| .breadcrumb-move { | ||||
|   transition: all .5s; | ||||
| } | ||||
| 
 | ||||
| .breadcrumb-leave-active { | ||||
|   position: absolute; | ||||
| } | ||||
| @ -0,0 +1,25 @@ | ||||
| // sidebar | ||||
| $menuText:#bfcbd9; | ||||
| $menuActiveText:#409EFF; | ||||
| $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 | ||||
| 
 | ||||
| $menuBg:#304156; | ||||
| $menuHover:#263445; | ||||
| 
 | ||||
| $subMenuBg:#1f2d3d; | ||||
| $subMenuHover:#001528; | ||||
| 
 | ||||
| $sideBarWidth: 210px; | ||||
| 
 | ||||
| // the :export directive is the magic sauce for webpack | ||||
| // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass | ||||
| :export { | ||||
|   menuText: $menuText; | ||||
|   menuActiveText: $menuActiveText; | ||||
|   subMenuActiveText: $subMenuActiveText; | ||||
|   menuBg: $menuBg; | ||||
|   menuHover: $menuHover; | ||||
|   subMenuBg: $subMenuBg; | ||||
|   subMenuHover: $subMenuHover; | ||||
|   sideBarWidth: $sideBarWidth; | ||||
| } | ||||
| @ -0,0 +1,15 @@ | ||||
| import Cookies from 'js-cookie' | ||||
| 
 | ||||
| const TokenKey = 'sop-admin-token' | ||||
| 
 | ||||
| export function getToken() { | ||||
|   return Cookies.get(TokenKey) | ||||
| } | ||||
| 
 | ||||
| export function setToken(token) { | ||||
|   return Cookies.set(TokenKey, token) | ||||
| } | ||||
| 
 | ||||
| export function removeToken() { | ||||
|   return Cookies.remove(TokenKey) | ||||
| } | ||||
| @ -0,0 +1,10 @@ | ||||
| import defaultSettings from '@/settings' | ||||
| 
 | ||||
| const title = defaultSettings.title || 'Vue Admin Template' | ||||
| 
 | ||||
| export default function getPageTitle(pageTitle) { | ||||
|   if (pageTitle) { | ||||
|     return `${pageTitle} - ${title}` | ||||
|   } | ||||
|   return `${title}` | ||||
| } | ||||
| @ -0,0 +1,101 @@ | ||||
| /* | ||||
| 注册全局方法 | ||||
|  */ | ||||
| import Vue from 'vue' | ||||
| import axios from 'axios' | ||||
| import { getToken, removeToken } from './auth' | ||||
| 
 | ||||
| // 创建axios实例
 | ||||
| const client = axios.create({ | ||||
|   baseURL: process.env.VUE_APP_BASE_API, // api 的 base_url
 | ||||
|   timeout: 5000 // 请求超时时间
 | ||||
| }) | ||||
| 
 | ||||
| Object.assign(Vue.prototype, { | ||||
|   /** | ||||
|    * 请求接口 | ||||
|    * @param uri uri,如:goods.get,goods.get/1.0 | ||||
|    * @param data 请求数据 | ||||
|    * @param callback 成功时回调 | ||||
|    * @param errorCallback 错误时回调 | ||||
|    */ | ||||
|   post: function(uri, data, callback, errorCallback) { | ||||
|     const that = this | ||||
|     const paramStr = JSON.stringify(data) | ||||
|     if (!uri.endsWith('/')) { | ||||
|       uri = uri + '/' | ||||
|     } | ||||
|     if (!uri.startsWith('/')) { | ||||
|       uri = '/' + uri | ||||
|     } | ||||
|     client.post(uri, { | ||||
|       data: encodeURIComponent(paramStr), | ||||
|       access_token: getToken() | ||||
|     }).then(function(response) { | ||||
|       const resp = response.data | ||||
|       const code = resp.code | ||||
|       if (!code || code === '-9') { | ||||
|         that.$message.error('系统错误') | ||||
|         return | ||||
|       } | ||||
|       if (code === '-100' || code === '18' || code === '21') { // 未登录
 | ||||
|         that.logout() | ||||
|         return | ||||
|       } | ||||
|       if (code === '0') { // 成功
 | ||||
|         callback && callback.call(that, resp) | ||||
|       } else { | ||||
|         that.$message.error(resp.msg) | ||||
|       } | ||||
|     }).catch(function(error) { | ||||
|       console.error('err' + error) // for debug
 | ||||
|       errorCallback && errorCallback(error) | ||||
|       that.$message.error(error.message) | ||||
|     }) | ||||
|   }, | ||||
|   /** | ||||
|    * tip,使用方式:this.tip('操作成功'),this.tip('错误', 'error') | ||||
|    * @param msg 内容 | ||||
|    * @param type success / info / warning / error | ||||
|    * @param stay 停留几秒,默认3秒 | ||||
|    */ | ||||
|   tip: function(msg, type, stay) { | ||||
|     stay = parseInt(stay) || 3 | ||||
|     this.$message({ | ||||
|       message: msg, | ||||
|       type: type || 'success', | ||||
|       duration: stay * 1000 | ||||
|     }) | ||||
|   }, | ||||
|   /** | ||||
|    * 提醒框 | ||||
|    * @param msg 消息 | ||||
|    * @param okHandler 成功回调 | ||||
|    * @param cancelHandler | ||||
|    */ | ||||
|   confirm: function(msg, okHandler, cancelHandler) { | ||||
|     const that = this | ||||
|     this.$confirm(msg, '提示', { | ||||
|       confirmButtonText: '确定', | ||||
|       cancelButtonText: '取消', | ||||
|       type: 'warning', | ||||
|       beforeClose: (action, instance, done) => { | ||||
|         if (action === 'confirm') { | ||||
|           okHandler.call(that, done) | ||||
|         } else if (action === 'cancel') { | ||||
|           if (cancelHandler) { | ||||
|             cancelHandler.call(that, done) | ||||
|           } else { | ||||
|             done() | ||||
|           } | ||||
|         } else { | ||||
|           done() | ||||
|         } | ||||
|       } | ||||
|     }).catch(function() {}) | ||||
|   }, | ||||
|   logout: function() { | ||||
|     removeToken() | ||||
|     this.$router.push({ path: `/login?redirect=${this.$route.fullPath}` }) | ||||
|   } | ||||
| }) | ||||
| @ -0,0 +1,110 @@ | ||||
| /** | ||||
|  * Created by PanJiaChen on 16/11/18. | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * Parse the time to string | ||||
|  * @param {(Object|string|number)} time | ||||
|  * @param {string} cFormat | ||||
|  * @returns {string} | ||||
|  */ | ||||
| export function parseTime(time, cFormat) { | ||||
|   if (arguments.length === 0) { | ||||
|     return null | ||||
|   } | ||||
|   const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' | ||||
|   let date | ||||
|   if (typeof time === 'object') { | ||||
|     date = time | ||||
|   } else { | ||||
|     if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { | ||||
|       time = parseInt(time) | ||||
|     } | ||||
|     if ((typeof time === 'number') && (time.toString().length === 10)) { | ||||
|       time = time * 1000 | ||||
|     } | ||||
|     date = new Date(time) | ||||
|   } | ||||
|   const formatObj = { | ||||
|     y: date.getFullYear(), | ||||
|     m: date.getMonth() + 1, | ||||
|     d: date.getDate(), | ||||
|     h: date.getHours(), | ||||
|     i: date.getMinutes(), | ||||
|     s: date.getSeconds(), | ||||
|     a: date.getDay() | ||||
|   } | ||||
|   const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { | ||||
|     let value = formatObj[key] | ||||
|     // Note: getDay() returns 0 on Sunday
 | ||||
|     if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } | ||||
|     if (result.length > 0 && value < 10) { | ||||
|       value = '0' + value | ||||
|     } | ||||
|     return value || 0 | ||||
|   }) | ||||
|   return time_str | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {number} time | ||||
|  * @param {string} option | ||||
|  * @returns {string} | ||||
|  */ | ||||
| export function formatTime(time, option) { | ||||
|   if (('' + time).length === 10) { | ||||
|     time = parseInt(time) * 1000 | ||||
|   } else { | ||||
|     time = +time | ||||
|   } | ||||
|   const d = new Date(time) | ||||
|   const now = Date.now() | ||||
| 
 | ||||
|   const diff = (now - d) / 1000 | ||||
| 
 | ||||
|   if (diff < 30) { | ||||
|     return '刚刚' | ||||
|   } else if (diff < 3600) { | ||||
|     // less 1 hour
 | ||||
|     return Math.ceil(diff / 60) + '分钟前' | ||||
|   } else if (diff < 3600 * 24) { | ||||
|     return Math.ceil(diff / 3600) + '小时前' | ||||
|   } else if (diff < 3600 * 24 * 2) { | ||||
|     return '1天前' | ||||
|   } | ||||
|   if (option) { | ||||
|     return parseTime(time, option) | ||||
|   } else { | ||||
|     return ( | ||||
|       d.getMonth() + | ||||
|       1 + | ||||
|       '月' + | ||||
|       d.getDate() + | ||||
|       '日' + | ||||
|       d.getHours() + | ||||
|       '时' + | ||||
|       d.getMinutes() + | ||||
|       '分' | ||||
|     ) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} url | ||||
|  * @returns {Object} | ||||
|  */ | ||||
| export function param2Obj(url) { | ||||
|   const search = url.split('?')[1] | ||||
|   if (!search) { | ||||
|     return {} | ||||
|   } | ||||
|   return JSON.parse( | ||||
|     '{"' + | ||||
|       decodeURIComponent(search) | ||||
|         .replace(/"/g, '\\"') | ||||
|         .replace(/&/g, '","') | ||||
|         .replace(/=/g, '":"') | ||||
|         .replace(/\+/g, ' ') + | ||||
|       '"}' | ||||
|   ) | ||||
| } | ||||
| @ -0,0 +1,85 @@ | ||||
| import axios from 'axios' | ||||
| import { MessageBox, Message } from 'element-ui' | ||||
| import store from '@/store' | ||||
| import { getToken } from '@/utils/auth' | ||||
| 
 | ||||
| // create an axios instance
 | ||||
| const service = axios.create({ | ||||
|   baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
 | ||||
|   withCredentials: true, // send cookies when cross-domain requests
 | ||||
|   timeout: 5000 // request timeout
 | ||||
| }) | ||||
| 
 | ||||
| // request interceptor
 | ||||
| service.interceptors.request.use( | ||||
|   config => { | ||||
|     // do something before request is sent
 | ||||
| 
 | ||||
|     if (store.getters.token) { | ||||
|       // let each request carry token
 | ||||
|       // ['X-Token'] is a custom headers key
 | ||||
|       // please modify it according to the actual situation
 | ||||
|       config.headers['X-Token'] = getToken() | ||||
|     } | ||||
|     return config | ||||
|   }, | ||||
|   error => { | ||||
|     // do something with request error
 | ||||
|     console.log(error) // for debug
 | ||||
|     return Promise.reject(error) | ||||
|   } | ||||
| ) | ||||
| 
 | ||||
| // response interceptor
 | ||||
| service.interceptors.response.use( | ||||
|   /** | ||||
|    * If you want to get http information such as headers or status | ||||
|    * Please return  response => response | ||||
|   */ | ||||
| 
 | ||||
|   /** | ||||
|    * Determine the request status by custom code | ||||
|    * Here is just an example | ||||
|    * You can also judge the status by HTTP Status Code | ||||
|    */ | ||||
|   response => { | ||||
|     const res = response.data | ||||
| 
 | ||||
|     // if the custom code is not 20000, it is judged as an error.
 | ||||
|     if (res.code !== 20000) { | ||||
|       Message({ | ||||
|         message: res.message || 'error', | ||||
|         type: 'error', | ||||
|         duration: 5 * 1000 | ||||
|       }) | ||||
| 
 | ||||
|       // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
 | ||||
|       if (res.code === 50008 || res.code === 50012 || res.code === 50014) { | ||||
|         // to re-login
 | ||||
|         MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { | ||||
|           confirmButtonText: 'Re-Login', | ||||
|           cancelButtonText: 'Cancel', | ||||
|           type: 'warning' | ||||
|         }).then(() => { | ||||
|           store.dispatch('user/resetToken').then(() => { | ||||
|             location.reload() | ||||
|           }) | ||||
|         }) | ||||
|       } | ||||
|       return Promise.reject(res.message || 'error') | ||||
|     } else { | ||||
|       return res | ||||
|     } | ||||
|   }, | ||||
|   error => { | ||||
|     console.log('err' + error) // for debug
 | ||||
|     Message({ | ||||
|       message: error.message, | ||||
|       type: 'error', | ||||
|       duration: 5 * 1000 | ||||
|     }) | ||||
|     return Promise.reject(error) | ||||
|   } | ||||
| ) | ||||
| 
 | ||||
| export default service | ||||
| @ -0,0 +1,20 @@ | ||||
| /** | ||||
|  * Created by PanJiaChen on 16/11/18. | ||||
|  */ | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} path | ||||
|  * @returns {Boolean} | ||||
|  */ | ||||
| export function isExternal(path) { | ||||
|   return /^(https?:|mailto:|tel:)/.test(path) | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} str | ||||
|  * @returns {Boolean} | ||||
|  */ | ||||
| export function validUsername(str) { | ||||
|   const valid_map = ['admin', 'editor'] | ||||
|   return valid_map.indexOf(str.trim()) >= 0 | ||||
| } | ||||
| @ -0,0 +1,228 @@ | ||||
| <template> | ||||
|   <div class="wscn-http404-container"> | ||||
|     <div class="wscn-http404"> | ||||
|       <div class="pic-404"> | ||||
|         <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404"> | ||||
|         <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404"> | ||||
|         <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404"> | ||||
|         <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404"> | ||||
|       </div> | ||||
|       <div class="bullshit"> | ||||
|         <div class="bullshit__oops">OOPS!</div> | ||||
|         <div class="bullshit__info">All rights reserved | ||||
|           <a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a> | ||||
|         </div> | ||||
|         <div class="bullshit__headline">{{ message }}</div> | ||||
|         <div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div> | ||||
|         <a href="" class="bullshit__return-home">Back to home</a> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| 
 | ||||
| export default { | ||||
|   name: 'Page404', | ||||
|   computed: { | ||||
|     message() { | ||||
|       return 'The webmaster said that you can not enter this page...' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .wscn-http404-container{ | ||||
|   transform: translate(-50%,-50%); | ||||
|   position: absolute; | ||||
|   top: 40%; | ||||
|   left: 50%; | ||||
| } | ||||
| .wscn-http404 { | ||||
|   position: relative; | ||||
|   width: 1200px; | ||||
|   padding: 0 50px; | ||||
|   overflow: hidden; | ||||
|   .pic-404 { | ||||
|     position: relative; | ||||
|     float: left; | ||||
|     width: 600px; | ||||
|     overflow: hidden; | ||||
|     &__parent { | ||||
|       width: 100%; | ||||
|     } | ||||
|     &__child { | ||||
|       position: absolute; | ||||
|       &.left { | ||||
|         width: 80px; | ||||
|         top: 17px; | ||||
|         left: 220px; | ||||
|         opacity: 0; | ||||
|         animation-name: cloudLeft; | ||||
|         animation-duration: 2s; | ||||
|         animation-timing-function: linear; | ||||
|         animation-fill-mode: forwards; | ||||
|         animation-delay: 1s; | ||||
|       } | ||||
|       &.mid { | ||||
|         width: 46px; | ||||
|         top: 10px; | ||||
|         left: 420px; | ||||
|         opacity: 0; | ||||
|         animation-name: cloudMid; | ||||
|         animation-duration: 2s; | ||||
|         animation-timing-function: linear; | ||||
|         animation-fill-mode: forwards; | ||||
|         animation-delay: 1.2s; | ||||
|       } | ||||
|       &.right { | ||||
|         width: 62px; | ||||
|         top: 100px; | ||||
|         left: 500px; | ||||
|         opacity: 0; | ||||
|         animation-name: cloudRight; | ||||
|         animation-duration: 2s; | ||||
|         animation-timing-function: linear; | ||||
|         animation-fill-mode: forwards; | ||||
|         animation-delay: 1s; | ||||
|       } | ||||
|       @keyframes cloudLeft { | ||||
|         0% { | ||||
|           top: 17px; | ||||
|           left: 220px; | ||||
|           opacity: 0; | ||||
|         } | ||||
|         20% { | ||||
|           top: 33px; | ||||
|           left: 188px; | ||||
|           opacity: 1; | ||||
|         } | ||||
|         80% { | ||||
|           top: 81px; | ||||
|           left: 92px; | ||||
|           opacity: 1; | ||||
|         } | ||||
|         100% { | ||||
|           top: 97px; | ||||
|           left: 60px; | ||||
|           opacity: 0; | ||||
|         } | ||||
|       } | ||||
|       @keyframes cloudMid { | ||||
|         0% { | ||||
|           top: 10px; | ||||
|           left: 420px; | ||||
|           opacity: 0; | ||||
|         } | ||||
|         20% { | ||||
|           top: 40px; | ||||
|           left: 360px; | ||||
|           opacity: 1; | ||||
|         } | ||||
|         70% { | ||||
|           top: 130px; | ||||
|           left: 180px; | ||||
|           opacity: 1; | ||||
|         } | ||||
|         100% { | ||||
|           top: 160px; | ||||
|           left: 120px; | ||||
|           opacity: 0; | ||||
|         } | ||||
|       } | ||||
|       @keyframes cloudRight { | ||||
|         0% { | ||||
|           top: 100px; | ||||
|           left: 500px; | ||||
|           opacity: 0; | ||||
|         } | ||||
|         20% { | ||||
|           top: 120px; | ||||
|           left: 460px; | ||||
|           opacity: 1; | ||||
|         } | ||||
|         80% { | ||||
|           top: 180px; | ||||
|           left: 340px; | ||||
|           opacity: 1; | ||||
|         } | ||||
|         100% { | ||||
|           top: 200px; | ||||
|           left: 300px; | ||||
|           opacity: 0; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   .bullshit { | ||||
|     position: relative; | ||||
|     float: left; | ||||
|     width: 300px; | ||||
|     padding: 30px 0; | ||||
|     overflow: hidden; | ||||
|     &__oops { | ||||
|       font-size: 32px; | ||||
|       font-weight: bold; | ||||
|       line-height: 40px; | ||||
|       color: #1482f0; | ||||
|       opacity: 0; | ||||
|       margin-bottom: 20px; | ||||
|       animation-name: slideUp; | ||||
|       animation-duration: 0.5s; | ||||
|       animation-fill-mode: forwards; | ||||
|     } | ||||
|     &__headline { | ||||
|       font-size: 20px; | ||||
|       line-height: 24px; | ||||
|       color: #222; | ||||
|       font-weight: bold; | ||||
|       opacity: 0; | ||||
|       margin-bottom: 10px; | ||||
|       animation-name: slideUp; | ||||
|       animation-duration: 0.5s; | ||||
|       animation-delay: 0.1s; | ||||
|       animation-fill-mode: forwards; | ||||
|     } | ||||
|     &__info { | ||||
|       font-size: 13px; | ||||
|       line-height: 21px; | ||||
|       color: grey; | ||||
|       opacity: 0; | ||||
|       margin-bottom: 30px; | ||||
|       animation-name: slideUp; | ||||
|       animation-duration: 0.5s; | ||||
|       animation-delay: 0.2s; | ||||
|       animation-fill-mode: forwards; | ||||
|     } | ||||
|     &__return-home { | ||||
|       display: block; | ||||
|       float: left; | ||||
|       width: 110px; | ||||
|       height: 36px; | ||||
|       background: #1482f0; | ||||
|       border-radius: 100px; | ||||
|       text-align: center; | ||||
|       color: #ffffff; | ||||
|       opacity: 0; | ||||
|       font-size: 14px; | ||||
|       line-height: 36px; | ||||
|       cursor: pointer; | ||||
|       animation-name: slideUp; | ||||
|       animation-duration: 0.5s; | ||||
|       animation-delay: 0.3s; | ||||
|       animation-fill-mode: forwards; | ||||
|     } | ||||
|     @keyframes slideUp { | ||||
|       0% { | ||||
|         transform: translateY(60px); | ||||
|         opacity: 0; | ||||
|       } | ||||
|       100% { | ||||
|         transform: translateY(0); | ||||
|         opacity: 1; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @ -0,0 +1,26 @@ | ||||
| <template> | ||||
|   <div class="dashboard-container"> | ||||
|     <div class="dashboard-text">欢迎使用SOP Admin</div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   created() { | ||||
|     this.post('admin.userinfo.get', {}, function(resp) { | ||||
|     }) | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .dashboard { | ||||
|   &-container { | ||||
|     margin: 30px; | ||||
|   } | ||||
|   &-text { | ||||
|     font-size: 18px; | ||||
|     line-height: 46px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @ -0,0 +1,328 @@ | ||||
| <template> | ||||
|   <div class="app-container"> | ||||
|     <el-form :inline="true" :model="searchFormData" class="demo-form-inline"> | ||||
|       <el-form-item label="appKey"> | ||||
|         <el-input v-model="searchFormData.appKey" :clearable="true" placeholder="appKey" size="mini" style="width: 250px;" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button type="primary" prefix-icon="el-icon-search" size="mini" @click="onSearchTable">查询</el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|     <el-button type="primary" size="mini" icon="el-icon-plus" @click="onAdd">新增ISV</el-button> | ||||
|     <el-table | ||||
|       :data="pageInfo.list" | ||||
|       border | ||||
|       fit | ||||
|       highlight-current-row | ||||
|       style="margin-top: 20px;" | ||||
|     > | ||||
|       <el-table-column | ||||
|         prop="id" | ||||
|         label="ID" | ||||
|         width="80" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         prop="appKey" | ||||
|         label="appKey" | ||||
|         width="250" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         prop="secret" | ||||
|         label="secret" | ||||
|         width="80" | ||||
|       > | ||||
|         <template slot-scope="scope"> | ||||
|           <el-button v-if="scope.row.signType === 2" type="text" size="mini" @click="onShowSecret(scope.row)">查看</el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         prop="" | ||||
|         label="公私钥" | ||||
|         width="80" | ||||
|       > | ||||
|         <template slot-scope="scope"> | ||||
|           <el-button v-if="scope.row.signType === 1" type="text" size="mini" @click="onShowPriPubKey(scope.row)">查看</el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         prop="signType" | ||||
|         label="签名类型" | ||||
|         width="80" | ||||
|       > | ||||
|         <template slot-scope="scope"> | ||||
|           <span v-if="scope.row.signType === 1">RSA2</span> | ||||
|           <span v-if="scope.row.signType === 2">MD5</span> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         prop="roleList" | ||||
|         label="角色" | ||||
|       > | ||||
|         <template slot-scope="scope"> | ||||
|           <div v-html="roleRender(scope.row)"></div> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         prop="status" | ||||
|         label="状态" | ||||
|         width="80" | ||||
|       > | ||||
|         <template slot-scope="scope"> | ||||
|           <span v-if="scope.row.status === 1" style="color:#67C23A">已启用</span> | ||||
|           <span v-if="scope.row.status === 2" style="color:#F56C6C">已禁用</span> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         prop="gmtCreate" | ||||
|         label="添加时间" | ||||
|         width="160" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         prop="gmtModified" | ||||
|         label="修改时间" | ||||
|         width="160" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         label="操作" | ||||
|         fixed="right" | ||||
|         width="100" | ||||
|       > | ||||
|         <template slot-scope="scope"> | ||||
|           <el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|     <el-pagination | ||||
|       background | ||||
|       style="margin-top: 5px" | ||||
|       :current-page="pageInfo.pageIndex" | ||||
|       :page-sizes="[5, 10, 20, 40]" | ||||
|       :page-size="pageInfo.pageSize" | ||||
|       :total="pageInfo.total" | ||||
|       layout="total, sizes, prev, pager, next" | ||||
|       @size-change="onSizeChange" | ||||
|       @current-change="onPageIndexChange" | ||||
|     /> | ||||
|     <!-- dialog --> | ||||
|     <el-dialog | ||||
|       :title="isvDialogTitle" | ||||
|       :visible.sync="isvDialogVisible" | ||||
|       :close-on-click-modal="false" | ||||
|       @close="onIsvDialogClose" | ||||
|     > | ||||
|       <el-form ref="isvForm" :rules="rulesIsvForm" :model="isvDialogFormData"> | ||||
|         <el-form-item label="" :label-width="formLabelWidth"> | ||||
|           <el-button size="mini" @click="onDataGen">一键生成数据</el-button> | ||||
|         </el-form-item> | ||||
|         <el-form-item prop="appKey" label="appKey" :label-width="formLabelWidth"> | ||||
|           <el-input v-model="isvDialogFormData.appKey" size="mini" /> | ||||
|         </el-form-item> | ||||
|         <el-form-item prop="signType" label="签名方式" :label-width="formLabelWidth"> | ||||
|           <el-radio-group v-model="isvDialogFormData.signType"> | ||||
|             <el-radio :label="1" name="status">RSA2</el-radio> | ||||
|             <el-radio :label="2" name="status">MD5</el-radio> | ||||
|           </el-radio-group> | ||||
|         </el-form-item> | ||||
|         <el-form-item v-show="isvDialogFormData.signType === 2" prop="secret" label="secret" :label-width="formLabelWidth"> | ||||
|           <el-input v-model="isvDialogFormData.secret" size="mini" /> | ||||
|         </el-form-item> | ||||
|         <el-form-item v-show="isvDialogFormData.signType === 1" prop="pubKey" label="公钥" :label-width="formLabelWidth"> | ||||
|           <el-input v-model="isvDialogFormData.pubKey" type="textarea" /> | ||||
|         </el-form-item> | ||||
|         <el-form-item v-show="isvDialogFormData.signType === 1" prop="priKey" label="私钥" :label-width="formLabelWidth"> | ||||
|           <el-input v-model="isvDialogFormData.priKey" type="textarea" /> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="角色" :label-width="formLabelWidth"> | ||||
|           <el-checkbox-group v-model="isvDialogFormData.roleCode"> | ||||
|             <el-checkbox v-for="item in roles" :key="item.roleCode" :label="item.roleCode">{{ item.description }}</el-checkbox> | ||||
|           </el-checkbox-group> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="状态" :label-width="formLabelWidth"> | ||||
|           <el-radio-group v-model="isvDialogFormData.status"> | ||||
|             <el-radio :label="1" name="status">启用</el-radio> | ||||
|             <el-radio :label="2" name="status">禁用</el-radio> | ||||
|           </el-radio-group> | ||||
|         </el-form-item> | ||||
|       </el-form> | ||||
|       <div slot="footer" class="dialog-footer"> | ||||
|         <el-button @click="isvDialogVisible = false">取 消</el-button> | ||||
|         <el-button type="primary" @click="onIsvDialogSave">保 存</el-button> | ||||
|       </div> | ||||
|     </el-dialog> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   data() { | ||||
|     const validateSecret = (rule, value, callback) => { | ||||
|       if (this.isvDialogFormData.signType === 2) { | ||||
|         if (value === '') { | ||||
|           callback(new Error('不能为空')) | ||||
|         } | ||||
|         if (value.length > 200) { | ||||
|           callback(new Error('长度不能超过200')) | ||||
|         } | ||||
|       } | ||||
|       callback() | ||||
|     } | ||||
|     const validatePubPriKey = (rule, value, callback) => { | ||||
|       if (this.isvDialogFormData.signType === 1) { | ||||
|         if (value === '') { | ||||
|           callback(new Error('不能为空')) | ||||
|         } | ||||
|       } | ||||
|       callback() | ||||
|     } | ||||
|     return { | ||||
|       formLabelWidth: '120px', | ||||
|       searchFormData: { | ||||
|         appKey: '' | ||||
|       }, | ||||
|       pageInfo: { | ||||
|         list: [], | ||||
|         pageIndex: 1, | ||||
|         pageSize: 10, | ||||
|         total: 0 | ||||
|       }, | ||||
|       roles: [], | ||||
|       // dialog | ||||
|       isvDialogVisible: false, | ||||
|       isvDialogTitle: '新增ISV', | ||||
|       isvDialogFormData: { | ||||
|         id: 0, | ||||
|         appKey: '', | ||||
|         secret: '', | ||||
|         pubKey: '', | ||||
|         priKey: '', | ||||
|         signType: 1, | ||||
|         status: 1, | ||||
|         roleCode: [] | ||||
|       }, | ||||
|       rulesIsvForm: { | ||||
|         appKey: [ | ||||
|           { required: true, message: '不能为空', trigger: 'blur' }, | ||||
|           { min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' } | ||||
|         ], | ||||
|         secret: [ | ||||
|           { validator: validateSecret, trigger: 'blur' } | ||||
|         ], | ||||
|         pubKey: [ | ||||
|           { validator: validatePubPriKey, trigger: 'blur' } | ||||
|         ], | ||||
|         priKey: [ | ||||
|           { validator: validatePubPriKey, trigger: 'blur' } | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     this.loadTable() | ||||
|     this.loadRouteRole() | ||||
|   }, | ||||
|   methods: { | ||||
|     loadTable() { | ||||
|       this.post('isv.info.page', this.searchFormData, function(resp) { | ||||
|         this.pageInfo = resp.data | ||||
|       }) | ||||
|     }, | ||||
|     loadRouteRole: function() { | ||||
|       if (this.roles.length === 0) { | ||||
|         this.post('role.listall', {}, function(resp) { | ||||
|           this.roles = resp.data | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     onShowSecret: function(row) { | ||||
|       this.$alert(row.secret, 'secret') | ||||
|     }, | ||||
|     onShowPriPubKey: function(row) { | ||||
|       const pubKey = row.pubKey | ||||
|       const priKey = row.priKey | ||||
|       const content = '<div>公钥:<textarea style="width: 380px;height: 100px;" readonly="readonly">' + pubKey + '</textarea><br>' + | ||||
|         '私钥:<textarea style="width: 380px;height: 100px;" readonly="readonly">' + priKey + '</textarea></div>' | ||||
|       this.$alert(content, '公私钥', { | ||||
|         dangerouslyUseHTMLString: true | ||||
|       }) | ||||
|     }, | ||||
|     onSearchTable: function() { | ||||
|       this.loadTable() | ||||
|     }, | ||||
|     onTableUpdate: function(row) { | ||||
|       this.isvDialogTitle = '修改ISV' | ||||
|       this.isvDialogVisible = true | ||||
|       this.$nextTick(() => { | ||||
|         this.post('isv.info.get', { id: row.id }, function(resp) { | ||||
|           const isvInfo = resp.data | ||||
|           const roleList = isvInfo.roleList | ||||
|           const roleCode = [] | ||||
|           for (let i = 0; i < roleList.length; i++) { | ||||
|             roleCode.push(roleList[i].roleCode) | ||||
|           } | ||||
|           isvInfo.roleCode = roleCode | ||||
|           Object.assign(this.isvDialogFormData, isvInfo) | ||||
|         }) | ||||
|       }) | ||||
|     }, | ||||
|     onSizeChange: function(size) { | ||||
|       this.searchFormData.pageSize = size | ||||
|       this.loadTable() | ||||
|     }, | ||||
|     onPageIndexChange: function(pageIndex) { | ||||
|       this.searchFormData.pageIndex = pageIndex | ||||
|       this.loadTable() | ||||
|     }, | ||||
|     onAdd: function() { | ||||
|       this.isvDialogFormData.id = 0 | ||||
|       this.isvDialogTitle = '新增ISV' | ||||
|       this.isvDialogVisible = true | ||||
|     }, | ||||
|     onIsvDialogSave: function() { | ||||
|       const that = this | ||||
|       this.$refs['isvForm'].validate((valid) => { | ||||
|         if (valid) { | ||||
|           const uri = this.isvDialogFormData.id === 0 ? 'isv.info.add' : 'isv.info.update' | ||||
|           that.post(uri, that.isvDialogFormData, function() { | ||||
|             that.isvDialogVisible = false | ||||
|             that.loadTable() | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     onIsvDialogClose: function() { | ||||
|       this.$refs.isvForm.resetFields() | ||||
|       this.isvDialogVisible = false | ||||
|     }, | ||||
|     roleRender: function(row) { | ||||
|       const html = [] | ||||
|       const roleList = row.roleList | ||||
|       for (let i = 0; i < roleList.length; i++) { | ||||
|         html.push(roleList[i].description) | ||||
|       } | ||||
|       return html.join(', ') | ||||
|     }, | ||||
|     onDataGen: function() { | ||||
|       this.post('isv.form.gen', {}, function(resp) { | ||||
|         const data = resp.data | ||||
|         // 如果是新增状态 | ||||
|         if (this.isvDialogFormData.id === 0) { | ||||
|           Object.assign(this.isvDialogFormData, data) | ||||
|         } else { | ||||
|           const signType = this.isvDialogFormData.signType | ||||
|           // RSA2 | ||||
|           if (signType === 1) { | ||||
|             Object.assign(this.isvDialogFormData, { | ||||
|               pubKey: data.pubKey, | ||||
|               priKey: data.priKey | ||||
|             }) | ||||
|           } else if (signType === 2) { | ||||
|             Object.assign(this.isvDialogFormData, { | ||||
|               secret: data.secret | ||||
|             }) | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,249 @@ | ||||
| <template> | ||||
|   <div class="login-container"> | ||||
|     <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left"> | ||||
| 
 | ||||
|       <div class="title-container"> | ||||
|         <h3 class="title">SOP Admin</h3> | ||||
|       </div> | ||||
| 
 | ||||
|       <el-form-item prop="username"> | ||||
|         <span class="svg-container"> | ||||
|           <svg-icon icon-class="user" /> | ||||
|         </span> | ||||
|         <el-input | ||||
|           ref="username" | ||||
|           v-model="loginForm.username" | ||||
|           placeholder="用户名" | ||||
|           name="username" | ||||
|           type="text" | ||||
|           tabindex="1" | ||||
|           auto-complete="on" | ||||
|         /> | ||||
|       </el-form-item> | ||||
| 
 | ||||
|       <el-form-item prop="password"> | ||||
|         <span class="svg-container"> | ||||
|           <svg-icon icon-class="password" /> | ||||
|         </span> | ||||
|         <el-input | ||||
|           :key="passwordType" | ||||
|           ref="password" | ||||
|           v-model="loginForm.password" | ||||
|           :type="passwordType" | ||||
|           placeholder="密码" | ||||
|           name="password" | ||||
|           tabindex="2" | ||||
|           auto-complete="on" | ||||
|           @keyup.enter.native="handleLogin" | ||||
|         /> | ||||
|         <span class="show-pwd" @click="showPwd"> | ||||
|           <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" /> | ||||
|         </span> | ||||
|       </el-form-item> | ||||
| 
 | ||||
|       <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登 录</el-button> | ||||
| 
 | ||||
|     </el-form> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { validUsername } from '@/utils/validate' | ||||
| import md5 from 'js-md5' | ||||
| import { setToken } from '@/utils/auth' | ||||
| 
 | ||||
| export default { | ||||
|   name: 'Login', | ||||
|   data() { | ||||
|     const validateUsername = (rule, value, callback) => { | ||||
|       if (!validUsername(value)) { | ||||
|         callback(new Error('请输入正确的用户名')) | ||||
|       } else { | ||||
|         callback() | ||||
|       } | ||||
|     } | ||||
|     const validatePassword = (rule, value, callback) => { | ||||
|       if (value.length === 0) { | ||||
|         callback(new Error('请输入密码')) | ||||
|       } else if (value.length < 6) { | ||||
|         callback(new Error('请密码长度不得小于6位')) | ||||
|       } else { | ||||
|         callback() | ||||
|       } | ||||
|     } | ||||
|     return { | ||||
|       loginForm: { | ||||
|         username: '', | ||||
|         password: '' | ||||
|       }, | ||||
|       loginRules: { | ||||
|         username: [{ required: true, trigger: 'blur', validator: validateUsername }], | ||||
|         password: [{ required: true, trigger: 'blur', validator: validatePassword }] | ||||
|       }, | ||||
|       loading: false, | ||||
|       passwordType: 'password', | ||||
|       redirect: undefined | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     $route: { | ||||
|       handler: function(route) { | ||||
|         this.redirect = route.query && route.query.redirect | ||||
|       }, | ||||
|       immediate: true | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     showPwd() { | ||||
|       if (this.passwordType === 'password') { | ||||
|         this.passwordType = '' | ||||
|       } else { | ||||
|         this.passwordType = 'password' | ||||
|       } | ||||
|       this.$nextTick(() => { | ||||
|         this.$refs.password.focus() | ||||
|       }) | ||||
|     }, | ||||
|     handleLogin() { | ||||
|       this.$refs.loginForm.validate(valid => { | ||||
|         // if (valid) { | ||||
|         //   this.loading = true | ||||
|         //   this.$store.dispatch('user/login', this.loginForm).then(() => { | ||||
|         //     this.$router.push({ path: this.redirect || '/' }) | ||||
|         //     this.loading = false | ||||
|         //   }).catch(() => { | ||||
|         //     this.loading = false | ||||
|         //   }) | ||||
|         // } else { | ||||
|         //   console.log('error submit!!') | ||||
|         //   return false | ||||
|         // } | ||||
|         if (valid) { | ||||
|           const data = this.loginForm | ||||
|           let pwd = data.password | ||||
|           pwd = md5(pwd) | ||||
|           const postData = { | ||||
|             username: data.username, | ||||
|             password: pwd | ||||
|           } | ||||
|           this.post('nologin.admin.login', postData, function(resp) { | ||||
|             setToken(resp.data) | ||||
|             this.$router.push({ path: this.redirect || '/' }) | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss"> | ||||
| /* 修复input 背景不协调 和光标变色 */ | ||||
| /* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */ | ||||
| 
 | ||||
| $bg:#283443; | ||||
| $light_gray:#fff; | ||||
| $cursor: #fff; | ||||
| 
 | ||||
| @supports (-webkit-mask: none) and (not (cater-color: $cursor)) { | ||||
|   .login-container .el-input input { | ||||
|     color: $cursor; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* reset element-ui css */ | ||||
| .login-container { | ||||
|   .el-input { | ||||
|     display: inline-block; | ||||
|     height: 47px; | ||||
|     width: 85%; | ||||
| 
 | ||||
|     input { | ||||
|       background: transparent; | ||||
|       border: 0px; | ||||
|       -webkit-appearance: none; | ||||
|       border-radius: 0px; | ||||
|       padding: 12px 5px 12px 15px; | ||||
|       color: $light_gray; | ||||
|       height: 47px; | ||||
|       caret-color: $cursor; | ||||
| 
 | ||||
|       &:-webkit-autofill { | ||||
|         box-shadow: 0 0 0px 1000px $bg inset !important; | ||||
|         -webkit-text-fill-color: $cursor !important; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .el-form-item { | ||||
|     border: 1px solid rgba(255, 255, 255, 0.1); | ||||
|     background: rgba(0, 0, 0, 0.1); | ||||
|     border-radius: 5px; | ||||
|     color: #454545; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| $bg:#2d3a4b; | ||||
| $dark_gray:#889aa4; | ||||
| $light_gray:#eee; | ||||
| 
 | ||||
| .login-container { | ||||
|   min-height: 100%; | ||||
|   width: 100%; | ||||
|   background-color: $bg; | ||||
|   overflow: hidden; | ||||
| 
 | ||||
|   .login-form { | ||||
|     position: relative; | ||||
|     width: 520px; | ||||
|     max-width: 100%; | ||||
|     padding: 160px 35px 0; | ||||
|     margin: 0 auto; | ||||
|     overflow: hidden; | ||||
|   } | ||||
| 
 | ||||
|   .tips { | ||||
|     font-size: 14px; | ||||
|     color: #fff; | ||||
|     margin-bottom: 10px; | ||||
| 
 | ||||
|     span { | ||||
|       &:first-of-type { | ||||
|         margin-right: 16px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .svg-container { | ||||
|     padding: 6px 5px 6px 15px; | ||||
|     color: $dark_gray; | ||||
|     vertical-align: middle; | ||||
|     width: 30px; | ||||
|     display: inline-block; | ||||
|   } | ||||
| 
 | ||||
|   .title-container { | ||||
|     position: relative; | ||||
| 
 | ||||
|     .title { | ||||
|       font-size: 26px; | ||||
|       color: $light_gray; | ||||
|       margin: 0px auto 40px auto; | ||||
|       text-align: center; | ||||
|       font-weight: bold; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .show-pwd { | ||||
|     position: absolute; | ||||
|     right: 10px; | ||||
|     top: 7px; | ||||
|     font-size: 16px; | ||||
|     color: $dark_gray; | ||||
|     cursor: pointer; | ||||
|     user-select: none; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @ -0,0 +1,347 @@ | ||||
| <template> | ||||
|   <div class="app-container"> | ||||
|     <el-container> | ||||
|       <el-aside style="min-height: 300px;width: 200px;"> | ||||
|         <el-input v-model="filterText" prefix-icon="el-icon-search" placeholder="搜索服务..." style="margin-bottom:20px;" size="mini" clearable /> | ||||
|         <el-tree | ||||
|           ref="tree2" | ||||
|           :data="treeData" | ||||
|           :props="defaultProps" | ||||
|           :filter-node-method="filterNode" | ||||
|           :highlight-current="true" | ||||
|           :expand-on-click-node="false" | ||||
|           empty-text="无数据" | ||||
|           node-key="id" | ||||
|           class="filter-tree" | ||||
|           default-expand-all | ||||
|           @node-click="onNodeClick" | ||||
|         > | ||||
|           <span slot-scope="{ node, data }" class="custom-tree-node"> | ||||
|             <span v-if="data.label.length < 15">{{ data.label }}</span> | ||||
|             <span v-else> | ||||
|               <el-tooltip :content="data.label" class="item" effect="light" placement="right"> | ||||
|                 <span>{{ data.label.substring(0, 15) + '...' }}</span> | ||||
|               </el-tooltip> | ||||
|             </span> | ||||
|           </span> | ||||
|         </el-tree> | ||||
|       </el-aside> | ||||
|       <el-main style="padding-top:0"> | ||||
|         <el-form :inline="true" :model="searchFormData" class="demo-form-inline"> | ||||
|           <el-form-item label="路由名称"> | ||||
|             <el-input v-model="searchFormData.id" placeholder="输入接口名或版本号" size="mini" /> | ||||
|           </el-form-item> | ||||
|           <el-form-item> | ||||
|             <el-button type="primary" icon="el-icon-search" size="mini" @click="onSearchTable">查询</el-button> | ||||
|           </el-form-item> | ||||
|         </el-form> | ||||
|         <el-table | ||||
|           :data="tableData" | ||||
|           border | ||||
|           max-height="500" | ||||
|         > | ||||
|           <el-table-column | ||||
|             prop="name" | ||||
|             label="接口名 (版本号)" | ||||
|             width="200" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               {{ scope.row.name + ' (' + scope.row.version + ')' }} | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             prop="limitType" | ||||
|             label="限流策略" | ||||
|             width="120" | ||||
|           > | ||||
|             <template slot-scope slot="header"> | ||||
|               限流策略 <i class="el-icon-question" style="cursor: pointer" @click="onLimitTypeTipClick"></i> | ||||
|             </template> | ||||
|             <template slot-scope="scope"> | ||||
|               <span v-if="scope.row.limitType === 1">漏桶策略</span> | ||||
|               <span v-if="scope.row.limitType === 2">令牌桶策略</span> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             prop="info" | ||||
|             label="限流信息" | ||||
|             width="500" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               <span v-html="infoRender(scope.row)"></span> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             prop="limitStatus" | ||||
|             label="状态" | ||||
|             width="80" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               <span v-if="scope.row.limitStatus === 1" style="color:#67C23A">已开启</span> | ||||
|               <span v-if="scope.row.limitStatus === 0" style="color:#909399">已关闭</span> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             label="操作" | ||||
|             width="80" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               <el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|         </el-table> | ||||
|         <!-- dialog --> | ||||
|         <el-dialog | ||||
|           title="设置限流" | ||||
|           :visible.sync="limitDialogVisible" | ||||
|           :close-on-click-modal="false" | ||||
|           @close="onLimitDialogClose" | ||||
|         > | ||||
|           <el-form ref="limitDialogFormMain" :model="limitDialogFormData"> | ||||
|             <el-form-item label="id" :label-width="formLabelWidth"> | ||||
|               <el-input v-model="limitDialogFormData.routeId" readonly="readonly"/> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="限流策略" :label-width="formLabelWidth"> | ||||
|               <el-radio-group v-model="limitDialogFormData.limitType"> | ||||
|                 <el-radio :label="1">漏桶策略</el-radio> | ||||
|                 <el-radio :label="2">令牌桶策略</el-radio> | ||||
|               </el-radio-group> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="开启状态" :label-width="formLabelWidth"> | ||||
|               <el-switch | ||||
|                 v-model="limitDialogFormData.limitStatus" | ||||
|                 active-color="#13ce66" | ||||
|                 inactive-color="#ff4949" | ||||
|                 :active-value="1" | ||||
|                 :inactive-value="0" | ||||
|               > | ||||
|               </el-switch> | ||||
|             </el-form-item> | ||||
|           </el-form> | ||||
|           <el-form | ||||
|             v-show="limitDialogFormData.limitType === 1 && limitDialogFormData.limitStatus" | ||||
|             ref="limitDialogFormLeaky" | ||||
|             :rules="rulesLeaky" | ||||
|             :model="limitDialogFormData" | ||||
|           > | ||||
|             <el-form-item label="每秒可处理请求数" prop="execCountPerSecond" :label-width="formLabelWidth"> | ||||
|               <el-input-number v-model="limitDialogFormData.execCountPerSecond" controls-position="right" :min="1" /> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="错误码" prop="limitCode" :label-width="formLabelWidth"> | ||||
|               <el-input v-model="limitDialogFormData.limitCode" /> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="错误信息" prop="limitMsg" :label-width="formLabelWidth"> | ||||
|               <el-input v-model="limitDialogFormData.limitMsg" /> | ||||
|             </el-form-item> | ||||
|           </el-form> | ||||
|           <el-form | ||||
|             v-show="limitDialogFormData.limitType === 2 && limitDialogFormData.limitStatus" | ||||
|             ref="limitDialogFormToken" | ||||
|             :rules="rulesToken" | ||||
|             :model="limitDialogFormData" | ||||
|           > | ||||
|             <el-form-item label="令牌桶容量" prop="tokenBucketCount" :label-width="formLabelWidth"> | ||||
|               <el-input-number v-model="limitDialogFormData.tokenBucketCount" controls-position="right" :min="1" /> | ||||
|             </el-form-item> | ||||
|           </el-form> | ||||
|           <div slot="footer" class="dialog-footer"> | ||||
|             <el-button @click="limitDialogVisible = false">取 消</el-button> | ||||
|             <el-button type="primary" @click="onLimitDialogSave">保 存</el-button> | ||||
|           </div> | ||||
|         </el-dialog> | ||||
|       </el-main> | ||||
|     </el-container> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       filterText: '', | ||||
|       treeData: [], | ||||
|       tableData: [], | ||||
|       serviceId: '', | ||||
|       searchFormData: {}, | ||||
|       defaultProps: { | ||||
|         children: 'children', | ||||
|         label: 'label' | ||||
|       }, | ||||
|       // dialog | ||||
|       limitDialogFormData: { | ||||
|         routeId: '', | ||||
|         execCountPerSecond: 5, | ||||
|         limitCode: '', | ||||
|         limitMsg: '', | ||||
|         tokenBucketCount: 5, | ||||
|         limitStatus: 0, // 0: 停用,1:启用 | ||||
|         limitType: 1 | ||||
|       }, | ||||
|       rulesLeaky: { | ||||
|         execCountPerSecond: [ | ||||
|           { required: true, message: '不能为空', trigger: 'blur' } | ||||
|         ], | ||||
|         limitCode: [ | ||||
|           { required: true, message: '不能为空', trigger: 'blur' }, | ||||
|           { min: 1, max: 64, message: '长度在 1 到 64 个字符', trigger: 'blur' } | ||||
|         ], | ||||
|         limitMsg: [ | ||||
|           { required: true, message: '不能为空', trigger: 'blur' }, | ||||
|           { min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' } | ||||
|         ] | ||||
|       }, | ||||
|       rulesToken: { | ||||
|         tokenBucketCount: [ | ||||
|           { required: true, message: '不能为空', trigger: 'blur' } | ||||
|         ] | ||||
|       }, | ||||
|       formLabelWidth: '150px', | ||||
|       limitDialogVisible: false | ||||
| 
 | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     filterText(val) { | ||||
|       this.$refs.tree2.filter(val) | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     this.loadTree() | ||||
|   }, | ||||
|   methods: { | ||||
|     // 加载树 | ||||
|     loadTree: function() { | ||||
|       this.post('service.list', {}, function(resp) { | ||||
|         const respData = resp.data | ||||
|         this.treeData = this.convertToTreeData(respData, 0) | ||||
|       }) | ||||
|     }, | ||||
|     // 树搜索 | ||||
|     filterNode(value, data) { | ||||
|       if (!value) return true | ||||
|       return data.label.indexOf(value) !== -1 | ||||
|     }, | ||||
|     // 树点击事件 | ||||
|     onNodeClick(data, node, tree) { | ||||
|       if (data.parentId) { | ||||
|         this.serviceId = data.label | ||||
|         this.searchFormData.serviceId = this.serviceId | ||||
|         this.loadTable() | ||||
|       } | ||||
|     }, | ||||
|     /** | ||||
|      * 数组转成树状结构 | ||||
|      * @param data 数据结构 [{ | ||||
|         "_parentId": 14, | ||||
|         "gmtCreate": "2019-01-15 09:44:38", | ||||
|         "gmtUpdate": "2019-01-15 09:44:38", | ||||
|         "id": 15, | ||||
|         "isShow": 1, | ||||
|         "name": "用户注册", | ||||
|         "orderIndex": 10000, | ||||
|         "parentId": 14 | ||||
|     },...] | ||||
|      * @param pid 初始父节点id,一般是0 | ||||
|      * @return 返回结果 [{ | ||||
|       label: '一级 1', | ||||
|       children: [{ | ||||
|         label: '二级 1-1', | ||||
|         children: [{ | ||||
|           label: '三级 1-1-1' | ||||
|         }] | ||||
|       }] | ||||
|     } | ||||
|      */ | ||||
|     convertToTreeData(data, pid) { | ||||
|       const result = [] | ||||
|       const root = { | ||||
|         label: '服务列表', | ||||
|         parentId: pid | ||||
|       } | ||||
|       const children = [] | ||||
|       for (let i = 0; i < data.length; i++) { | ||||
|         const item = { label: data[i].serviceId, parentId: 1 } | ||||
|         children.push(item) | ||||
|       } | ||||
|       root.children = children | ||||
|       result.push(root) | ||||
|       return result | ||||
|     }, | ||||
|     // table | ||||
|     loadTable: function() { | ||||
|       this.post('route.limit.list', this.searchFormData, function(resp) { | ||||
|         this.tableData = resp.data | ||||
|       }) | ||||
|     }, | ||||
|     onSearchTable: function() { | ||||
|       this.loadTable() | ||||
|     }, | ||||
|     onTableUpdate: function(row) { | ||||
|       this.limitDialogVisible = true | ||||
|       this.$nextTick(() => { | ||||
|         Object.assign(this.limitDialogFormData, row) | ||||
|       }) | ||||
|     }, | ||||
|     resetForm(formName) { | ||||
|       const frm = this.$refs[formName] | ||||
|       frm && frm.resetFields() | ||||
|     }, | ||||
|     onLimitDialogClose: function() { | ||||
|       this.resetForm('limitDialogFormLeaky') | ||||
|       this.resetForm('limitDialogFormToken') | ||||
|       this.limitDialogVisible = false | ||||
|     }, | ||||
|     infoRender: function(row) { | ||||
|       if (!row.hasRecord) { | ||||
|         return '--' | ||||
|       } | ||||
|       const html = [] | ||||
|       if (row.limitType === 1) { | ||||
|         html.push('每秒可处理请求数:' + row.execCountPerSecond) | ||||
|         html.push('subCode:' + row.limitCode) | ||||
|         html.push('subMsg:' + row.limitMsg) | ||||
|       } else if (row.limitType === 2) { | ||||
|         html.push('令牌桶容量:' + row.tokenBucketCount) | ||||
|       } | ||||
|       return html.join(',') | ||||
|     }, | ||||
|     onLimitDialogSave: function() { | ||||
|       this.doValidate(function() { | ||||
|         this.limitDialogFormData.serviceId = this.serviceId | ||||
|         this.post('route.limit.update', this.limitDialogFormData, function(resp) { | ||||
|           this.limitDialogVisible = false | ||||
|           this.loadTable() | ||||
|         }) | ||||
|       }) | ||||
|     }, | ||||
|     doValidate: function(callback) { | ||||
|       const that = this | ||||
|       if (this.limitDialogFormData.limitStatus === 0) { | ||||
|         callback.call(this) | ||||
|         return | ||||
|       } | ||||
|       if (this.limitDialogFormData.limitType === 1) { | ||||
|         this.$refs['limitDialogFormLeaky'].validate((valid) => { | ||||
|           if (valid) { | ||||
|             callback.call(that) | ||||
|           } | ||||
|         }) | ||||
|       } else { | ||||
|         this.$refs['limitDialogFormToken'].validate((valid) => { | ||||
|           if (valid) { | ||||
|             callback.call(that) | ||||
|           } | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     onLimitTypeTipClick: function() { | ||||
|       const leakyRemark = '漏桶策略:每秒处理固定数量的请求,超出请求返回错误信息。' | ||||
|       const tokenRemark = '令牌桶策略:每秒放置固定数量的令牌数,每个请求进来后先去拿令牌,拿到了令牌才能继续,拿不到则等候令牌重新生成了再拿。' | ||||
|       const content = leakyRemark + '<br>' + tokenRemark | ||||
|       this.$alert(content, '限流策略', { | ||||
|         dangerouslyUseHTMLString: true | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,136 @@ | ||||
| <template> | ||||
|   <div class="app-container"> | ||||
|     <el-form :inline="true" :model="searchFormData" class="demo-form-inline"> | ||||
|       <el-form-item label="serviceId"> | ||||
|         <el-input v-model="searchFormData.serviceId" :clearable="true" placeholder="serviceId" size="mini" style="width: 250px;" /> | ||||
|       </el-form-item> | ||||
|       <el-form-item> | ||||
|         <el-button type="primary" prefix-icon="el-icon-search" size="mini" @click="onSearchTable">查询</el-button> | ||||
|       </el-form-item> | ||||
|     </el-form> | ||||
|     <el-table | ||||
|       :data="tableData" | ||||
|       style="width: 100%;margin-bottom: 20px;" | ||||
|       border | ||||
|       row-key="id" | ||||
|     > | ||||
|       <el-table-column | ||||
|         prop="name" | ||||
|         label="服务名称(serviceId)" | ||||
|         width="200" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         prop="instanceId" | ||||
|         label="instanceId" | ||||
|         width="250" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         prop="ipAddr" | ||||
|         label="IP地址" | ||||
|         width="150" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         prop="serverPort" | ||||
|         label="端口号" | ||||
|         width="100" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         prop="status" | ||||
|         label="服务状态" | ||||
|         width="100" | ||||
|       > | ||||
|         <template slot-scope="scope"> | ||||
|           <el-tag v-if="scope.row.parentId > 0 && scope.row.status === 'UP'" type="success">已上线</el-tag> | ||||
|           <el-tag v-if="scope.row.parentId > 0 && scope.row.status === 'OUT_OF_SERVICE'" type="danger">已下线</el-tag> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|       <el-table-column | ||||
|         prop="updateTime" | ||||
|         label="最后更新时间" | ||||
|         width="160" | ||||
|       /> | ||||
|       <el-table-column | ||||
|         label="操作" | ||||
|         width="100" | ||||
|       > | ||||
|         <template slot-scope="scope"> | ||||
|           <el-button v-if="scope.row.parentId > 0 && scope.row.status === 'UP'" type="text" size="mini" @click="onOffline(scope.row)">下线</el-button> | ||||
|           <el-button v-if="scope.row.parentId > 0 && scope.row.status === 'OUT_OF_SERVICE'" type="text" size="mini" @click="onOnline(scope.row)">上线</el-button> | ||||
|         </template> | ||||
|       </el-table-column> | ||||
|     </el-table> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       searchFormData: { | ||||
|         serviceId: '' | ||||
|       }, | ||||
|       tableData: [] | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     this.loadTable() | ||||
|   }, | ||||
|   methods: { | ||||
|     loadTable: function() { | ||||
|       this.post('service.instance.list', this.searchFormData, function(resp) { | ||||
|         this.tableData = this.buildTreeData(resp.data) | ||||
|       }) | ||||
|     }, | ||||
|     buildTreeData: function(data) { | ||||
|       data.forEach(ele => { | ||||
|         const parentId = ele.parentId | ||||
|         if (parentId === 0) { | ||||
|           // 是根元素 ,不做任何操作,如果是正常的for-i循环,可以直接continue. | ||||
|         } else { | ||||
|           // 如果ele是子元素的话 ,把ele扔到他的父亲的child数组中. | ||||
|           data.forEach(d => { | ||||
|             if (d.id === parentId) { | ||||
|               let childArray = d.child | ||||
|               if (!childArray) { | ||||
|                 childArray = [] | ||||
|               } | ||||
|               childArray.push(ele) | ||||
|               d.children = childArray | ||||
|             } | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|       // 去除重复元素 | ||||
|       data = data.filter(ele => ele.parentId === 0) | ||||
|       return data | ||||
|     }, | ||||
|     onSearchTable: function() { | ||||
|       this.loadTable() | ||||
|     }, | ||||
|     onOffline: function(row) { | ||||
|       this.confirm('确定要下线【' + row.name + '】吗?', function(done) { | ||||
|         const params = { | ||||
|           serviceId: row.name, | ||||
|           instanceId: row.instanceId | ||||
|         } | ||||
|         this.post('service.instance.offline', params, function() { | ||||
|           this.tip('下线成功') | ||||
|           done() | ||||
|         }) | ||||
|       }) | ||||
|     }, | ||||
|     onOnline: function(row) { | ||||
|       this.confirm('确定要上线【' + row.name + '】吗?', function(done) { | ||||
|         const params = { | ||||
|           serviceId: row.name, | ||||
|           instanceId: row.instanceId | ||||
|         } | ||||
|         this.post('service.instance.online', params, function() { | ||||
|           this.tip('上线成功') | ||||
|           done() | ||||
|         }) | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,314 @@ | ||||
| <template> | ||||
|   <div class="app-container"> | ||||
|     <el-container> | ||||
|       <el-aside style="min-height: 300px;width: 200px;"> | ||||
|         <el-input v-model="filterText" prefix-icon="el-icon-search" placeholder="搜索服务..." style="margin-bottom:20px;" size="mini" clearable /> | ||||
|         <el-tree | ||||
|           ref="tree2" | ||||
|           :data="treeData" | ||||
|           :props="defaultProps" | ||||
|           :filter-node-method="filterNode" | ||||
|           :highlight-current="true" | ||||
|           :expand-on-click-node="false" | ||||
|           empty-text="无数据" | ||||
|           node-key="id" | ||||
|           class="filter-tree" | ||||
|           default-expand-all | ||||
|           @node-click="onNodeClick" | ||||
|         > | ||||
|           <span slot-scope="{ node, data }" class="custom-tree-node"> | ||||
|             <span v-if="data.label.length < 15">{{ data.label }}</span> | ||||
|             <span v-else> | ||||
|               <el-tooltip :content="data.label" class="item" effect="light" placement="right"> | ||||
|                 <span>{{ data.label.substring(0, 15) + '...' }}</span> | ||||
|               </el-tooltip> | ||||
|             </span> | ||||
|           </span> | ||||
|         </el-tree> | ||||
|       </el-aside> | ||||
|       <el-main style="padding-top:0"> | ||||
|         <el-form :inline="true" :model="searchFormData" class="demo-form-inline"> | ||||
|           <el-form-item label="路由名称"> | ||||
|             <el-input v-model="searchFormData.id" placeholder="输入接口名或版本号" size="mini" /> | ||||
|           </el-form-item> | ||||
|           <el-form-item> | ||||
|             <el-button type="primary" icon="el-icon-search" size="mini" @click="onSearchTable">查询</el-button> | ||||
|           </el-form-item> | ||||
|         </el-form> | ||||
|         <el-table | ||||
|           :data="tableData" | ||||
|           border | ||||
|           max-height="500" | ||||
|         > | ||||
|           <el-table-column | ||||
|             prop="name" | ||||
|             label="接口名 (版本号)" | ||||
|             width="200" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               {{ scope.row.name + ' (' + scope.row.version + ')' }} | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             prop="uri" | ||||
|             label="LoadBalance" | ||||
|             width="350" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               {{ scope.row.uri + scope.row.path }} | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             prop="roles" | ||||
|             label="访问权限" | ||||
|             width="100" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               <span v-html="roleRender(scope.row)"></span> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             prop="ignoreValidate" | ||||
|             label="忽略验证" | ||||
|             width="80" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               <span v-if="scope.row.ignoreValidate === 1" style="color:#67C23A">是</span> | ||||
|               <span v-if="scope.row.ignoreValidate === 0" style="color:#909399">否</span> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             prop="mergeResult" | ||||
|             label="合并结果" | ||||
|             width="80" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               <span v-if="scope.row.mergeResult === 1" style="color:#67C23A">合并</span> | ||||
|               <span v-if="scope.row.mergeResult === 0" style="color:#E6A23C">不合并</span> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             prop="status" | ||||
|             label="状态" | ||||
|             width="80" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               <span v-if="scope.row.status === 0" style="color:#909399">待审核</span> | ||||
|               <span v-if="scope.row.status === 1" style="color:#67C23A">已启用</span> | ||||
|               <span v-if="scope.row.status === 2" style="color:#F56C6C">已禁用</span> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|           <el-table-column | ||||
|             label="操作" | ||||
|             fixed="right" | ||||
|             width="100" | ||||
|           > | ||||
|             <template slot-scope="scope"> | ||||
|               <el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button> | ||||
|               <el-button v-if="scope.row.permission" type="text" size="mini" @click="onTableAuth(scope.row)">授权</el-button> | ||||
|             </template> | ||||
|           </el-table-column> | ||||
|         </el-table> | ||||
|         <!-- route dialog --> | ||||
|         <el-dialog title="修改路由" :visible.sync="routeDialogVisible" :close-on-click-modal="false"> | ||||
|           <el-form :model="routeDialogFormData"> | ||||
|             <el-form-item label="id" :label-width="formLabelWidth"> | ||||
|               <el-input v-model="routeDialogFormData.id" readonly="readonly" /> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="uri" :label-width="formLabelWidth"> | ||||
|               <el-input v-model="routeDialogFormData.uri" /> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="path" :label-width="formLabelWidth"> | ||||
|               <el-input v-model="routeDialogFormData.path" /> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="状态" :label-width="formLabelWidth"> | ||||
|               <el-radio-group v-model="routeDialogFormData.status"> | ||||
|                 <el-radio :label="1" name="status">启用</el-radio> | ||||
|                 <el-radio :label="2" name="status" style="color:#F56C6C">禁用</el-radio> | ||||
|               </el-radio-group> | ||||
|             </el-form-item> | ||||
|           </el-form> | ||||
|           <div slot="footer" class="dialog-footer"> | ||||
|             <el-button @click="routeDialogVisible = false">取 消</el-button> | ||||
|             <el-button type="primary" @click="onRouteDialogSave">保 存</el-button> | ||||
|           </div> | ||||
|         </el-dialog> | ||||
|         <!-- auth dialog --> | ||||
|         <el-dialog title="修改路由" :visible.sync="authDialogVisible" :close-on-click-modal="false"> | ||||
|           <el-form :model="authDialogFormData"> | ||||
|             <el-form-item label="id" :label-width="formLabelWidth"> | ||||
|               <el-input v-model="authDialogFormData.routeId" readonly="readonly" /> | ||||
|             </el-form-item> | ||||
|             <el-form-item label="角色" :label-width="formLabelWidth"> | ||||
|               <el-checkbox-group v-model="authDialogFormData.roleCode"> | ||||
|                 <el-checkbox v-for="item in roles" :key="item.roleCode" :label="item.roleCode">{{ item.description }}</el-checkbox> | ||||
|               </el-checkbox-group> | ||||
|             </el-form-item> | ||||
|           </el-form> | ||||
|           <div slot="footer" class="dialog-footer"> | ||||
|             <el-button @click="authDialogVisible = false">取 消</el-button> | ||||
|             <el-button type="primary" @click="onAuthDialogSave">保 存</el-button> | ||||
|           </div> | ||||
|         </el-dialog> | ||||
|       </el-main> | ||||
|     </el-container> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       filterText: '', | ||||
|       treeData: [], | ||||
|       tableData: [], | ||||
|       serviceId: '', | ||||
|       searchFormData: {}, | ||||
|       defaultProps: { | ||||
|         children: 'children', | ||||
|         label: 'label' | ||||
|       }, | ||||
|       // dialog | ||||
|       routeDialogFormData: { | ||||
|         status: 1 | ||||
|       }, | ||||
|       formLabelWidth: '120px', | ||||
|       routeDialogVisible: false, | ||||
| 
 | ||||
|       roles: [], | ||||
|       authDialogFormData: { | ||||
|         routeId: '', | ||||
|         roleCode: [] | ||||
|       }, | ||||
|       authDialogVisible: false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     filterText(val) { | ||||
|       this.$refs.tree2.filter(val) | ||||
|     } | ||||
|   }, | ||||
|   created() { | ||||
|     this.loadTree() | ||||
|     this.loadRouteRole() | ||||
|   }, | ||||
|   methods: { | ||||
|     // 加载树 | ||||
|     loadTree: function() { | ||||
|       this.post('service.list', {}, function(resp) { | ||||
|         const respData = resp.data | ||||
|         this.treeData = this.convertToTreeData(respData, 0) | ||||
|       }) | ||||
|     }, | ||||
|     // 树搜索 | ||||
|     filterNode(value, data) { | ||||
|       if (!value) return true | ||||
|       return data.label.indexOf(value) !== -1 | ||||
|     }, | ||||
|     // 树点击事件 | ||||
|     onNodeClick(data, node, tree) { | ||||
|       if (data.parentId) { | ||||
|         this.serviceId = data.label | ||||
|         this.searchFormData.serviceId = this.serviceId | ||||
|         this.loadTable() | ||||
|       } | ||||
|     }, | ||||
|     /** | ||||
|      * 数组转成树状结构 | ||||
|      * @param data 数据结构 [{ | ||||
|           "_parentId": 14, | ||||
|           "gmtCreate": "2019-01-15 09:44:38", | ||||
|           "gmtUpdate": "2019-01-15 09:44:38", | ||||
|           "id": 15, | ||||
|           "isShow": 1, | ||||
|           "name": "用户注册", | ||||
|           "orderIndex": 10000, | ||||
|           "parentId": 14 | ||||
|       },...] | ||||
|      * @param pid 初始父节点id,一般是0 | ||||
|      * @return 返回结果 [{ | ||||
|         label: '一级 1', | ||||
|         children: [{ | ||||
|           label: '二级 1-1', | ||||
|           children: [{ | ||||
|             label: '三级 1-1-1' | ||||
|           }] | ||||
|         }] | ||||
|       } | ||||
|      */ | ||||
|     convertToTreeData(data, pid) { | ||||
|       const result = [] | ||||
|       const root = { | ||||
|         label: '服务列表', | ||||
|         parentId: pid | ||||
|       } | ||||
|       const children = [] | ||||
|       for (let i = 0; i < data.length; i++) { | ||||
|         const item = { label: data[i].serviceId, parentId: 1 } | ||||
|         children.push(item) | ||||
|       } | ||||
|       root.children = children | ||||
|       result.push(root) | ||||
|       return result | ||||
|     }, | ||||
|     // table | ||||
|     loadTable: function() { | ||||
|       this.post('route.list', this.searchFormData, function(resp) { | ||||
|         this.tableData = resp.data | ||||
|       }) | ||||
|     }, | ||||
|     onSearchTable: function() { | ||||
|       this.loadTable() | ||||
|     }, | ||||
|     onTableUpdate: function(row) { | ||||
|       Object.assign(this.routeDialogFormData, row) | ||||
|       this.routeDialogVisible = true | ||||
|     }, | ||||
|     onTableAuth: function(row) { | ||||
|       this.authDialogFormData.routeId = row.id | ||||
|       const searchData = { id: row.id, serviceId: this.serviceId } | ||||
|       this.post('route.role.get', searchData, function(resp) { | ||||
|         const roleList = resp.data | ||||
|         const roleCodes = [] | ||||
|         for (let i = 0; i < roleList.length; i++) { | ||||
|           roleCodes.push(roleList[i].roleCode) | ||||
|         } | ||||
|         this.authDialogFormData.roleCode = roleCodes | ||||
|         this.authDialogVisible = true | ||||
|       }) | ||||
|     }, | ||||
|     loadRouteRole: function() { | ||||
|       if (this.roles.length === 0) { | ||||
|         this.post('role.listall', {}, function(resp) { | ||||
|           this.roles = resp.data | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     roleRender: function(row) { | ||||
|       if (!row.permission) { | ||||
|         return '(公开)' | ||||
|       } | ||||
|       const html = [] | ||||
|       const roles = row.roles | ||||
|       for (let i = 0; i < roles.length; i++) { | ||||
|         html.push(roles[i].description) | ||||
|       } | ||||
|       return html.length > 0 ? html.join(', ') : '<span class="x-red">未授权</span>' | ||||
|     }, | ||||
|     onRouteDialogSave: function() { | ||||
|       this.routeDialogFormData.serviceId = this.serviceId | ||||
|       this.post('route.update', this.routeDialogFormData, function() { | ||||
|         this.routeDialogVisible = false | ||||
|         this.loadTable() | ||||
|       }) | ||||
|     }, | ||||
|     onAuthDialogSave: function() { | ||||
|       this.post('route.role.update', this.authDialogFormData, function() { | ||||
|         console.log(this.authDialogFormData) | ||||
|         this.authDialogVisible = false | ||||
|         this.loadTable() | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -0,0 +1,5 @@ | ||||
| module.exports = { | ||||
|   env: { | ||||
|     jest: true | ||||
|   } | ||||
| } | ||||
| @ -0,0 +1,98 @@ | ||||
| import { mount, createLocalVue } from '@vue/test-utils' | ||||
| import VueRouter from 'vue-router' | ||||
| import ElementUI from 'element-ui' | ||||
| import Breadcrumb from '@/components/Breadcrumb/index.vue' | ||||
| 
 | ||||
| const localVue = createLocalVue() | ||||
| localVue.use(VueRouter) | ||||
| localVue.use(ElementUI) | ||||
| 
 | ||||
| const routes = [ | ||||
|   { | ||||
|     path: '/', | ||||
|     name: 'home', | ||||
|     children: [{ | ||||
|       path: 'dashboard', | ||||
|       name: 'dashboard' | ||||
|     }] | ||||
|   }, | ||||
|   { | ||||
|     path: '/menu', | ||||
|     name: 'menu', | ||||
|     children: [{ | ||||
|       path: 'menu1', | ||||
|       name: 'menu1', | ||||
|       meta: { title: 'menu1' }, | ||||
|       children: [{ | ||||
|         path: 'menu1-1', | ||||
|         name: 'menu1-1', | ||||
|         meta: { title: 'menu1-1' } | ||||
|       }, | ||||
|       { | ||||
|         path: 'menu1-2', | ||||
|         name: 'menu1-2', | ||||
|         redirect: 'noredirect', | ||||
|         meta: { title: 'menu1-2' }, | ||||
|         children: [{ | ||||
|           path: 'menu1-2-1', | ||||
|           name: 'menu1-2-1', | ||||
|           meta: { title: 'menu1-2-1' } | ||||
|         }, | ||||
|         { | ||||
|           path: 'menu1-2-2', | ||||
|           name: 'menu1-2-2' | ||||
|         }] | ||||
|       }] | ||||
|     }] | ||||
|   }] | ||||
| 
 | ||||
| const router = new VueRouter({ | ||||
|   routes | ||||
| }) | ||||
| 
 | ||||
| describe('Breadcrumb.vue', () => { | ||||
|   const wrapper = mount(Breadcrumb, { | ||||
|     localVue, | ||||
|     router | ||||
|   }) | ||||
|   it('dashboard', () => { | ||||
|     router.push('/dashboard') | ||||
|     const len = wrapper.findAll('.el-breadcrumb__inner').length | ||||
|     expect(len).toBe(1) | ||||
|   }) | ||||
|   it('normal route', () => { | ||||
|     router.push('/menu/menu1') | ||||
|     const len = wrapper.findAll('.el-breadcrumb__inner').length | ||||
|     expect(len).toBe(2) | ||||
|   }) | ||||
|   it('nested route', () => { | ||||
|     router.push('/menu/menu1/menu1-2/menu1-2-1') | ||||
|     const len = wrapper.findAll('.el-breadcrumb__inner').length | ||||
|     expect(len).toBe(4) | ||||
|   }) | ||||
|   it('no meta.title', () => { | ||||
|     router.push('/menu/menu1/menu1-2/menu1-2-2') | ||||
|     const len = wrapper.findAll('.el-breadcrumb__inner').length | ||||
|     expect(len).toBe(3) | ||||
|   }) | ||||
|   // it('click link', () => {
 | ||||
|   //   router.push('/menu/menu1/menu1-2/menu1-2-2')
 | ||||
|   //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
 | ||||
|   //   const second = breadcrumbArray.at(1)
 | ||||
|   //   console.log(breadcrumbArray)
 | ||||
|   //   const href = second.find('a').attributes().href
 | ||||
|   //   expect(href).toBe('#/menu/menu1')
 | ||||
|   // })
 | ||||
|   // it('noRedirect', () => {
 | ||||
|   //   router.push('/menu/menu1/menu1-2/menu1-2-1')
 | ||||
|   //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
 | ||||
|   //   const redirectBreadcrumb = breadcrumbArray.at(2)
 | ||||
|   //   expect(redirectBreadcrumb.contains('a')).toBe(false)
 | ||||
|   // })
 | ||||
|   it('last breadcrumb', () => { | ||||
|     router.push('/menu/menu1/menu1-2/menu1-2-1') | ||||
|     const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') | ||||
|     const redirectBreadcrumb = breadcrumbArray.at(3) | ||||
|     expect(redirectBreadcrumb.contains('a')).toBe(false) | ||||
|   }) | ||||
| }) | ||||
| @ -0,0 +1,18 @@ | ||||
| import { shallowMount } from '@vue/test-utils' | ||||
| import Hamburger from '@/components/Hamburger/index.vue' | ||||
| describe('Hamburger.vue', () => { | ||||
|   it('toggle click', () => { | ||||
|     const wrapper = shallowMount(Hamburger) | ||||
|     const mockFn = jest.fn() | ||||
|     wrapper.vm.$on('toggleClick', mockFn) | ||||
|     wrapper.find('.hamburger').trigger('click') | ||||
|     expect(mockFn).toBeCalled() | ||||
|   }) | ||||
|   it('prop isActive', () => { | ||||
|     const wrapper = shallowMount(Hamburger) | ||||
|     wrapper.setProps({ isActive: true }) | ||||
|     expect(wrapper.contains('.is-active')).toBe(true) | ||||
|     wrapper.setProps({ isActive: false }) | ||||
|     expect(wrapper.contains('.is-active')).toBe(false) | ||||
|   }) | ||||
| }) | ||||
| @ -0,0 +1,22 @@ | ||||
| import { shallowMount } from '@vue/test-utils' | ||||
| import SvgIcon from '@/components/SvgIcon/index.vue' | ||||
| describe('SvgIcon.vue', () => { | ||||
|   it('iconClass', () => { | ||||
|     const wrapper = shallowMount(SvgIcon, { | ||||
|       propsData: { | ||||
|         iconClass: 'test' | ||||
|       } | ||||
|     }) | ||||
|     expect(wrapper.find('use').attributes().href).toBe('#icon-test') | ||||
|   }) | ||||
|   it('className', () => { | ||||
|     const wrapper = shallowMount(SvgIcon, { | ||||
|       propsData: { | ||||
|         iconClass: 'test' | ||||
|       } | ||||
|     }) | ||||
|     expect(wrapper.classes().length).toBe(1) | ||||
|     wrapper.setProps({ className: 'test' }) | ||||
|     expect(wrapper.classes().includes('test')).toBe(true) | ||||
|   }) | ||||
| }) | ||||
| @ -0,0 +1,30 @@ | ||||
| import { formatTime } from '@/utils/index.js' | ||||
| 
 | ||||
| describe('Utils:formatTime', () => { | ||||
|   const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
 | ||||
|   const retrofit = 5 * 1000 | ||||
| 
 | ||||
|   it('ten digits timestamp', () => { | ||||
|     expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') | ||||
|   }) | ||||
|   it('test now', () => { | ||||
|     expect(formatTime(+new Date() - 1)).toBe('刚刚') | ||||
|   }) | ||||
|   it('less two minute', () => { | ||||
|     expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') | ||||
|   }) | ||||
|   it('less two hour', () => { | ||||
|     expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') | ||||
|   }) | ||||
|   it('less one day', () => { | ||||
|     expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') | ||||
|   }) | ||||
|   it('more than one day', () => { | ||||
|     expect(formatTime(d)).toBe('7月13日17时54分') | ||||
|   }) | ||||
|   it('format', () => { | ||||
|     expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') | ||||
|     expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') | ||||
|     expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') | ||||
|   }) | ||||
| }) | ||||
| @ -0,0 +1,28 @@ | ||||
| import { parseTime } from '@/utils/index.js' | ||||
| 
 | ||||
| describe('Utils:parseTime', () => { | ||||
|   const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
 | ||||
|   it('timestamp', () => { | ||||
|     expect(parseTime(d)).toBe('2018-07-13 17:54:01') | ||||
|   }) | ||||
|   it('ten digits timestamp', () => { | ||||
|     expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') | ||||
|   }) | ||||
|   it('new Date', () => { | ||||
|     expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') | ||||
|   }) | ||||
|   it('format', () => { | ||||
|     expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') | ||||
|     expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') | ||||
|     expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') | ||||
|   }) | ||||
|   it('get the day of the week', () => { | ||||
|     expect(parseTime(d, '{a}')).toBe('五') // 星期五
 | ||||
|   }) | ||||
|   it('get the day of the week', () => { | ||||
|     expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
 | ||||
|   }) | ||||
|   it('empty argument', () => { | ||||
|     expect(parseTime()).toBeNull() | ||||
|   }) | ||||
| }) | ||||
| @ -0,0 +1,17 @@ | ||||
| import { validUsername, isExternal } from '@/utils/validate.js' | ||||
| 
 | ||||
| describe('Utils:validate', () => { | ||||
|   it('validUsername', () => { | ||||
|     expect(validUsername('admin')).toBe(true) | ||||
|     expect(validUsername('editor')).toBe(true) | ||||
|     expect(validUsername('xxxx')).toBe(false) | ||||
|   }) | ||||
|   it('isExternal', () => { | ||||
|     expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) | ||||
|     expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) | ||||
|     expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) | ||||
|     expect(isExternal('/dashboard')).toBe(false) | ||||
|     expect(isExternal('./dashboard')).toBe(false) | ||||
|     expect(isExternal('dashboard')).toBe(false) | ||||
|   }) | ||||
| }) | ||||
| @ -0,0 +1,133 @@ | ||||
| 'use strict' | ||||
| const path = require('path') | ||||
| const defaultSettings = require('./src/settings.js') | ||||
| 
 | ||||
| function resolve(dir) { | ||||
|   return path.join(__dirname, dir) | ||||
| } | ||||
| 
 | ||||
| const name = defaultSettings.title || 'vue Admin Template' // page title
 | ||||
| const port = 9528 // dev port
 | ||||
| 
 | ||||
| // All configuration item explanations can be find in https://cli.vuejs.org/config/
 | ||||
| module.exports = { | ||||
|   /** | ||||
|    * You will need to set publicPath if you plan to deploy your site under a sub path, | ||||
|    * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
 | ||||
|    * then publicPath should be set to "/bar/". | ||||
|    * In most cases please use '/' !!! | ||||
|    * Detail: https://cli.vuejs.org/config/#publicpath
 | ||||
|    */ | ||||
|   publicPath: '/', | ||||
|   outputDir: 'dist', | ||||
|   assetsDir: 'static', | ||||
|   lintOnSave: process.env.NODE_ENV === 'development', | ||||
|   productionSourceMap: false, | ||||
|   devServer: { | ||||
|     port: port, | ||||
|     open: true, | ||||
|     overlay: { | ||||
|       warnings: false, | ||||
|       errors: true | ||||
|     }, | ||||
|     proxy: { | ||||
|       // change xxx-api/login => mock/login
 | ||||
|       // detail: https://cli.vuejs.org/config/#devserver-proxy
 | ||||
|       [process.env.VUE_APP_BASE_API]: { | ||||
|         target: `http://localhost:${port}/mock`, | ||||
|         changeOrigin: true, | ||||
|         pathRewrite: { | ||||
|           ['^' + process.env.VUE_APP_BASE_API]: '' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     after: require('./mock/mock-server.js') | ||||
|   }, | ||||
|   configureWebpack: { | ||||
|     // provide the app's title in webpack's name field, so that
 | ||||
|     // it can be accessed in index.html to inject the correct title.
 | ||||
|     name: name, | ||||
|     resolve: { | ||||
|       alias: { | ||||
|         '@': resolve('src') | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   chainWebpack(config) { | ||||
|     config.plugins.delete('preload') // TODO: need test
 | ||||
|     config.plugins.delete('prefetch') // TODO: need test
 | ||||
| 
 | ||||
|     // set svg-sprite-loader
 | ||||
|     config.module | ||||
|       .rule('svg') | ||||
|       .exclude.add(resolve('src/icons')) | ||||
|       .end() | ||||
|     config.module | ||||
|       .rule('icons') | ||||
|       .test(/\.svg$/) | ||||
|       .include.add(resolve('src/icons')) | ||||
|       .end() | ||||
|       .use('svg-sprite-loader') | ||||
|       .loader('svg-sprite-loader') | ||||
|       .options({ | ||||
|         symbolId: 'icon-[name]' | ||||
|       }) | ||||
|       .end() | ||||
| 
 | ||||
|     // set preserveWhitespace
 | ||||
|     config.module | ||||
|       .rule('vue') | ||||
|       .use('vue-loader') | ||||
|       .loader('vue-loader') | ||||
|       .tap(options => { | ||||
|         options.compilerOptions.preserveWhitespace = true | ||||
|         return options | ||||
|       }) | ||||
|       .end() | ||||
| 
 | ||||
|     config | ||||
|     // https://webpack.js.org/configuration/devtool/#development
 | ||||
|       .when(process.env.NODE_ENV === 'development', | ||||
|         config => config.devtool('cheap-source-map') | ||||
|       ) | ||||
| 
 | ||||
|     config | ||||
|       .when(process.env.NODE_ENV !== 'development', | ||||
|         config => { | ||||
|           config | ||||
|             .plugin('ScriptExtHtmlWebpackPlugin') | ||||
|             .after('html') | ||||
|             .use('script-ext-html-webpack-plugin', [{ | ||||
|             // `runtime` must same as runtimeChunk name. default is `runtime`
 | ||||
|               inline: /runtime\..*\.js$/ | ||||
|             }]) | ||||
|             .end() | ||||
|           config | ||||
|             .optimization.splitChunks({ | ||||
|               chunks: 'all', | ||||
|               cacheGroups: { | ||||
|                 libs: { | ||||
|                   name: 'chunk-libs', | ||||
|                   test: /[\\/]node_modules[\\/]/, | ||||
|                   priority: 10, | ||||
|                   chunks: 'initial' // only package third parties that are initially dependent
 | ||||
|                 }, | ||||
|                 elementUI: { | ||||
|                   name: 'chunk-elementUI', // split elementUI into a single package
 | ||||
|                   priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
 | ||||
|                   test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
 | ||||
|                 }, | ||||
|                 commons: { | ||||
|                   name: 'chunk-commons', | ||||
|                   test: resolve('src/components'), // can customize your rules
 | ||||
|                   minChunks: 3, //  minimum common number
 | ||||
|                   priority: 5, | ||||
|                   reuseExistingChunk: true | ||||
|                 } | ||||
|               } | ||||
|             }) | ||||
|           config.optimization.runtimeChunk('single') | ||||
|         } | ||||
|       ) | ||||
|   } | ||||
| } | ||||