@ -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') |
||||||
|
} |
||||||
|
) |
||||||
|
} |
||||||
|
} |