commit 952ae1e8f34b2967d3317e16731b782f00702835 Author: 郑杰 Date: Sat Dec 22 18:54:09 2018 +0800 eladmin 1.0 版本发布 diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..26c173e --- /dev/null +++ b/.babelrc @@ -0,0 +1,17 @@ +{ + "presets": [ + ["env", { + "modules": false, + "targets": { + "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] + } + }], + "stage-2" + ], + "plugins":["transform-vue-jsx", "transform-runtime"], + "env": { + "development":{ + "plugins": ["dynamic-import-node"] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ea6e20f --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e3a4037 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +build/*.js +config/*.js +src/assets diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..0e5c28a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,196 @@ +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/name-property-casing": ["error", "PascalCase"], + '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': [2, 'allow-null'], + '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': 2, + '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'] + } +} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78a0ead --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/.postcssrc.js b/.postcssrc.js new file mode 100644 index 0000000..eee3e92 --- /dev/null +++ b/.postcssrc.js @@ -0,0 +1,10 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + "plugins": { + "postcss-import": {}, + "postcss-url": {}, + // to edit target browsers: use "browserslist" field in package.json + "autoprefixer": {} + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..16574d9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: stable +script: npm run test +notifications: + email: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..91e3c25 --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "{}" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright 2018 Elune + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..684de04 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# eladmin-qt + +eladmin 前端源码,后端源码:[https://github.com/elunez/eladmin](https://github.com/elunez/eladmin) + +#### 初始模板 +前端模板基于花衩裤:[https://github.com/PanJiaChen/vue-admin-template](https://github.com/PanJiaChen/vue-admin-template) + +## Build Setup +``` bash +# Clone project +git clone https://github.com/elunez/eladmin-qd + +# Install dependencies +npm install + +# Serve with hot reload at localhost:9528 +npm run dev + +# Build for production with minification +npm run build + +# Build for production and view the bundle analyzer report +npm run build --report +``` \ No newline at end of file diff --git a/build/build.js b/build/build.js new file mode 100644 index 0000000..8c6cebd --- /dev/null +++ b/build/build.js @@ -0,0 +1,45 @@ +'use strict' +require('./check-versions')() + +process.env.NODE_ENV = 'production' + +const ora = require('ora') +const rm = require('rimraf') +const path = require('path') +const chalk = require('chalk') +const webpack = require('webpack') +const config = require('../config') +const webpackConfig = require('./webpack.prod.conf') + +const spinner = ora('building for production...') +spinner.start() + +rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { + if (err) throw err + webpack(webpackConfig, (err, stats) => { + spinner.stop() + if (err) throw err + process.stdout.write( + stats.toString({ + colors: true, + modules: false, + children: false, + chunks: false, + chunkModules: false + }) + '\n\n' + ) + + if (stats.hasErrors()) { + console.log(chalk.red(' Build failed with errors.\n')) + process.exit(1) + } + + console.log(chalk.cyan(' Build complete.\n')) + console.log( + chalk.yellow( + ' Tip: built files are meant to be served over an HTTP server.\n' + + " Opening index.html over file:// won't work.\n" + ) + ) + }) +}) diff --git a/build/check-versions.js b/build/check-versions.js new file mode 100644 index 0000000..c5c29e9 --- /dev/null +++ b/build/check-versions.js @@ -0,0 +1,64 @@ +'use strict' +const chalk = require('chalk') +const semver = require('semver') +const packageConfig = require('../package.json') +const shell = require('shelljs') + +function exec(cmd) { + return require('child_process') + .execSync(cmd) + .toString() + .trim() +} + +const versionRequirements = [ + { + name: 'node', + currentVersion: semver.clean(process.version), + versionRequirement: packageConfig.engines.node + } +] + +if (shell.which('npm')) { + versionRequirements.push({ + name: 'npm', + currentVersion: exec('npm --version'), + versionRequirement: packageConfig.engines.npm + }) +} + +module.exports = function() { + const warnings = [] + + for (let i = 0; i < versionRequirements.length; i++) { + const mod = versionRequirements[i] + + if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { + warnings.push( + mod.name + + ': ' + + chalk.red(mod.currentVersion) + + ' should be ' + + chalk.green(mod.versionRequirement) + ) + } + } + + if (warnings.length) { + console.log('') + console.log( + chalk.yellow( + 'To use this template, you must update following to modules:' + ) + ) + console.log() + + for (let i = 0; i < warnings.length; i++) { + const warning = warnings[i] + console.log(' ' + warning) + } + + console.log() + process.exit(1) + } +} diff --git a/build/logo.png b/build/logo.png new file mode 100644 index 0000000..f3d2503 Binary files /dev/null and b/build/logo.png differ diff --git a/build/utils.js b/build/utils.js new file mode 100644 index 0000000..c96d093 --- /dev/null +++ b/build/utils.js @@ -0,0 +1,108 @@ +'use strict' +const path = require('path') +const config = require('../config') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const packageConfig = require('../package.json') + +exports.assetsPath = function(_path) { + const assetsSubDirectory = + process.env.NODE_ENV === 'production' + ? config.build.assetsSubDirectory + : config.dev.assetsSubDirectory + + return path.posix.join(assetsSubDirectory, _path) +} + +exports.cssLoaders = function(options) { + options = options || {} + + const cssLoader = { + loader: 'css-loader', + options: { + sourceMap: options.sourceMap + } + } + + const postcssLoader = { + loader: 'postcss-loader', + options: { + sourceMap: options.sourceMap + } + } + + // generate loader string to be used with extract text plugin + function generateLoaders(loader, loaderOptions) { + const loaders = [] + + // Extract CSS when that option is specified + // (which is the case during production build) + if (options.extract) { + loaders.push(MiniCssExtractPlugin.loader) + } else { + loaders.push('vue-style-loader') + } + + loaders.push(cssLoader) + + if (options.usePostCSS) { + loaders.push(postcssLoader) + } + + if (loader) { + loaders.push({ + loader: loader + '-loader', + options: Object.assign({}, loaderOptions, { + sourceMap: options.sourceMap + }) + }) + } + + return loaders + } + // https://vue-loader.vuejs.org/en/configurations/extract-css.html + return { + css: generateLoaders(), + postcss: generateLoaders(), + less: generateLoaders('less'), + sass: generateLoaders('sass', { + indentedSyntax: true + }), + scss: generateLoaders('sass'), + stylus: generateLoaders('stylus'), + styl: generateLoaders('stylus') + } +} + +// Generate loaders for standalone style files (outside of .vue) +exports.styleLoaders = function(options) { + const output = [] + const loaders = exports.cssLoaders(options) + + for (const extension in loaders) { + const loader = loaders[extension] + output.push({ + test: new RegExp('\\.' + extension + '$'), + use: loader + }) + } + + return output +} + +exports.createNotifierCallback = () => { + const notifier = require('node-notifier') + + return (severity, errors) => { + if (severity !== 'error') return + + const error = errors[0] + const filename = error.file && error.file.split('!').pop() + + notifier.notify({ + title: packageConfig.name, + message: severity + ': ' + error.name, + subtitle: filename || '', + icon: path.join(__dirname, 'logo.png') + }) + } +} diff --git a/build/vue-loader.conf.js b/build/vue-loader.conf.js new file mode 100644 index 0000000..5496c93 --- /dev/null +++ b/build/vue-loader.conf.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + //You can set the vue-loader configuration by yourself. +} diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js new file mode 100644 index 0000000..3b946b4 --- /dev/null +++ b/build/webpack.base.conf.js @@ -0,0 +1,107 @@ +'use strict' +const path = require('path') +const utils = require('./utils') +const config = require('../config') +const { VueLoaderPlugin } = require('vue-loader') +const vueLoaderConfig = require('./vue-loader.conf') + +function resolve(dir) { + return path.join(__dirname, '..', dir) +} + +const createLintingRule = () => ({ + test: /\.(js|vue)$/, + loader: 'eslint-loader', + enforce: 'pre', + include: [resolve('src'), resolve('test')], + options: { + formatter: require('eslint-friendly-formatter'), + emitWarning: !config.dev.showEslintErrorsInOverlay + } +}) + +module.exports = { + context: path.resolve(__dirname, '../'), + entry: { + app: './src/main.js' + }, + output: { + path: config.build.assetsRoot, + filename: '[name].js', + publicPath: + process.env.NODE_ENV === 'production' + ? config.build.assetsPublicPath + : config.dev.assetsPublicPath + }, + resolve: { + extensions: ['.js', '.vue', '.json'], + alias: { + '@': resolve('src') + } + }, + module: { + rules: [ + ...(config.dev.useEslint ? [createLintingRule()] : []), + { + test: /\.vue$/, + loader: 'vue-loader', + options: vueLoaderConfig + }, + { + test: /\.js$/, + loader: 'babel-loader?cacheDirectory', + include: [ + resolve('src'), + resolve('test'), + resolve('node_modules/webpack-dev-server/client') + ] + }, + { + test: /\.svg$/, + loader: 'svg-sprite-loader', + include: [resolve('src/icons')], + options: { + symbolId: 'icon-[name]' + } + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'url-loader', + exclude: [resolve('src/icons')], + options: { + limit: 10000, + name: utils.assetsPath('img/[name].[hash:7].[ext]') + } + }, + { + test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('media/[name].[hash:7].[ext]') + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('fonts/[name].[hash:7].[ext]') + } + } + ] + }, + plugins: [new VueLoaderPlugin()], + node: { + // prevent webpack from injecting useless setImmediate polyfill because Vue + // source contains it (although only uses it if it's native). + setImmediate: false, + // prevent webpack from injecting mocks to Node native modules + // that does not make sense for the client + dgram: 'empty', + fs: 'empty', + net: 'empty', + tls: 'empty', + child_process: 'empty' + } +} diff --git a/build/webpack.dev.conf.js b/build/webpack.dev.conf.js new file mode 100644 index 0000000..13525ee --- /dev/null +++ b/build/webpack.dev.conf.js @@ -0,0 +1,98 @@ +'use strict' +const path = require('path') +const utils = require('./utils') +const webpack = require('webpack') +const config = require('../config') +const merge = require('webpack-merge') +const baseWebpackConfig = require('./webpack.base.conf') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') +const portfinder = require('portfinder') + +function resolve(dir) { + return path.join(__dirname, '..', dir) +} + +const HOST = process.env.HOST +const PORT = process.env.PORT && Number(process.env.PORT) + +const devWebpackConfig = merge(baseWebpackConfig, { + mode: 'development', + module: { + rules: utils.styleLoaders({ + sourceMap: config.dev.cssSourceMap, + usePostCSS: true + }) + }, + // cheap-module-eval-source-map is faster for development + devtool: config.dev.devtool, + + // these devServer options should be customized in /config/index.js + devServer: { + clientLogLevel: 'warning', + historyApiFallback: true, + hot: true, + compress: true, + host: HOST || config.dev.host, + port: PORT || config.dev.port, + open: config.dev.autoOpenBrowser, + overlay: config.dev.errorOverlay + ? { warnings: false, errors: true } + : false, + publicPath: config.dev.assetsPublicPath, + proxy: config.dev.proxyTable, + quiet: true, // necessary for FriendlyErrorsPlugin + watchOptions: { + poll: config.dev.poll + } + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': require('../config/dev.env') + }), + new webpack.HotModuleReplacementPlugin(), + // https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: 'index.html', + template: 'index.html', + inject: true, + favicon: resolve('favicon.ico'), + title: 'vue-element-admin', + templateParameters: { + BASE_URL: config.dev.assetsPublicPath + config.dev.assetsSubDirectory, + }, + }), + ] +}) + +module.exports = new Promise((resolve, reject) => { + portfinder.basePort = process.env.PORT || config.dev.port + portfinder.getPort((err, port) => { + if (err) { + reject(err) + } else { + // publish the new Port, necessary for e2e tests + process.env.PORT = port + // add port to devServer config + devWebpackConfig.devServer.port = port + + // Add FriendlyErrorsPlugin + devWebpackConfig.plugins.push( + new FriendlyErrorsPlugin({ + compilationSuccessInfo: { + messages: [ + `Your application is running here: http://${ + devWebpackConfig.devServer.host + }:${port}` + ] + }, + onErrors: config.dev.notifyOnErrors + ? utils.createNotifierCallback() + : undefined + }) + ) + + resolve(devWebpackConfig) + } + }) +}) diff --git a/build/webpack.prod.conf.js b/build/webpack.prod.conf.js new file mode 100644 index 0000000..946a134 --- /dev/null +++ b/build/webpack.prod.conf.js @@ -0,0 +1,188 @@ +'use strict' +const path = require('path') +const utils = require('./utils') +const webpack = require('webpack') +const config = require('../config') +const merge = require('webpack-merge') +const baseWebpackConfig = require('./webpack.base.conf') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') +const UglifyJsPlugin = require('uglifyjs-webpack-plugin') + +function resolve(dir) { + return path.join(__dirname, '..', dir) +} + +const env = require('../config/' + process.env.env_config + '.env') + +// For NamedChunksPlugin +const seen = new Set() +const nameLength = 4 + +const webpackConfig = merge(baseWebpackConfig, { + mode: 'production', + module: { + rules: utils.styleLoaders({ + sourceMap: config.build.productionSourceMap, + extract: true, + usePostCSS: true + }) + }, + devtool: config.build.productionSourceMap ? config.build.devtool : false, + output: { + path: config.build.assetsRoot, + filename: utils.assetsPath('js/[name].[chunkhash:8].js'), + chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].js') + }, + plugins: [ + // http://vuejs.github.io/vue-loader/en/workflow/production.html + new webpack.DefinePlugin({ + 'process.env': env + }), + // extract css into its own file + new MiniCssExtractPlugin({ + filename: utils.assetsPath('css/[name].[contenthash:8].css'), + chunkFilename: utils.assetsPath('css/[name].[contenthash:8].css') + }), + // generate dist index.html with correct asset hash for caching. + // you can customize output by editing /index.html + // see https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: config.build.index, + template: 'index.html', + inject: true, + favicon: resolve('favicon.ico'), + title: 'vue-element-admin', + templateParameters: { + BASE_URL: config.build.assetsPublicPath + config.build.assetsSubDirectory, + }, + minify: { + removeComments: true, + collapseWhitespace: true, + removeAttributeQuotes: true + // more options: + // https://github.com/kangax/html-minifier#options-quick-reference + } + // default sort mode uses toposort which cannot handle cyclic deps + // in certain cases, and in webpack 4, chunk order in HTML doesn't + // matter anyway + }), + new ScriptExtHtmlWebpackPlugin({ + //`runtime` must same as runtimeChunk name. default is `runtime` + inline: /runtime\..*\.js$/ + }), + // keep chunk.id stable when chunk has no name + new webpack.NamedChunksPlugin(chunk => { + if (chunk.name) { + return chunk.name + } + const modules = Array.from(chunk.modulesIterable) + if (modules.length > 1) { + const hash = require('hash-sum') + const joinedHash = hash(modules.map(m => m.id).join('_')) + let len = nameLength + while (seen.has(joinedHash.substr(0, len))) len++ + seen.add(joinedHash.substr(0, len)) + return `chunk-${joinedHash.substr(0, len)}` + } else { + return modules[0].id + } + }), + // keep module.id stable when vender modules does not change + new webpack.HashedModuleIdsPlugin(), + // copy custom static assets + new CopyWebpackPlugin([ + { + from: path.resolve(__dirname, '../static'), + to: config.build.assetsSubDirectory, + ignore: ['.*'] + } + ]) + ], + optimization: { + splitChunks: { + chunks: 'all', + cacheGroups: { + libs: { + name: 'chunk-libs', + test: /[\\/]node_modules[\\/]/, + priority: 10, + chunks: 'initial' // 只打包初始时依赖的第三方 + }, + elementUI: { + name: 'chunk-elementUI', // 单独将 elementUI 拆包 + priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app + test: /[\\/]node_modules[\\/]element-ui[\\/]/ + }, + commons: { + name: 'chunk-commons', + test: resolve('src/components'), // 可自定义拓展你的规则 + minChunks: 3, // 最小公用次数 + priority: 5, + reuseExistingChunk: true + } + } + }, + runtimeChunk: 'single', + minimizer: [ + new UglifyJsPlugin({ + uglifyOptions: { + mangle: { + safari10: true + } + }, + sourceMap: config.build.productionSourceMap, + cache: true, + parallel: true + }), + // Compress extracted CSS. We are using this plugin so that possible + // duplicated CSS from different components can be deduped. + new OptimizeCSSAssetsPlugin() + ] + } +}) + +if (config.build.productionGzip) { + const CompressionWebpackPlugin = require('compression-webpack-plugin') + + webpackConfig.plugins.push( + new CompressionWebpackPlugin({ + asset: '[path].gz[query]', + algorithm: 'gzip', + test: new RegExp( + '\\.(' + config.build.productionGzipExtensions.join('|') + ')$' + ), + threshold: 10240, + minRatio: 0.8 + }) + ) +} + +if (config.build.generateAnalyzerReport || config.build.bundleAnalyzerReport) { + const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin + + if (config.build.bundleAnalyzerReport) { + webpackConfig.plugins.push( + new BundleAnalyzerPlugin({ + analyzerPort: 8080, + generateStatsFile: false + }) + ) + } + + if (config.build.generateAnalyzerReport) { + webpackConfig.plugins.push( + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + reportFilename: 'bundle-report.html', + openAnalyzer: false + }) + ) + } +} + +module.exports = webpackConfig diff --git a/config/dev.env.js b/config/dev.env.js new file mode 100644 index 0000000..d02eab1 --- /dev/null +++ b/config/dev.env.js @@ -0,0 +1,8 @@ +'use strict' +const merge = require('webpack-merge') +const prodEnv = require('./prod.env') + +module.exports = merge(prodEnv, { + NODE_ENV: '"development"', + BASE_API: '"http://localhost"', +}) diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..f70a9d5 --- /dev/null +++ b/config/index.js @@ -0,0 +1,86 @@ +'use strict' +// Template version: 1.2.6 +// see http://vuejs-templates.github.io/webpack for documentation. + +const path = require('path') + +module.exports = { + dev: { + // Paths + assetsSubDirectory: 'static', + assetsPublicPath: '/', + proxyTable: {}, + + // Various Dev Server settings + host: 'localhost', // can be overwritten by process.env.HOST + port: 8013, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined + autoOpenBrowser: true, + errorOverlay: true, + notifyOnErrors: false, + poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- + + // Use Eslint Loader? + // If true, your code will be linted during bundling and + // linting errors and warnings will be shown in the console. + useEslint: true, + // If true, eslint errors and warnings will also be shown in the error overlay + // in the browser. + showEslintErrorsInOverlay: false, + + /** + * Source Maps + */ + + // https://webpack.js.org/configuration/devtool/#development + devtool: 'cheap-source-map', + + // CSS Sourcemaps off by default because relative paths are "buggy" + // with this option, according to the CSS-Loader README + // (https://github.com/webpack/css-loader#sourcemaps) + // In our experience, they generally work as expected, + // just be aware of this issue when enabling this option. + cssSourceMap: false + }, + + build: { + // Template for index.html + index: path.resolve(__dirname, '../dist/index.html'), + + // Paths + assetsRoot: path.resolve(__dirname, '../dist'), + assetsSubDirectory: 'static', + + /** + * You can set by youself according to actual condition + * You will need to set this 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 assetsPublicPath should be set to "/bar/". + * In most cases please use '/' !!! + */ + assetsPublicPath: '/', + + /** + * Source Maps + */ + + productionSourceMap: false, + // https://webpack.js.org/configuration/devtool/#production + devtool: 'source-map', + + // Gzip off by default as many popular static hosts such as + // Surge or Netlify already gzip all static assets for you. + // Before setting to `true`, make sure to: + // npm install --save-dev compression-webpack-plugin + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report || false, + + // `npm run build:prod --generate_report` + generateAnalyzerReport: process.env.npm_config_generate_report || false + } +} diff --git a/config/prod.env.js b/config/prod.env.js new file mode 100644 index 0000000..836ec67 --- /dev/null +++ b/config/prod.env.js @@ -0,0 +1,5 @@ +'use strict' +module.exports = { + NODE_ENV: '"production"', + BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"', +} diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..34b63ac Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..db13804 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + el-admin + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..1943cbf --- /dev/null +++ b/package.json @@ -0,0 +1,100 @@ +{ + "name": "eladmin-qt", + "version": "3.8.0", + "license": "MIT", + "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", + "author": "Pan ", + "scripts": { + "dev": "cross-env BABEL_ENV=development webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", + "start": "npm run dev", + "build": "node build/build.js", + "build:report": "npm_config_report=true npm run build", + "lint": "eslint --ext .js,.vue src", + "test": "npm run lint", + "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" + }, + "repository": { + "type": "git", + "url": "https://gitee.com/elunez/eladmin-qt" + }, + "dependencies": { + "axios": "0.18.0", + "element-ui": "2.4.6", + "js-cookie": "2.2.0", + "normalize.css": "7.0.0", + "clipboard": "1.7.1", + "nprogress": "0.2.0", + "vue": "2.5.17", + "vue-count-to": "1.0.13", + "echarts": "4.1.0", + "screenfull": "3.3.3", + "vue-router": "3.0.1", + "vuex": "3.0.1", + "xlsx": "^0.11.16", + "jszip": "3.1.5", + "@riophae/vue-treeselect": "0.0.37", + "file-saver": "1.3.8" + }, + "devDependencies": { + "autoprefixer": "8.5.0", + "babel-core": "6.26.3", + "babel-eslint": "8.2.6", + "babel-helper-vue-jsx-merge-props": "2.0.3", + "babel-loader": "7.1.5", + "babel-plugin-dynamic-import-node": "2.0.0", + "babel-plugin-syntax-jsx": "6.18.0", + "babel-plugin-transform-runtime": "6.23.0", + "babel-plugin-transform-vue-jsx": "3.7.0", + "babel-preset-env": "1.7.0", + "babel-preset-stage-2": "6.24.1", + "cross-env": "^5.1.1", + "chalk": "2.4.1", + "copy-webpack-plugin": "4.5.2", + "css-loader": "1.0.0", + "eslint": "4.19.1", + "eslint-friendly-formatter": "4.0.1", + "eslint-loader": "2.0.0", + "eslint-plugin-vue": "4.7.1", + "eventsource-polyfill": "0.9.6", + "file-loader": "1.1.11", + "friendly-errors-webpack-plugin": "1.7.0", + "html-webpack-plugin": "4.0.0-alpha", + "mini-css-extract-plugin": "0.4.1", + "node-notifier": "5.2.1", + "node-sass": "^4.7.2", + "optimize-css-assets-webpack-plugin": "5.0.0", + "ora": "3.0.0", + "path-to-regexp": "2.4.0", + "portfinder": "1.0.16", + "postcss-import": "12.0.0", + "postcss-loader": "2.1.6", + "postcss-url": "7.3.2", + "rimraf": "2.6.2", + "sass-loader": "7.0.3", + "script-loader": "0.7.2", + "script-ext-html-webpack-plugin": "2.0.1", + "semver": "5.5.0", + "shelljs": "0.8.2", + "svg-sprite-loader": "3.8.0", + "svgo": "1.0.5", + "uglifyjs-webpack-plugin": "1.2.7", + "url-loader": "1.0.1", + "vue-loader": "15.3.0", + "vue-style-loader": "4.1.2", + "vue-template-compiler": "2.5.17", + "webpack": "4.16.5", + "webpack-bundle-analyzer": "2.13.1", + "webpack-cli": "3.1.0", + "webpack-dev-server": "3.1.5", + "webpack-merge": "4.1.4" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..721d3a3 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/api/data.js b/src/api/data.js new file mode 100644 index 0000000..7a91ac6 --- /dev/null +++ b/src/api/data.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +export function initData(url, params) { + return request({ + url: url, + method: 'get', + params + }) +} diff --git a/src/api/login.js b/src/api/login.js new file mode 100644 index 0000000..1ba7cad --- /dev/null +++ b/src/api/login.js @@ -0,0 +1,19 @@ +import request from '@/utils/request' + +export function login(username, password) { + return request({ + url: 'auth/login', + method: 'post', + data: { + username, + password + } + }) +} + +export function getInfo() { + return request({ + url: 'auth/info', + method: 'get' + }) +} diff --git a/src/api/menu.js b/src/api/menu.js new file mode 100644 index 0000000..52a3737 --- /dev/null +++ b/src/api/menu.js @@ -0,0 +1,39 @@ +import request from '@/utils/request' + +// 获取所有的菜单树 +export function getMenusTree() { + return request({ + url: 'api/menus/tree', + method: 'get' + }) +} + +export function buildMenus() { + return request({ + url: 'api/menus/build', + method: 'get' + }) +} + +export function add(data) { + return request({ + url: 'api/menus', + method: 'post', + data + }) +} + +export function del(id) { + return request({ + url: 'api/menus/' + id, + method: 'delete' + }) +} + +export function edit(data) { + return request({ + url: 'api/menus', + method: 'put', + data + }) +} diff --git a/src/api/permission.js b/src/api/permission.js new file mode 100644 index 0000000..b8683e8 --- /dev/null +++ b/src/api/permission.js @@ -0,0 +1,32 @@ +import request from '@/utils/request' + +// 获取所有的权限树 +export function getPermissionTree() { + return request({ + url: 'api/permissions/tree', + method: 'get' + }) +} + +export function add(data) { + return request({ + url: 'api/permissions', + method: 'post', + data + }) +} + +export function del(id) { + return request({ + url: 'api/permissions/' + id, + method: 'delete' + }) +} + +export function edit(data) { + return request({ + url: 'api/permissions', + method: 'put', + data + }) +} diff --git a/src/api/redis.js b/src/api/redis.js new file mode 100644 index 0000000..efb16e6 --- /dev/null +++ b/src/api/redis.js @@ -0,0 +1,31 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/redis', + method: 'post', + data + }) +} + +export function del(id) { + return request({ + url: 'api/redis/' + id, + method: 'delete' + }) +} + +export function delAll() { + return request({ + url: 'api/redis/all', + method: 'delete' + }) +} + +export function edit(data) { + return request({ + url: 'api/redis', + method: 'put', + data + }) +} diff --git a/src/api/role.js b/src/api/role.js new file mode 100644 index 0000000..69c8810 --- /dev/null +++ b/src/api/role.js @@ -0,0 +1,32 @@ +import request from '@/utils/request' + +// 获取所有的Role +export function getRoleTree() { + return request({ + url: 'api/roles/tree', + method: 'get' + }) +} + +export function add(data) { + return request({ + url: 'api/roles', + method: 'post', + data + }) +} + +export function del(id) { + return request({ + url: 'api/roles/' + id, + method: 'delete' + }) +} + +export function edit(data) { + return request({ + url: 'api/roles', + method: 'put', + data + }) +} diff --git a/src/api/user.js b/src/api/user.js new file mode 100644 index 0000000..c61b0db --- /dev/null +++ b/src/api/user.js @@ -0,0 +1,24 @@ +import request from '@/utils/request' + +export function add(data) { + return request({ + url: 'api/users', + method: 'post', + data + }) +} + +export function del(id) { + return request({ + url: 'api/users/' + id, + method: 'delete' + }) +} + +export function edit(data) { + return request({ + url: 'api/users', + method: 'put', + data + }) +} diff --git a/src/api/visits.js b/src/api/visits.js new file mode 100644 index 0000000..147ae7c --- /dev/null +++ b/src/api/visits.js @@ -0,0 +1,22 @@ +import request from '@/utils/request' + +export function count() { + return request({ + url: 'api/visits', + method: 'post' + }) +} + +export function get() { + return request({ + url: 'api/visits', + method: 'get' + }) +} + +export function getChartData() { + return request({ + url: 'api/visits/chartData', + method: 'get' + }) +} diff --git a/src/assets/401_images/401.gif b/src/assets/401_images/401.gif new file mode 100644 index 0000000..cd6e0d9 Binary files /dev/null and b/src/assets/401_images/401.gif differ diff --git a/src/assets/404_images/404.png b/src/assets/404_images/404.png new file mode 100644 index 0000000..3d8e230 Binary files /dev/null and b/src/assets/404_images/404.png differ diff --git a/src/assets/404_images/404_cloud.png b/src/assets/404_images/404_cloud.png new file mode 100644 index 0000000..c6281d0 Binary files /dev/null and b/src/assets/404_images/404_cloud.png differ diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..89d967e --- /dev/null +++ b/src/components/Breadcrumb/index.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/components/GithubCorner/index.vue b/src/components/GithubCorner/index.vue new file mode 100644 index 0000000..726b212 --- /dev/null +++ b/src/components/GithubCorner/index.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue new file mode 100644 index 0000000..f1f9bda --- /dev/null +++ b/src/components/Hamburger/index.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue new file mode 100644 index 0000000..d2f5388 --- /dev/null +++ b/src/components/IconSelect/index.vue @@ -0,0 +1,71 @@ + + + + + + diff --git a/src/components/IconSelect/requireIcons.js b/src/components/IconSelect/requireIcons.js new file mode 100644 index 0000000..83a3395 --- /dev/null +++ b/src/components/IconSelect/requireIcons.js @@ -0,0 +1,11 @@ + +const req = require.context('../../icons/svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys() + +const re = /\.\/(.*)\.svg/ + +const icons = requireAll(req).map(i => { + return i.match(re)[1] +}) + +export default icons diff --git a/src/components/PanThumb/index.vue b/src/components/PanThumb/index.vue new file mode 100644 index 0000000..ec549f4 --- /dev/null +++ b/src/components/PanThumb/index.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/components/Screenfull/index.vue b/src/components/Screenfull/index.vue new file mode 100644 index 0000000..4cdcb56 --- /dev/null +++ b/src/components/Screenfull/index.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/src/components/ScrollPane/index.vue b/src/components/ScrollPane/index.vue new file mode 100644 index 0000000..89998e8 --- /dev/null +++ b/src/components/ScrollPane/index.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/components/SvgIcon/index.vue b/src/components/SvgIcon/index.vue new file mode 100644 index 0000000..12a1f58 --- /dev/null +++ b/src/components/SvgIcon/index.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/components/TextHoverEffect/Mallki.vue b/src/components/TextHoverEffect/Mallki.vue new file mode 100644 index 0000000..4ea29fc --- /dev/null +++ b/src/components/TextHoverEffect/Mallki.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/components/TreeTable/eval.js b/src/components/TreeTable/eval.js new file mode 100644 index 0000000..d9b89e1 --- /dev/null +++ b/src/components/TreeTable/eval.js @@ -0,0 +1,29 @@ +/** +* @Author: jianglei +* @Date: 2017-10-12 12:06:49 +*/ +'use strict' +import Vue from 'vue' +export default function treeToArray(data, expandAll, parent = null, level = null) { + let tmp = [] + Array.from(data).forEach(function(record) { + if (record._expanded === undefined) { + Vue.set(record, '_expanded', expandAll) + } + let _level = 1 + if (level !== undefined && level !== null) { + _level = level + 1 + } + Vue.set(record, '_level', _level) + // 如果有父元素 + if (parent) { + Vue.set(record, 'parent', parent) + } + tmp.push(record) + if (record.children && record.children.length > 0) { + const children = treeToArray(record.children, expandAll, record, _level) + tmp = tmp.concat(children) + } + }) + return tmp +} diff --git a/src/components/TreeTable/index.vue b/src/components/TreeTable/index.vue new file mode 100644 index 0000000..a48765c --- /dev/null +++ b/src/components/TreeTable/index.vue @@ -0,0 +1,127 @@ + + + + + + diff --git a/src/components/TreeTable/readme.md b/src/components/TreeTable/readme.md new file mode 100644 index 0000000..5b598e1 --- /dev/null +++ b/src/components/TreeTable/readme.md @@ -0,0 +1,89 @@ +## 写在前面 +此组件仅提供一个创建TreeTable的解决思路 + +## prop说明 +#### *data* + **必填** + + 原始数据,要求是一个数组或者对象 + ```javascript + [{ + key1: value1, + key2: value2, + children: [{ + key1: value1 + }, + { + key1: value1 + }] + }, + { + key1: value1 + }] + ``` + 或者 + ```javascript + { + key1: value1, + key2: value2, + children: [{ + key1: value1 + }, + { + key1: value1 + }] + } + ``` + +#### columns + 列属性,要求是一个数组 + + 1. text: 显示在表头的文字 + 2. value: 对应data的key。treeTable将显示相应的value + 3. width: 每列的宽度,为一个数字(可选) + + 如果你想要每个字段都有自定义的样式或者嵌套其他组件,columns可不提供,直接像在el-table一样写即可,如果没有自定义内容,提供columns将更加的便捷方便 + + 如果你有几个字段是需要自定义的,几个不需要,那么可以将不需要自定义的字段放入columns,将需要自定义的内容放入到slot中,详情见后文 + ```javascript + [{ + value:string, + text:string, + width:number + },{ + value:string, + text:string, + width:number + }] + ``` + +#### expandAll + 是否默认全部展开,boolean值,默认为false + +#### evalFunc + 解析函数,function,非必须 + + 如果不提供,将使用默认的[evalFunc](./eval.js) + + 如果提供了evalFunc,那么会用提供的evalFunc去解析data,并返回treeTable渲染所需要的值。如何编写一个evalFunc,请参考[*eval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/components/TreeTable/eval.js)或[*customEval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customEval.js) + +#### evalArgs + 解析函数的参数,是一个数组 + + **请注意,自定义的解析函数参数第一个为this.data,第二个参数为, this.expandAll,你不需要在evalArgs填写。一定记住,这两个参数是强制性的,并且位置不可颠倒** *this.data为需要解析的数据,this.expandAll为是否默认展开* + + 如你的解析函数需要的参数为`(this.data, this.expandAll,1,2,3,4)`,那么你只需要将`[1,2,3,4]`赋值给`evalArgs`就可以了 + + 如果你的解析函数参数只有`(this.data, this.expandAll)`,那么就可以不用填写evalArgs了 + + 具体可参考[*customEval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customEval.js)的函数参数和[customTreeTable](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customTreeTable.vue)的`evalArgs`属性值 + + ## slot + 这是一个自定义列的插槽。 + + 默认情况下,treeTable只有一行行展示数据的功能。但是一般情况下,我们会要给行加上一个操作按钮或者根据当行数据展示不同的样式,这时我们就需要自定义列了。请参考[customTreeTable](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customTreeTable.vue),[实例效果](https://panjiachen.github.io/vue-element-admin/#/table/tree-table) + + `slot`和`columns属性`可同时存在,columns里面的数据列会在slot自定义列的左边展示 + + ## 其他 + 如果有其他的需求,请参考[el-table](http://element-cn.eleme.io/#/en-US/component/table)的api自行修改index.vue diff --git a/src/icons/index.js b/src/icons/index.js new file mode 100644 index 0000000..d9fe4d8 --- /dev/null +++ b/src/icons/index.js @@ -0,0 +1,9 @@ +import Vue from 'vue' +import SvgIcon from '@/components/SvgIcon' // svg组件 + +// register globally +Vue.component('svg-icon', SvgIcon) + +const requireAll = requireContext => requireContext.keys().map(requireContext) +const req = require.context('./svg', false, /\.svg$/) +requireAll(req) diff --git a/src/icons/svg/add.svg b/src/icons/svg/add.svg new file mode 100644 index 0000000..2ddbd0b --- /dev/null +++ b/src/icons/svg/add.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/src/icons/svg/icon.svg b/src/icons/svg/icon.svg new file mode 100644 index 0000000..0faf313 --- /dev/null +++ b/src/icons/svg/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/index.svg b/src/icons/svg/index.svg new file mode 100644 index 0000000..5317d37 --- /dev/null +++ b/src/icons/svg/index.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/ipvisits.svg b/src/icons/svg/ipvisits.svg new file mode 100644 index 0000000..59eebc6 --- /dev/null +++ b/src/icons/svg/ipvisits.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/log.svg b/src/icons/svg/log.svg new file mode 100644 index 0000000..8f26a90 --- /dev/null +++ b/src/icons/svg/log.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/menu.svg b/src/icons/svg/menu.svg new file mode 100644 index 0000000..5c8e20d --- /dev/null +++ b/src/icons/svg/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/monitor.svg b/src/icons/svg/monitor.svg new file mode 100644 index 0000000..bc308cb --- /dev/null +++ b/src/icons/svg/monitor.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/src/icons/svg/password.svg b/src/icons/svg/password.svg new file mode 100644 index 0000000..e291d85 --- /dev/null +++ b/src/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/peoples.svg b/src/icons/svg/peoples.svg new file mode 100644 index 0000000..2c91161 --- /dev/null +++ b/src/icons/svg/peoples.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/permission.svg b/src/icons/svg/permission.svg new file mode 100644 index 0000000..b389846 --- /dev/null +++ b/src/icons/svg/permission.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/src/icons/svg/redis.svg b/src/icons/svg/redis.svg new file mode 100644 index 0000000..4b6da33 --- /dev/null +++ b/src/icons/svg/redis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/role.svg b/src/icons/svg/role.svg new file mode 100644 index 0000000..a474450 --- /dev/null +++ b/src/icons/svg/role.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/src/icons/svg/sqlMonitor.svg b/src/icons/svg/sqlMonitor.svg new file mode 100644 index 0000000..f530a06 --- /dev/null +++ b/src/icons/svg/sqlMonitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/system.svg b/src/icons/svg/system.svg new file mode 100644 index 0000000..dba28cf --- /dev/null +++ b/src/icons/svg/system.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/src/icons/svg/user.svg b/src/icons/svg/user.svg new file mode 100644 index 0000000..0ba0716 --- /dev/null +++ b/src/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/visits.svg b/src/icons/svg/visits.svg new file mode 100644 index 0000000..88c8150 --- /dev/null +++ b/src/icons/svg/visits.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svg/zujian.svg b/src/icons/svg/zujian.svg new file mode 100644 index 0000000..8e0cc0a --- /dev/null +++ b/src/icons/svg/zujian.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/svgo.yml b/src/icons/svgo.yml new file mode 100644 index 0000000..d11906a --- /dev/null +++ b/src/icons/svgo.yml @@ -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' diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..cc2da1f --- /dev/null +++ b/src/main.js @@ -0,0 +1,26 @@ +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 router from './router' +import store from './store' + +import '@/icons' // icon +import './permission' // permission control + +Vue.use(ElementUI, { locale }) +Vue.config.productionTip = false + +new Vue({ + el: '#app', + router, + store, + render: h => h(App) +}) diff --git a/src/mixins/initData.js b/src/mixins/initData.js new file mode 100644 index 0000000..a11ebed --- /dev/null +++ b/src/mixins/initData.js @@ -0,0 +1,42 @@ +import { initData } from '@/api/data' + +export default { + data() { + return { + loading: true, data: [], page: 0, size: 10, total: 0, url: '', params: {}, query: {} + } + }, + methods: { + async init() { + if (!await this.beforeInit()) { + return + } + return new Promise((resolve, reject) => { + this.loading = true + initData(this.url, this.params).then(res => { + this.total = res.totalElements + this.data = res.content + setTimeout(() => { + this.loading = false + }, 230) + resolve(res) + }).catch(err => { + this.loading = false + reject(err) + }) + }) + }, + beforeInit() { + return true + }, + pageChange(e) { + this.page = e - 1 + this.init() + }, + sizeChange(e) { + this.page = 0 + this.size = e + this.init() + } + } +} diff --git a/src/permission.js b/src/permission.js new file mode 100644 index 0000000..5aa3d12 --- /dev/null +++ b/src/permission.js @@ -0,0 +1,68 @@ +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' // getToken from cookie +import { buildMenus } from '@/api/menu' +import { filterAsyncRouter } from './store/modules/permission' + +NProgress.configure({ showSpinner: false })// NProgress Configuration + +const whiteList = ['/login']// no redirect whitelist + +// auth judge function +function hasPermission(roles, permissionRoles) { + if (roles.indexOf('ADMIN') >= 0) return true // admin auth passed directly + if (!permissionRoles) return true + return roles.some(role => permissionRoles.indexOf(role) >= 0) +} + +router.beforeEach((to, from, next) => { + NProgress.start() // start progress bar + if (getToken()) { // determine if there has token + /* has token*/ + if (to.path === '/login') { + next({ path: '/' }) + NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it + } else { + if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 + store.dispatch('GetInfo').then(res => { // 拉取user_info + buildMenus().then(res => { + const asyncRouter = filterAsyncRouter(res) + asyncRouter.push({ path: '*', redirect: '/404', hidden: true }) + store.dispatch('GenerateRoutes', asyncRouter).then(() => { // 存储路由 + console.log(asyncRouter) + router.addRoutes(asyncRouter) // 动态添加可访问路由表 + next({ ...to, replace: true })// hack方法 确保addRoutes已完成 + }) + }) + }).catch((err) => { + store.dispatch('LogOut').then(() => { + Message.error(err || 'Verification failed, please login again') + next({ path: '/' }) + }) + }) + } else { + // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ + if (hasPermission(store.getters.roles, to.meta.roles)) { + next() + } else { + next({ path: '/401', replace: true, query: { noGoBack: true }}) + } + } + } + } else { + /* has no token*/ + if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 + next() + } else { + next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页 + NProgress.done() + } + } +}) + +router.afterEach(() => { + NProgress.done() // finish progress bar +}) diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..37f70e8 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,57 @@ +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) + +/* Layout */ +import Layout from '../views/layout/Layout' + +/** +* hidden: true if `hidden:true` will not show in the sidebar(default is false) +* alwaysShow: true if set true, will always show the root menu, whatever its child routes length +* if not set alwaysShow, only more than one route under the children +* it will becomes nested mode, otherwise not show the root menu +* redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb +* name:'router-name' the name is used by (must set!!!) +* meta : { + title: 'title' the name show in submenu and breadcrumb (recommend set) + icon: 'svg-name' the icon show in the sidebar, + } +**/ + +export const constantRouterMap = [ + { path: '/login', + component: () => import('@/views/login/index'), + hidden: true + }, + { + path: '/404', + component: () => import('@/views/errorPage/404'), + hidden: true + }, + { + path: '/401', + component: () => import('@/views/errorPage/401'), + hidden: true + }, + { + path: '/', + component: Layout, + redirect: 'dashboard', + children: [ + { + path: 'dashboard', + component: () => import('@/views/dashboard/index'), + name: '首页', + meta: { title: '首页', icon: 'index', noCache: true } + } + ] + } + // { path: '*', redirect: '/404', hidden: true } +] + +export default new Router({ + mode: 'history', + scrollBehavior: () => ({ y: 0 }), + routes: constantRouterMap +}) diff --git a/src/store/getters.js b/src/store/getters.js new file mode 100644 index 0000000..5489f06 --- /dev/null +++ b/src/store/getters.js @@ -0,0 +1,13 @@ +const getters = { + sidebar: state => state.app.sidebar, + device: state => state.app.device, + token: state => state.user.token, + avatar: state => state.user.avatar, + visitedViews: state => state.tagsView.visitedViews, + cachedViews: state => state.tagsView.cachedViews, + name: state => state.user.name, + roles: state => state.user.roles, + permission_routers: state => state.permission.routers, + addRouters: state => state.permission.addRouters +} +export default getters diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..e527688 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,21 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import app from './modules/app' +import user from './modules/user' +import tagsView from './modules/tagsView' +import permission from './modules/permission' +import getters from './getters' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + modules: { + app, + user, + tagsView, + permission + }, + getters +}) + +export default store diff --git a/src/store/modules/app.js b/src/store/modules/app.js new file mode 100644 index 0000000..8aefaa6 --- /dev/null +++ b/src/store/modules/app.js @@ -0,0 +1,43 @@ +import Cookies from 'js-cookie' + +const app = { + state: { + sidebar: { + opened: !+Cookies.get('sidebarStatus'), + withoutAnimation: false + }, + device: 'desktop' + }, + mutations: { + TOGGLE_SIDEBAR: state => { + if (state.sidebar.opened) { + Cookies.set('sidebarStatus', 1) + } else { + Cookies.set('sidebarStatus', 0) + } + state.sidebar.opened = !state.sidebar.opened + state.sidebar.withoutAnimation = false + }, + CLOSE_SIDEBAR: (state, withoutAnimation) => { + Cookies.set('sidebarStatus', 1) + state.sidebar.opened = false + state.sidebar.withoutAnimation = withoutAnimation + }, + TOGGLE_DEVICE: (state, device) => { + state.device = device + } + }, + actions: { + ToggleSideBar: ({ commit }) => { + commit('TOGGLE_SIDEBAR') + }, + closeSideBar({ commit }, { withoutAnimation }) { + commit('CLOSE_SIDEBAR', withoutAnimation) + }, + toggleDevice({ commit }, device) { + commit('TOGGLE_DEVICE', device) + } + } +} + +export default app diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js new file mode 100644 index 0000000..2f545ef --- /dev/null +++ b/src/store/modules/permission.js @@ -0,0 +1,44 @@ +import { constantRouterMap } from '@/router' +import Layout from '@/views/layout/Layout' + +const permission = { + state: { + routers: constantRouterMap, + addRouters: [] + }, + mutations: { + SET_ROUTERS: (state, routers) => { + state.addRouters = routers + state.routers = constantRouterMap.concat(routers) + } + }, + actions: { + GenerateRoutes({ commit }, asyncRouter) { + commit('SET_ROUTERS', asyncRouter) + } + } +} + +export const filterAsyncRouter = (routers) => { // 遍历后台传来的路由字符串,转换为组件对象 + const accessedRouters = routers.filter(router => { + if (router.component) { + if (router.component === 'Layout') { // Layout组件特殊处理 + router.component = Layout + } else { + const component = router.component + router.component = loadView(component) + } + } + if (router.children && router.children.length) { + router.children = filterAsyncRouter(router.children) + } + return true + }) + return accessedRouters +} + +export const loadView = (view) => { // 路由懒加载 + return () => import(/* webpackChunkName: "view-[request]" */ `@/views/${view}.vue`) +} + +export default permission diff --git a/src/store/modules/tagsView.js b/src/store/modules/tagsView.js new file mode 100644 index 0000000..cbf9eeb --- /dev/null +++ b/src/store/modules/tagsView.js @@ -0,0 +1,162 @@ +const tagsView = { + state: { + visitedViews: [], + cachedViews: [] + }, + mutations: { + ADD_VISITED_VIEW: (state, view) => { + if (state.visitedViews.some(v => v.path === view.path)) return + state.visitedViews.push( + Object.assign({}, view, { + title: view.meta.title || 'no-name' + }) + ) + }, + ADD_CACHED_VIEW: (state, view) => { + if (state.cachedViews.includes(view.name)) return + if (!view.meta.noCache) { + state.cachedViews.push(view.name) + } + }, + + DEL_VISITED_VIEW: (state, view) => { + for (const [i, v] of state.visitedViews.entries()) { + if (v.path === view.path) { + state.visitedViews.splice(i, 1) + break + } + } + }, + DEL_CACHED_VIEW: (state, view) => { + for (const i of state.cachedViews) { + if (i === view.name) { + const index = state.cachedViews.indexOf(i) + state.cachedViews.splice(index, 1) + break + } + } + }, + + DEL_OTHERS_VISITED_VIEWS: (state, view) => { + for (const [i, v] of state.visitedViews.entries()) { + if (v.path === view.path) { + state.visitedViews = state.visitedViews.slice(i, i + 1) + break + } + } + }, + DEL_OTHERS_CACHED_VIEWS: (state, view) => { + for (const i of state.cachedViews) { + if (i === view.name) { + const index = state.cachedViews.indexOf(i) + state.cachedViews = state.cachedViews.slice(index, index + 1) + break + } + } + }, + + DEL_ALL_VISITED_VIEWS: state => { + state.visitedViews = [] + }, + DEL_ALL_CACHED_VIEWS: state => { + state.cachedViews = [] + }, + + UPDATE_VISITED_VIEW: (state, view) => { + for (let v of state.visitedViews) { + if (v.path === view.path) { + v = Object.assign(v, view) + break + } + } + } + + }, + actions: { + addView({ dispatch }, view) { + dispatch('addVisitedView', view) + dispatch('addCachedView', view) + }, + addVisitedView({ commit }, view) { + commit('ADD_VISITED_VIEW', view) + }, + addCachedView({ commit }, view) { + commit('ADD_CACHED_VIEW', view) + }, + + delView({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delVisitedView', view) + dispatch('delCachedView', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delVisitedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_VISITED_VIEW', view) + resolve([...state.visitedViews]) + }) + }, + delCachedView({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_CACHED_VIEW', view) + resolve([...state.cachedViews]) + }) + }, + + delOthersViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delOthersVisitedViews', view) + dispatch('delOthersCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delOthersVisitedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_VISITED_VIEWS', view) + resolve([...state.visitedViews]) + }) + }, + delOthersCachedViews({ commit, state }, view) { + return new Promise(resolve => { + commit('DEL_OTHERS_CACHED_VIEWS', view) + resolve([...state.cachedViews]) + }) + }, + + delAllViews({ dispatch, state }, view) { + return new Promise(resolve => { + dispatch('delAllVisitedViews', view) + dispatch('delAllCachedViews', view) + resolve({ + visitedViews: [...state.visitedViews], + cachedViews: [...state.cachedViews] + }) + }) + }, + delAllVisitedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_VISITED_VIEWS') + resolve([...state.visitedViews]) + }) + }, + delAllCachedViews({ commit, state }) { + return new Promise(resolve => { + commit('DEL_ALL_CACHED_VIEWS') + resolve([...state.cachedViews]) + }) + }, + + updateVisitedView({ commit }, view) { + commit('UPDATE_VISITED_VIEW', view) + } + } +} + +export default tagsView diff --git a/src/store/modules/user.js b/src/store/modules/user.js new file mode 100644 index 0000000..35c48f7 --- /dev/null +++ b/src/store/modules/user.js @@ -0,0 +1,68 @@ +import { login, getInfo } from '@/api/login' +import { getToken, setToken, removeToken } from '@/utils/auth' + +const user = { + state: { + token: getToken(), + name: '', + avatar: '', + roles: [] + }, + + mutations: { + SET_TOKEN: (state, token) => { + state.token = token + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_AVATAR: (state, avatar) => { + state.avatar = avatar + }, + SET_ROLES: (state, roles) => { + state.roles = roles + } + }, + + actions: { + // 登录 + Login({ commit }, userInfo) { + const username = userInfo.username.trim() + return new Promise((resolve, reject) => { + login(username, userInfo.password).then(res => { + setToken(res.token) + commit('SET_TOKEN', res.token) + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 获取用户信息 + GetInfo({ commit, state }) { + return new Promise((resolve, reject) => { + getInfo().then(res => { + commit('SET_ROLES', res.roles) + commit('SET_NAME', res.username) + commit('SET_AVATAR', res.avatar) + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + + // 登出 + LogOut({ commit, state }) { + return new Promise((resolve, reject) => { + commit('SET_TOKEN', '') + commit('SET_ROLES', []) + removeToken() + resolve() + }) + } + } +} + +export default user diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss new file mode 100644 index 0000000..ef7bb5d --- /dev/null +++ b/src/styles/element-ui.scss @@ -0,0 +1,29 @@ + //to reset element-ui default css +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + +//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + +//element ui upload +.upload-container { + .el-upload { + width: 100%; + .el-upload-dragger { + width: 100%; + height: 200px; + } + } +} diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..5afdc45 --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,137 @@ +@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, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + outline: none; + text-decoration: none; +} + +div:focus{ + outline: none; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +.warn-content{ + background: rgba(66,185,131,.1); + border-radius: 2px; + padding: 16px; + padding: 1rem; + line-height: 1.6rem; + word-spacing: .05rem; + a{ + color: #42b983; + font-weight: 600; + } +} + +//main-container全局样式 +.app-main{ + min-height: 100% +} + +.app-container { + padding: 20px; +} + +.head-container { + padding-bottom: 10px; + .filter-item { + display: inline-block; + vertical-align: middle; + margin-bottom: 10px; + } + input { + height: 30.5px; + line-height: 30.5px; + } + .el-input__icon { + line-height: 31px; + } +} + +.el-avatar { + display: inline-block; + text-align: center; + background: #ccc; + color: #fff; + white-space: nowrap; + position: relative; + overflow: hidden; + vertical-align: middle; + width: 32px; + height: 32px; + line-height: 32px; + border-radius: 16px; +} + +.badge { + position: relative; + display: inline-block; + padding: 0 6px; + font-size: 12px; + text-align: center; + background-color: #2F4056; + color: #fff; + border-radius: 2px; +} + +.badge-bg-orange { + background-color: #FF5722!important; +} diff --git a/src/styles/mixin.scss b/src/styles/mixin.scss new file mode 100644 index 0000000..601d7a0 --- /dev/null +++ b/src/styles/mixin.scss @@ -0,0 +1,27 @@ +@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%; +} + diff --git a/src/styles/sidebar.scss b/src/styles/sidebar.scss new file mode 100644 index 0000000..d9b651a --- /dev/null +++ b/src/styles/sidebar.scss @@ -0,0 +1,133 @@ +#app { + // 主体区域 + .main-container { + min-height: 100%; + transition: margin-left .28s; + margin-left: 180px; + position: relative; + } + // 侧边栏 + .sidebar-container { + transition: width 0.28s; + width: 180px !important; + 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; + } + .el-scrollbar__bar.is-vertical{ + right: 0px; + } + .scrollbar-wrapper { + overflow-x: hidden!important; + .el-scrollbar__view { + height: 100%; + } + } + .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; + } + .is-active > .el-submenu__title{ + color: #f4f4f5!important; + } + } + .hideSidebar { + .sidebar-container { + width: 36px !important; + } + .main-container { + margin-left: 36px; + } + .submenu-title-noDropdown { + padding-left: 10px !important; + position: relative; + .el-tooltip { + padding: 0 10px !important; + } + } + .el-submenu { + overflow: hidden; + &>.el-submenu__title { + padding-left: 10px !important; + .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; + } + } + } + } + } + .sidebar-container .nest-menu .el-submenu>.el-submenu__title, + .sidebar-container .el-submenu .el-menu-item { + min-width: 180px !important; + background-color: $subMenuBg !important; + &:hover { + background-color: $menuHover !important; + } + } + .el-menu--collapse .el-menu .el-submenu { + min-width: 180px !important; + } + + //适配移动端 + .mobile { + .main-container { + margin-left: 0px; + } + .sidebar-container { + transition: transform .28s; + width: 180px !important; + } + &.hideSidebar { + .sidebar-container { + transition-duration: 0.3s; + transform: translate3d(-180px, 0, 0); + } + } + } + .withoutAnimation { + .main-container, + .sidebar-container { + transition: none; + } + } +} + +.el-menu--vertical{ + & >.el-menu{ + .svg-icon{ + margin-right: 16px; + } + } +} diff --git a/src/styles/transition.scss b/src/styles/transition.scss new file mode 100644 index 0000000..8dd9b04 --- /dev/null +++ b/src/styles/transition.scss @@ -0,0 +1,46 @@ +//globl 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); +} + +/*fade*/ +.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; +} diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 0000000..2fee827 --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,4 @@ +//sidebar +$menuBg:#304156; +$subMenuBg:#1f2d3d; +$menuHover:#001528; diff --git a/src/utils/auth.js b/src/utils/auth.js new file mode 100644 index 0000000..08a43d6 --- /dev/null +++ b/src/utils/auth.js @@ -0,0 +1,15 @@ +import Cookies from 'js-cookie' + +const TokenKey = '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) +} diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js new file mode 100644 index 0000000..2529590 --- /dev/null +++ b/src/utils/clipboard.js @@ -0,0 +1,36 @@ +import Vue from 'vue' +import Clipboard from 'clipboard' + +function clipboardSuccess() { + Vue.prototype.$message({ + message: '复制成功', + type: 'success', + duration: 1500 + }) +} + +function clipboardError() { + Vue.prototype.$message({ + message: '复制失败', + type: 'error' + }) +} + +export default function handleClipboard(text, event) { + const clipboard = new Clipboard(event.target, { + text: () => text + }) + clipboard.on('success', () => { + clipboardSuccess() + clipboard.off('error') + clipboard.off('success') + clipboard.destroy() + }) + clipboard.on('error', () => { + clipboardError() + clipboard.off('error') + clipboard.off('success') + clipboard.destroy() + }) + clipboard.onClick(event) +} diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..a6600ae --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,95 @@ +/** + * Created by jiachenpan on 16/11/18. + */ + +export function parseTime(time) { + if (time) { + var date = new Date(time) + var year = date.getFullYear() + /* 在日期格式中,月份是从0开始的,因此要加0 + * 使用三元表达式在小于10的前面加0,以达到格式统一 如 09:11:05 + * */ + var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1 + var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() + var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() + var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() + var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() + // 拼接 + return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds + } else { + return '' + } +} + +export function formatTime(time, option) { + time = +time * 1000 + 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() + + '分' + ) + } +} + +export function debounce(func, wait, immediate) { + let timeout, args, context, timestamp, result + + const later = function() { + // 据上一次触发时间间隔 + const last = +new Date() - timestamp + + // 上次被包装函数被调用时间间隔last小于设定时间间隔wait + if (last < wait && last > 0) { + timeout = setTimeout(later, wait - last) + } else { + timeout = null + // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用 + if (!immediate) { + result = func.apply(context, args) + if (!timeout) context = args = null + } + } + } + + return function(...args) { + context = this + timestamp = +new Date() + const callNow = immediate && !timeout + // 如果延时不存在,重新设定延时 + if (!timeout) timeout = setTimeout(later, wait) + if (callNow) { + result = func.apply(context, args) + context = args = null + } + + return result + } +} + +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} diff --git a/src/utils/md5.js b/src/utils/md5.js new file mode 100644 index 0000000..912c29c --- /dev/null +++ b/src/utils/md5.js @@ -0,0 +1,178 @@ +export function md5(sMessage) { + function RotateLeft(lValue, iShiftBits) { + return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)) + } + function AddUnsigned(lX, lY) { + var lX4, lY4, lX8, lY8, lResult + lX8 = (lX & 0x80000000) + lY8 = (lY & 0x80000000) + lX4 = (lX & 0x40000000) + lY4 = (lY & 0x40000000) + lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF) + if (lX4 & lY4) return (lResult ^ 0x80000000 ^ lX8 ^ lY8) + if (lX4 | lY4) { + if (lResult & 0x40000000) return (lResult ^ 0xC0000000 ^ lX8 ^ lY8) + else return (lResult ^ 0x40000000 ^ lX8 ^ lY8) + } else return (lResult ^ lX8 ^ lY8) + } + function F(x, y, z) { + return (x & y) | ((~x) & z) + } + function G(x, y, z) { + return (x & z) | (y & (~z)) + } + function H(x, y, z) { + return (x ^ y ^ z) + } + function I(x, y, z) { + return (y ^ (x | (~z))) + } + function FF(a, b, c, d, x, s, ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)) + return AddUnsigned(RotateLeft(a, s), b) + } + function GG(a, b, c, d, x, s, ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)) + return AddUnsigned(RotateLeft(a, s), b) + } + function HH(a, b, c, d, x, s, ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)) + return AddUnsigned(RotateLeft(a, s), b) + } + function II(a, b, c, d, x, s, ac) { + a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)) + return AddUnsigned(RotateLeft(a, s), b) + } + function ConvertToWordArray(sMessage) { + var lWordCount + var lMessageLength = sMessage.length + var lNumberOfWords_temp1 = lMessageLength + 8 + var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64 + var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16 + var lWordArray = Array(lNumberOfWords - 1) + var lBytePosition = 0 + var lByteCount = 0 + while (lByteCount < lMessageLength) { + lWordCount = (lByteCount - (lByteCount % 4)) / 4 + lBytePosition = (lByteCount % 4) * 8 + lWordArray[lWordCount] = (lWordArray[lWordCount] | (sMessage.charCodeAt(lByteCount) << lBytePosition)) + lByteCount++ + } + lWordCount = (lByteCount - (lByteCount % 4)) / 4 + lBytePosition = (lByteCount % 4) * 8 + lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition) + lWordArray[lNumberOfWords - 2] = lMessageLength << 3 + lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29 + return lWordArray + } + function WordToHex(lValue) { + var WordToHexValue = '' + var WordToHexValue_temp = '' + var lByte, lCount + for (lCount = 0; lCount <= 3; lCount++) { + lByte = (lValue >>> (lCount * 8)) & 255 + WordToHexValue_temp = '0' + lByte.toString(16) + WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2) + } + return WordToHexValue + } + var x = [] + var k, AA, BB, CC, DD, a, b, c, d + var S11 = 7 + var S12 = 12 + var S13 = 17 + var S14 = 22 + var S21 = 5 + var S22 = 9 + var S23 = 14 + var S24 = 20 + var S31 = 4 + var S32 = 11 + var S33 = 16 + var S34 = 23 + var S41 = 6 + var S42 = 10 + var S43 = 15 + var S44 = 21 + x = ConvertToWordArray(sMessage) + a = 0x67452301 + b = 0xEFCDAB89 + c = 0x98BADCFE + d = 0x10325476 + for (k = 0; k < x.length; k += 16) { + AA = a + BB = b + CC = c + DD = d + a = FF(a, b, c, d, x[k + 0], S11, 0xD76AA478) + d = FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756) + c = FF(c, d, a, b, x[k + 2], S13, 0x242070DB) + b = FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE) + a = FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF) + d = FF(d, a, b, c, x[k + 5], S12, 0x4787C62A) + c = FF(c, d, a, b, x[k + 6], S13, 0xA8304613) + b = FF(b, c, d, a, x[k + 7], S14, 0xFD469501) + a = FF(a, b, c, d, x[k + 8], S11, 0x698098D8) + d = FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF) + c = FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1) + b = FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE) + a = FF(a, b, c, d, x[k + 12], S11, 0x6B901122) + d = FF(d, a, b, c, x[k + 13], S12, 0xFD987193) + c = FF(c, d, a, b, x[k + 14], S13, 0xA679438E) + b = FF(b, c, d, a, x[k + 15], S14, 0x49B40821) + a = GG(a, b, c, d, x[k + 1], S21, 0xF61E2562) + d = GG(d, a, b, c, x[k + 6], S22, 0xC040B340) + c = GG(c, d, a, b, x[k + 11], S23, 0x265E5A51) + b = GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA) + a = GG(a, b, c, d, x[k + 5], S21, 0xD62F105D) + d = GG(d, a, b, c, x[k + 10], S22, 0x2441453) + c = GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681) + b = GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8) + a = GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6) + d = GG(d, a, b, c, x[k + 14], S22, 0xC33707D6) + c = GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87) + b = GG(b, c, d, a, x[k + 8], S24, 0x455A14ED) + a = GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905) + d = GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8) + c = GG(c, d, a, b, x[k + 7], S23, 0x676F02D9) + b = GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A) + a = HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942) + d = HH(d, a, b, c, x[k + 8], S32, 0x8771F681) + c = HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122) + b = HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C) + a = HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44) + d = HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9) + c = HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60) + b = HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70) + a = HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6) + d = HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA) + c = HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085) + b = HH(b, c, d, a, x[k + 6], S34, 0x4881D05) + a = HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039) + d = HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5) + c = HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8) + b = HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665) + a = II(a, b, c, d, x[k + 0], S41, 0xF4292244) + d = II(d, a, b, c, x[k + 7], S42, 0x432AFF97) + c = II(c, d, a, b, x[k + 14], S43, 0xAB9423A7) + b = II(b, c, d, a, x[k + 5], S44, 0xFC93A039) + a = II(a, b, c, d, x[k + 12], S41, 0x655B59C3) + d = II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92) + c = II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D) + b = II(b, c, d, a, x[k + 1], S44, 0x85845DD1) + a = II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F) + d = II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0) + c = II(c, d, a, b, x[k + 6], S43, 0xA3014314) + b = II(b, c, d, a, x[k + 13], S44, 0x4E0811A1) + a = II(a, b, c, d, x[k + 4], S41, 0xF7537E82) + d = II(d, a, b, c, x[k + 11], S42, 0xBD3AF235) + c = II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB) + b = II(b, c, d, a, x[k + 9], S44, 0xEB86D391) + a = AddUnsigned(a, AA) + b = AddUnsigned(b, BB) + c = AddUnsigned(c, CC) + d = AddUnsigned(d, DD) + } + var temp = WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d) + return temp.toLowerCase() +} diff --git a/src/utils/permission.js b/src/utils/permission.js new file mode 100644 index 0000000..724f0b2 --- /dev/null +++ b/src/utils/permission.js @@ -0,0 +1,25 @@ +import store from '@/store' + +/** + * @param {Array} value + * @returns {Boolean} + * @example see @/views/auth/directive.vue + */ +export default function checkPermission(value) { + if (value && value instanceof Array && value.length > 0) { + const roles = store.getters && store.getters.roles + const permissionRoles = value + + const hasPermission = roles.some(role => { + return permissionRoles.includes(role) + }) + + if (!hasPermission) { + return false + } + return true + } else { + console.error(`need roles! Like v-permission="['admin','editor']"`) + return false + } +} diff --git a/src/utils/request.js b/src/utils/request.js new file mode 100644 index 0000000..7d4a7eb --- /dev/null +++ b/src/utils/request.js @@ -0,0 +1,69 @@ +import axios from 'axios' +import { Notification, MessageBox } from 'element-ui' +import store from '../store' +import { getToken } from '@/utils/auth' + +// 创建axios实例 +const service = axios.create({ + baseURL: process.env.BASE_API, // api 的 base_url + timeout: 5000 // 请求超时时间 +}) + +// request拦截器 +service.interceptors.request.use( + config => { + if (store.getters.token) { + config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 + } + config.headers['Content-Type'] = 'application/json' + return config + }, + error => { + // Do something with request error + console.log(error) // for debug + Promise.reject(error) + } +) + +// response 拦截器 +service.interceptors.response.use( + response => { + const code = response.status + if (code < 200 || code > 300) { + Notification.error({ + title: response.message + }) + return Promise.reject('error') + } else { + return response.data + } + }, + error => { + const code = error.response.data.status + if (code === 403 || code === 401) { + MessageBox.confirm( + 'Token 无效或已经过期,你可以取消继续留在该页面,或者重新登录', + '提示', + { + confirmButtonText: '重新登录', + cancelButtonText: '取消', + type: 'warning' + } + ).then(() => { + store.dispatch('LogOut').then(() => { + location.reload() // 为了重新实例化vue-router对象 避免bug + }) + }) + } else { + const errorMsg = error.response.data.message + if (errorMsg !== undefined) { + Notification.error({ + title: errorMsg, + duration: 2500 + }) + } + } + return Promise.reject(error) + } +) +export default service diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000..ca70943 --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,27 @@ +/** + * Created by jiachenpan on 16/11/18. + */ + +/* 合法uri*/ +export function validateURL(textval) { + const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ + return urlregex.test(textval) +} + +/* 小写字母*/ +export function validateLowerCase(str) { + const reg = /^[a-z]+$/ + return reg.test(str) +} + +/* 大写字母*/ +export function validateUpperCase(str) { + const reg = /^[A-Z]+$/ + return reg.test(str) +} + +/* 大小写字母*/ +export function validatAlphabets(str) { + const reg = /^[A-Za-z]+$/ + return reg.test(str) +} diff --git a/src/vendor/Export2Excel.js b/src/vendor/Export2Excel.js new file mode 100644 index 0000000..ba956dc --- /dev/null +++ b/src/vendor/Export2Excel.js @@ -0,0 +1,206 @@ +/* eslint-disable */ +require('script-loader!file-saver'); +import XLSX from 'xlsx' + +function generateArray(table) { + var out = []; + var rows = table.querySelectorAll('tr'); + var ranges = []; + for (var R = 0; R < rows.length; ++R) { + var outRow = []; + var row = rows[R]; + var columns = row.querySelectorAll('td'); + for (var C = 0; C < columns.length; ++C) { + var cell = columns[C]; + var colspan = cell.getAttribute('colspan'); + var rowspan = cell.getAttribute('rowspan'); + var cellValue = cell.innerText; + if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue; + + //Skip ranges + ranges.forEach(function (range) { + if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) { + for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null); + } + }); + + //Handle Row Span + if (rowspan || colspan) { + rowspan = rowspan || 1; + colspan = colspan || 1; + ranges.push({ + s: { + r: R, + c: outRow.length + }, + e: { + r: R + rowspan - 1, + c: outRow.length + colspan - 1 + } + }); + }; + + //Handle Value + outRow.push(cellValue !== "" ? cellValue : null); + + //Handle Colspan + if (colspan) + for (var k = 0; k < colspan - 1; ++k) outRow.push(null); + } + out.push(outRow); + } + return [out, ranges]; +}; + +function datenum(v, date1904) { + if (date1904) v += 1462; + var epoch = Date.parse(v); + return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); +} + +function sheet_from_array_of_arrays(data, opts) { + var ws = {}; + var range = { + s: { + c: 10000000, + r: 10000000 + }, + e: { + c: 0, + r: 0 + } + }; + for (var R = 0; R != data.length; ++R) { + for (var C = 0; C != data[R].length; ++C) { + if (range.s.r > R) range.s.r = R; + if (range.s.c > C) range.s.c = C; + if (range.e.r < R) range.e.r = R; + if (range.e.c < C) range.e.c = C; + var cell = { + v: data[R][C] + }; + if (cell.v == null) continue; + var cell_ref = XLSX.utils.encode_cell({ + c: C, + r: R + }); + + if (typeof cell.v === 'number') cell.t = 'n'; + else if (typeof cell.v === 'boolean') cell.t = 'b'; + else if (cell.v instanceof Date) { + cell.t = 'n'; + cell.z = XLSX.SSF._table[14]; + cell.v = datenum(cell.v); + } else cell.t = 's'; + + ws[cell_ref] = cell; + } + } + if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range); + return ws; +} + +function Workbook() { + if (!(this instanceof Workbook)) return new Workbook(); + this.SheetNames = []; + this.Sheets = {}; +} + +function s2ab(s) { + var buf = new ArrayBuffer(s.length); + var view = new Uint8Array(buf); + for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; + return buf; +} + +export function export_table_to_excel(id) { + var theTable = document.getElementById(id); + var oo = generateArray(theTable); + var ranges = oo[1]; + + /* original data */ + var data = oo[0]; + var ws_name = "SheetJS"; + + var wb = new Workbook(), + ws = sheet_from_array_of_arrays(data); + + /* add ranges to worksheet */ + // ws['!cols'] = ['apple', 'banan']; + ws['!merges'] = ranges; + + /* add worksheet to workbook */ + wb.SheetNames.push(ws_name); + wb.Sheets[ws_name] = ws; + + var wbout = XLSX.write(wb, { + bookType: 'xlsx', + bookSST: false, + type: 'binary' + }); + + saveAs(new Blob([s2ab(wbout)], { + type: "application/octet-stream" + }), "test.xlsx") +} + +export function export_json_to_excel({ + header, + data, + filename, + autoWidth = true, + bookType= 'xlsx' +} = {}) { + /* original data */ + filename = filename || 'excel-list' + data = [...data] + data.unshift(header); + var ws_name = "SheetJS"; + var wb = new Workbook(), + ws = sheet_from_array_of_arrays(data); + + if (autoWidth) { + /*设置worksheet每列的最大宽度*/ + const colWidth = data.map(row => row.map(val => { + /*先判断是否为null/undefined*/ + if (val == null) { + return { + 'wch': 10 + }; + } + /*再判断是否为中文*/ + else if (val.toString().charCodeAt(0) > 255) { + return { + 'wch': val.toString().length * 2 + }; + } else { + return { + 'wch': val.toString().length + }; + } + })) + /*以第一行为初始值*/ + let result = colWidth[0]; + for (let i = 1; i < colWidth.length; i++) { + for (let j = 0; j < colWidth[i].length; j++) { + if (result[j]['wch'] < colWidth[i][j]['wch']) { + result[j]['wch'] = colWidth[i][j]['wch']; + } + } + } + ws['!cols'] = result; + } + + /* add worksheet to workbook */ + wb.SheetNames.push(ws_name); + wb.Sheets[ws_name] = ws; + + var wbout = XLSX.write(wb, { + bookType: bookType, + bookSST: false, + type: 'binary' + }); + saveAs(new Blob([s2ab(wbout)], { + type: "application/octet-stream" + }), `${filename}.${bookType}`); +} diff --git a/src/vendor/Export2Zip.js b/src/vendor/Export2Zip.js new file mode 100644 index 0000000..f776465 --- /dev/null +++ b/src/vendor/Export2Zip.js @@ -0,0 +1,24 @@ +/* eslint-disable */ +require('script-loader!file-saver'); +import JSZip from 'jszip' + +export function export_txt_to_zip(th, jsonData, txtName, zipName) { + const zip = new JSZip() + const txt_name = txtName || 'file' + const zip_name = zipName || 'file' + const data = jsonData + let txtData = `${th}\r\n` + data.forEach((row) => { + let tempStr = '' + tempStr = row.toString() + txtData += `${tempStr}\r\n` + }) + zip.file(`${txt_name}.txt`, txtData) + zip.generateAsync({ + type: "blob" + }).then((blob) => { + saveAs(blob, `${zip_name}.zip`) + }, (err) => { + alert('导出失败') + }) +} diff --git a/src/views/components/IconSelect.vue b/src/views/components/IconSelect.vue new file mode 100644 index 0000000..9e4cc10 --- /dev/null +++ b/src/views/components/IconSelect.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/src/views/dashboard/admin/components/BarChart.vue b/src/views/dashboard/admin/components/BarChart.vue new file mode 100644 index 0000000..28e0aef --- /dev/null +++ b/src/views/dashboard/admin/components/BarChart.vue @@ -0,0 +1,106 @@ + + + diff --git a/src/views/dashboard/admin/components/LineChart.vue b/src/views/dashboard/admin/components/LineChart.vue new file mode 100644 index 0000000..2b4c676 --- /dev/null +++ b/src/views/dashboard/admin/components/LineChart.vue @@ -0,0 +1,155 @@ + + + diff --git a/src/views/dashboard/admin/components/PanelGroup.vue b/src/views/dashboard/admin/components/PanelGroup.vue new file mode 100644 index 0000000..1764711 --- /dev/null +++ b/src/views/dashboard/admin/components/PanelGroup.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src/views/dashboard/admin/components/PieChart.vue b/src/views/dashboard/admin/components/PieChart.vue new file mode 100644 index 0000000..edb17d5 --- /dev/null +++ b/src/views/dashboard/admin/components/PieChart.vue @@ -0,0 +1,84 @@ + + + diff --git a/src/views/dashboard/admin/components/RaddarChart.vue b/src/views/dashboard/admin/components/RaddarChart.vue new file mode 100644 index 0000000..11fde32 --- /dev/null +++ b/src/views/dashboard/admin/components/RaddarChart.vue @@ -0,0 +1,120 @@ + + + diff --git a/src/views/dashboard/admin/index.vue b/src/views/dashboard/admin/index.vue new file mode 100644 index 0000000..2d7cc38 --- /dev/null +++ b/src/views/dashboard/admin/index.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue new file mode 100644 index 0000000..ef50f6a --- /dev/null +++ b/src/views/dashboard/index.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/views/errorPage/401.vue b/src/views/errorPage/401.vue new file mode 100644 index 0000000..2442012 --- /dev/null +++ b/src/views/errorPage/401.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/views/errorPage/404.vue b/src/views/errorPage/404.vue new file mode 100644 index 0000000..d97ab72 --- /dev/null +++ b/src/views/errorPage/404.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/src/views/layout/Layout.vue b/src/views/layout/Layout.vue new file mode 100644 index 0000000..0e14f16 --- /dev/null +++ b/src/views/layout/Layout.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/views/layout/components/AppMain.vue b/src/views/layout/components/AppMain.vue new file mode 100644 index 0000000..8a8fe46 --- /dev/null +++ b/src/views/layout/components/AppMain.vue @@ -0,0 +1,34 @@ + + + + + + diff --git a/src/views/layout/components/Navbar.vue b/src/views/layout/components/Navbar.vue new file mode 100644 index 0000000..d336e79 --- /dev/null +++ b/src/views/layout/components/Navbar.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/src/views/layout/components/Sidebar/FixiOSBug.js b/src/views/layout/components/Sidebar/FixiOSBug.js new file mode 100644 index 0000000..5e0a926 --- /dev/null +++ b/src/views/layout/components/Sidebar/FixiOSBug.js @@ -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 mouseeleave 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) + } + } + } + } +} diff --git a/src/views/layout/components/Sidebar/Item.vue b/src/views/layout/components/Sidebar/Item.vue new file mode 100644 index 0000000..b515f61 --- /dev/null +++ b/src/views/layout/components/Sidebar/Item.vue @@ -0,0 +1,29 @@ + diff --git a/src/views/layout/components/Sidebar/Link.vue b/src/views/layout/components/Sidebar/Link.vue new file mode 100644 index 0000000..5d366f2 --- /dev/null +++ b/src/views/layout/components/Sidebar/Link.vue @@ -0,0 +1,39 @@ + + + + diff --git a/src/views/layout/components/Sidebar/SidebarItem.vue b/src/views/layout/components/Sidebar/SidebarItem.vue new file mode 100644 index 0000000..eb57b2a --- /dev/null +++ b/src/views/layout/components/Sidebar/SidebarItem.vue @@ -0,0 +1,103 @@ + + + diff --git a/src/views/layout/components/Sidebar/index.vue b/src/views/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..dff85f1 --- /dev/null +++ b/src/views/layout/components/Sidebar/index.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/views/layout/components/TagsView.vue b/src/views/layout/components/TagsView.vue new file mode 100644 index 0000000..3c102e7 --- /dev/null +++ b/src/views/layout/components/TagsView.vue @@ -0,0 +1,237 @@ + + + + + + + diff --git a/src/views/layout/components/index.js b/src/views/layout/components/index.js new file mode 100644 index 0000000..9fc98d6 --- /dev/null +++ b/src/views/layout/components/index.js @@ -0,0 +1,4 @@ +export { default as Navbar } from './Navbar' +export { default as Sidebar } from './Sidebar' +export { default as AppMain } from './AppMain' +export { default as TagsView } from './TagsView' diff --git a/src/views/layout/mixin/ResizeHandler.js b/src/views/layout/mixin/ResizeHandler.js new file mode 100644 index 0000000..2636d5b --- /dev/null +++ b/src/views/layout/mixin/ResizeHandler.js @@ -0,0 +1,41 @@ +import store from '@/store' + +const { body } = document +const WIDTH = 1024 +const RATIO = 3 + +export default { + watch: { + $route(route) { + if (this.device === 'mobile' && this.sidebar.opened) { + store.dispatch('closeSideBar', { withoutAnimation: false }) + } + } + }, + beforeMount() { + window.addEventListener('resize', this.resizeHandler) + }, + mounted() { + const isMobile = this.isMobile() + if (isMobile) { + store.dispatch('toggleDevice', 'mobile') + store.dispatch('closeSideBar', { withoutAnimation: true }) + } + }, + methods: { + isMobile() { + const rect = body.getBoundingClientRect() + return rect.width - RATIO < WIDTH + }, + resizeHandler() { + if (!document.hidden) { + const isMobile = this.isMobile() + store.dispatch('toggleDevice', isMobile ? 'mobile' : 'desktop') + + if (isMobile) { + store.dispatch('closeSideBar', { withoutAnimation: true }) + } + } + } + } +} diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 0000000..1cdba49 --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,212 @@ + + + + + + + diff --git a/src/views/monitor/log/index.vue b/src/views/monitor/log/index.vue new file mode 100644 index 0000000..da8d852 --- /dev/null +++ b/src/views/monitor/log/index.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/src/views/monitor/log/module/search.vue b/src/views/monitor/log/module/search.vue new file mode 100644 index 0000000..3e94f12 --- /dev/null +++ b/src/views/monitor/log/module/search.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/views/monitor/redis/index.vue b/src/views/monitor/redis/index.vue new file mode 100644 index 0000000..d456825 --- /dev/null +++ b/src/views/monitor/redis/index.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/views/monitor/redis/module/add.vue b/src/views/monitor/redis/module/add.vue new file mode 100644 index 0000000..a148174 --- /dev/null +++ b/src/views/monitor/redis/module/add.vue @@ -0,0 +1,73 @@ + + + + diff --git a/src/views/monitor/redis/module/edit.vue b/src/views/monitor/redis/module/edit.vue new file mode 100644 index 0000000..98e3d90 --- /dev/null +++ b/src/views/monitor/redis/module/edit.vue @@ -0,0 +1,89 @@ + + + + diff --git a/src/views/monitor/redis/module/search.vue b/src/views/monitor/redis/module/search.vue new file mode 100644 index 0000000..545f117 --- /dev/null +++ b/src/views/monitor/redis/module/search.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue new file mode 100644 index 0000000..ac9314b --- /dev/null +++ b/src/views/system/menu/index.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/src/views/system/menu/module/add.vue b/src/views/system/menu/module/add.vue new file mode 100644 index 0000000..d78414a --- /dev/null +++ b/src/views/system/menu/module/add.vue @@ -0,0 +1,129 @@ + + + + diff --git a/src/views/system/menu/module/edit.vue b/src/views/system/menu/module/edit.vue new file mode 100644 index 0000000..ac387f4 --- /dev/null +++ b/src/views/system/menu/module/edit.vue @@ -0,0 +1,144 @@ + + + + diff --git a/src/views/system/menu/module/search.vue b/src/views/system/menu/module/search.vue new file mode 100644 index 0000000..d28dcc5 --- /dev/null +++ b/src/views/system/menu/module/search.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/views/system/permission/index.vue b/src/views/system/permission/index.vue new file mode 100644 index 0000000..21fe7cd --- /dev/null +++ b/src/views/system/permission/index.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/views/system/permission/module/add.vue b/src/views/system/permission/module/add.vue new file mode 100644 index 0000000..929dbdb --- /dev/null +++ b/src/views/system/permission/module/add.vue @@ -0,0 +1,86 @@ + + + + diff --git a/src/views/system/permission/module/edit.vue b/src/views/system/permission/module/edit.vue new file mode 100644 index 0000000..cc46dc2 --- /dev/null +++ b/src/views/system/permission/module/edit.vue @@ -0,0 +1,98 @@ + + + + diff --git a/src/views/system/permission/module/search.vue b/src/views/system/permission/module/search.vue new file mode 100644 index 0000000..c26fc52 --- /dev/null +++ b/src/views/system/permission/module/search.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue new file mode 100644 index 0000000..fb3d1ca --- /dev/null +++ b/src/views/system/role/index.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/views/system/role/module/add.vue b/src/views/system/role/module/add.vue new file mode 100644 index 0000000..ea9c818 --- /dev/null +++ b/src/views/system/role/module/add.vue @@ -0,0 +1,88 @@ + + + + diff --git a/src/views/system/role/module/edit.vue b/src/views/system/role/module/edit.vue new file mode 100644 index 0000000..5034d6b --- /dev/null +++ b/src/views/system/role/module/edit.vue @@ -0,0 +1,105 @@ + + + + diff --git a/src/views/system/role/module/search.vue b/src/views/system/role/module/search.vue new file mode 100644 index 0000000..c42ae7d --- /dev/null +++ b/src/views/system/role/module/search.vue @@ -0,0 +1,64 @@ + + + diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue new file mode 100644 index 0000000..316105e --- /dev/null +++ b/src/views/system/user/index.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/views/system/user/module/add.vue b/src/views/system/user/module/add.vue new file mode 100644 index 0000000..cd39282 --- /dev/null +++ b/src/views/system/user/module/add.vue @@ -0,0 +1,101 @@ + + + + diff --git a/src/views/system/user/module/edit.vue b/src/views/system/user/module/edit.vue new file mode 100644 index 0000000..fa6c1d1 --- /dev/null +++ b/src/views/system/user/module/edit.vue @@ -0,0 +1,115 @@ + + + + diff --git a/src/views/system/user/module/search.vue b/src/views/system/user/module/search.vue new file mode 100644 index 0000000..7248d16 --- /dev/null +++ b/src/views/system/user/module/search.vue @@ -0,0 +1,79 @@ + + + diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29