commit c528bd8ad0e61884e219efcbf2b1832f7991dc7e Author: Pan Date: Mon Jun 26 13:38:24 2017 +0800 init diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..5e6838b --- /dev/null +++ b/.babelrc @@ -0,0 +1,7 @@ +{ + "presets": [ + ["env", { "modules": false }], + "stage-2" + ], + "plugins": ["transform-runtime"] +} 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..a388ba2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,318 @@ +module.exports = { + root: true, + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module' + }, + env: { + browser: true, + node: true + }, + extends: 'eslint:recommended', + // required to lint *.vue files + plugins: [ + 'html' + ], + // check if imports actually resolve + 'settings': { + 'import/resolver': { + 'webpack': { + 'config': 'build/webpack.base.conf.js' + } + } + }, + // add your custom rules here + 'rules': { + // don't require .vue extension when importing + // 'import/extensions': ['error', 'always', { + // 'js': 'never', + // 'vue': 'never' + // }], + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + /* + * Possible Errors + */ + + // disallow unnecessary parentheses + 'no-extra-parens': ['error', 'all', {'nestedBinaryExpressions': false}], + + // disallow negating the left operand of relational operators + 'no-unsafe-negation': 'error', + + // enforce valid JSDoc comments + 'valid-jsdoc': 'off', + + /* + * Best Practices + */ + + // enforce return statements in callbacks of array methods + 'array-callback-return': 'error', + + // enforce consistent brace style for all control statements + curly: ['error', 'multi-line'], + + // enforce consistent newlines before and after dots + 'dot-location': ['error', 'property'], + + // enforce dot notation whenever possible + 'dot-notation': 'error', + + // require the use of === and !== + 'eqeqeq': ['error', 'smart'], + + // disallow the use of arguments.caller or arguments.callee + 'no-caller': 'error', + + // disallow empty functions + 'no-empty-function': 'error', + + // disallow unnecessary calls to .bind() + 'no-extra-bind': 'error', + + // disallow unnecessary labels + 'no-extra-label': 'error', + + // disallow leading or trailing decimal points in numeric literals + 'no-floating-decimal': 'error', + + // disallow assignments to native objects or read-only global variables + 'no-global-assign': 'error', + + // disallow the use of eval()-like methods + 'no-implied-eval': 'error', + + // disallow the use of the __iterator__ property + 'no-iterator': 'error', + + // disallow unnecessary nested blocks + 'no-lone-blocks': 'error', + + // disallow multiple spaces + 'no-multi-spaces': 'error', + + // disallow new operators with the String, Number, and Boolean objects + 'no-new-wrappers': 'error', + + // disallow octal escape sequences in string literals + 'no-octal-escape': 'error', + + // disallow the use of the __proto__ property + 'no-proto': 'error', + + // disallow comparisons where both sides are exactly the same + 'no-self-compare': 'error', + + // disallow throwing literals as exceptions + 'no-throw-literal': 'error', + + // disallow unused expressions + 'no-unused-expressions': 'error', + + // disallow unnecessary calls to .call() and .apply() + 'no-useless-call': 'error', + + // disallow unnecessary concatenation of literals or template literals + 'no-useless-concat': 'error', + + // disallow unnecessary escape characters + 'no-useless-escape': 'error', + + // disallow void operators + 'no-void': 'error', + + // require parentheses around immediate function invocations + 'wrap-iife': 'error', + + // require or disallow “Yoda” conditions + yoda: 'error', + + /* + * Variables + */ + + // disallow labels that share a name with a variable + 'no-label-var': 'error', + + // disallow initializing variables to undefined + 'no-undef-init': 'error', + 'no-undef': 'off', + // disallow the use of variables before they are defined + 'no-use-before-define': 'error', + + /* + * Node.js and CommonJS + */ + + // disallow new operators with calls to require + 'no-new-require': 'error', + + /* + * Stylistic Issues + */ + + // enforce consistent spacing inside array brackets + 'array-bracket-spacing': 'error', + + // enforce consistent spacing inside single-line blocks + 'block-spacing': 'error', + + // enforce consistent brace style for blocks + 'brace-style': ['error', '1tbs', {'allowSingleLine': true}], + + // require or disallow trailing commas + 'comma-dangle': 'error', + + // enforce consistent spacing before and after commas + 'comma-spacing': 'error', + + // enforce consistent comma style + 'comma-style': 'error', + + // enforce consistent spacing inside computed property brackets + 'computed-property-spacing': 'error', + + // require or disallow spacing between function identifiers and their invocations + 'func-call-spacing': 'error', + + // enforce consistent indentation + indent: ['error', 2, {SwitchCase: 1}], + + // enforce the consistent use of either double or single quotes in JSX attributes + 'jsx-quotes': 'error', + + // enforce consistent spacing between keys and values in object literal properties + 'key-spacing': 'error', + + // enforce consistent spacing before and after keywords + 'keyword-spacing': 'error', + + // enforce consistent linebreak style + 'linebreak-style': 'error', + + // require or disallow newlines around directives + 'lines-around-directive': 'error', + + // require constructor names to begin with a capital letter + 'new-cap': 'off', + + // require parentheses when invoking a constructor with no arguments + 'new-parens': 'error', + + // disallow Array constructors + 'no-array-constructor': 'error', + + // disallow Object constructors + 'no-new-object': 'error', + + // disallow trailing whitespace at the end of lines + 'no-trailing-spaces': 'error', + + // disallow ternary operators when simpler alternatives exist + 'no-unneeded-ternary': 'error', + + // disallow whitespace before properties + 'no-whitespace-before-property': 'error', + + // enforce consistent spacing inside braces + 'object-curly-spacing': ['error', 'always'], + + // require or disallow padding within blocks + 'padded-blocks': ['error', 'never'], + + // require quotes around object literal property names + 'quote-props': ['error', 'as-needed'], + + // enforce the consistent use of either backticks, double, or single quotes + quotes: ['error', 'single'], + + // enforce consistent spacing before and after semicolons + 'semi-spacing': 'error', + + // require or disallow semicolons instead of ASI + // semi: ['error', 'never'], + + // enforce consistent spacing before blocks + 'space-before-blocks': 'error', + + 'no-console': 'off', + + // enforce consistent spacing before function definition opening parenthesis + 'space-before-function-paren': ['error', 'never'], + + // enforce consistent spacing inside parentheses + 'space-in-parens': 'error', + + // require spacing around infix operators + 'space-infix-ops': 'error', + + // enforce consistent spacing before or after unary operators + 'space-unary-ops': 'error', + + // enforce consistent spacing after the // or /* in a comment + 'spaced-comment': 'error', + + // require or disallow Unicode byte order mark (BOM) + 'unicode-bom': 'error', + + + /* + * ECMAScript 6 + */ + + // require braces around arrow function bodies + 'arrow-body-style': 'error', + + // require parentheses around arrow function arguments + 'arrow-parens': ['error', 'as-needed'], + + // enforce consistent spacing before and after the arrow in arrow functions + 'arrow-spacing': 'error', + + // enforce consistent spacing around * operators in generator functions + 'generator-star-spacing': ['error', 'after'], + + // disallow duplicate module imports + 'no-duplicate-imports': 'error', + + // disallow unnecessary computed property keys in object literals + 'no-useless-computed-key': 'error', + + // disallow unnecessary constructors + 'no-useless-constructor': 'error', + + // disallow renaming import, export, and destructured assignments to the same name + 'no-useless-rename': 'error', + + // require let or const instead of var + 'no-var': 'error', + + // require or disallow method and property shorthand syntax for object literals + 'object-shorthand': 'error', + + // require arrow functions as callbacks + 'prefer-arrow-callback': 'error', + + // require const declarations for variables that are never reassigned after declared + 'prefer-const': 'error', + + // disallow parseInt() in favor of binary, octal, and hexadecimal literals + 'prefer-numeric-literals': 'error', + + // require rest parameters instead of arguments + 'prefer-rest-params': 'error', + + // require spread operators instead of .apply() + 'prefer-spread': 'error', + + // enforce spacing between rest and spread operators and their expressions + 'rest-spread-spacing': 'error', + + // require or disallow spacing around embedded expressions of template strings + 'template-curly-spacing': 'error', + + // require or disallow spacing around the * in yield* expressions + 'yield-star-spacing': 'error' + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09552ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/.postcssrc.js b/.postcssrc.js new file mode 100644 index 0000000..ea9a5ab --- /dev/null +++ b/.postcssrc.js @@ -0,0 +1,8 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + "plugins": { + // to edit target browsers: use "browserlist" field in package.json + "autoprefixer": {} + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..8fbf707 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# vue-admin + +> A Vue.js project + +## Build Setup + +``` bash +# install dependencies +npm install + +# serve with hot reload at localhost:8080 +npm run dev + +# build for production with minification +npm run build + +# build for production and view the bundle analyzer report +npm run build --report +``` + +For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). diff --git a/build/build.js b/build/build.js new file mode 100644 index 0000000..6b8add1 --- /dev/null +++ b/build/build.js @@ -0,0 +1,35 @@ +require('./check-versions')() + +process.env.NODE_ENV = 'production' + +var ora = require('ora') +var rm = require('rimraf') +var path = require('path') +var chalk = require('chalk') +var webpack = require('webpack') +var config = require('../config') +var webpackConfig = require('./webpack.prod.conf') + +var spinner = ora('building for production...') +spinner.start() + +rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { + if (err) throw err + webpack(webpackConfig, function (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') + + 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..100f3a0 --- /dev/null +++ b/build/check-versions.js @@ -0,0 +1,48 @@ +var chalk = require('chalk') +var semver = require('semver') +var packageConfig = require('../package.json') +var shell = require('shelljs') +function exec (cmd) { + return require('child_process').execSync(cmd).toString().trim() +} + +var 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 () { + var warnings = [] + for (var i = 0; i < versionRequirements.length; i++) { + var 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 (var i = 0; i < warnings.length; i++) { + var warning = warnings[i] + console.log(' ' + warning) + } + console.log() + process.exit(1) + } +} diff --git a/build/dev-client.js b/build/dev-client.js new file mode 100644 index 0000000..18aa1e2 --- /dev/null +++ b/build/dev-client.js @@ -0,0 +1,9 @@ +/* eslint-disable */ +require('eventsource-polyfill') +var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') + +hotClient.subscribe(function (event) { + if (event.action === 'reload') { + window.location.reload() + } +}) diff --git a/build/dev-server.js b/build/dev-server.js new file mode 100644 index 0000000..782dc6f --- /dev/null +++ b/build/dev-server.js @@ -0,0 +1,89 @@ +require('./check-versions')() + +var config = require('../config') +if (!process.env.NODE_ENV) { + process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) +} + +var opn = require('opn') +var path = require('path') +var express = require('express') +var webpack = require('webpack') +var proxyMiddleware = require('http-proxy-middleware') +var webpackConfig = require('./webpack.dev.conf') + +// default port where dev server listens for incoming traffic +var port = process.env.PORT || config.dev.port +// automatically open browser, if not set will be false +var autoOpenBrowser = !!config.dev.autoOpenBrowser +// Define HTTP proxies to your custom API backend +// https://github.com/chimurai/http-proxy-middleware +var proxyTable = config.dev.proxyTable + +var app = express() +var compiler = webpack(webpackConfig) + +var devMiddleware = require('webpack-dev-middleware')(compiler, { + publicPath: webpackConfig.output.publicPath, + quiet: true +}) + +var hotMiddleware = require('webpack-hot-middleware')(compiler, { + log: () => {} +}) +// force page reload when html-webpack-plugin template changes +compiler.plugin('compilation', function (compilation) { + compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { + hotMiddleware.publish({ action: 'reload' }) + cb() + }) +}) + +// proxy api requests +Object.keys(proxyTable).forEach(function (context) { + var options = proxyTable[context] + if (typeof options === 'string') { + options = { target: options } + } + app.use(proxyMiddleware(options.filter || context, options)) +}) + +// handle fallback for HTML5 history API +app.use(require('connect-history-api-fallback')()) + +// serve webpack bundle output +app.use(devMiddleware) + +// enable hot-reload and state-preserving +// compilation error display +app.use(hotMiddleware) + +// serve pure static assets +var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) +app.use(staticPath, express.static('./static')) + +var uri = 'http://localhost:' + port + +var _resolve +var readyPromise = new Promise(resolve => { + _resolve = resolve +}) + +console.log('> Starting dev server...') +devMiddleware.waitUntilValid(() => { + console.log('> Listening at ' + uri + '\n') + // when env is testing, don't need open it + if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { + opn(uri) + } + _resolve() +}) + +var server = app.listen(port) + +module.exports = { + ready: readyPromise, + close: () => { + server.close() + } +} diff --git a/build/utils.js b/build/utils.js new file mode 100644 index 0000000..b1d54b4 --- /dev/null +++ b/build/utils.js @@ -0,0 +1,71 @@ +var path = require('path') +var config = require('../config') +var ExtractTextPlugin = require('extract-text-webpack-plugin') + +exports.assetsPath = function (_path) { + var assetsSubDirectory = process.env.NODE_ENV === 'production' + ? config.build.assetsSubDirectory + : config.dev.assetsSubDirectory + return path.posix.join(assetsSubDirectory, _path) +} + +exports.cssLoaders = function (options) { + options = options || {} + + var cssLoader = { + loader: 'css-loader', + options: { + minimize: process.env.NODE_ENV === 'production', + sourceMap: options.sourceMap + } + } + + // generate loader string to be used with extract text plugin + function generateLoaders (loader, loaderOptions) { + var loaders = [cssLoader] + if (loader) { + loaders.push({ + loader: loader + '-loader', + options: Object.assign({}, loaderOptions, { + sourceMap: options.sourceMap + }) + }) + } + + // Extract CSS when that option is specified + // (which is the case during production build) + if (options.extract) { + return ExtractTextPlugin.extract({ + use: loaders, + fallback: 'vue-style-loader' + }) + } else { + return ['vue-style-loader'].concat(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) { + var output = [] + var loaders = exports.cssLoaders(options) + for (var extension in loaders) { + var loader = loaders[extension] + output.push({ + test: new RegExp('\\.' + extension + '$'), + use: loader + }) + } + return output +} diff --git a/build/vue-loader.conf.js b/build/vue-loader.conf.js new file mode 100644 index 0000000..7aee79b --- /dev/null +++ b/build/vue-loader.conf.js @@ -0,0 +1,12 @@ +var utils = require('./utils') +var config = require('../config') +var isProduction = process.env.NODE_ENV === 'production' + +module.exports = { + loaders: utils.cssLoaders({ + sourceMap: isProduction + ? config.build.productionSourceMap + : config.dev.cssSourceMap, + extract: isProduction + }) +} diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js new file mode 100644 index 0000000..30817ad --- /dev/null +++ b/build/webpack.base.conf.js @@ -0,0 +1,67 @@ +var path = require('path') +var utils = require('./utils') +var config = require('../config') +var vueLoaderConfig = require('./vue-loader.conf') + +function resolve (dir) { + return path.join(__dirname, '..', dir) +} + +module.exports = { + 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: { + 'vue$': 'vue/dist/vue.esm.js', + '@': resolve('src') + } + }, + module: { + rules: [ + // { + // test: /\.(js|vue)$/, + // loader: 'eslint-loader', + // enforce: 'pre', + // include: [resolve('src'), resolve('test')], + // options: { + // formatter: require('eslint-friendly-formatter') + // } + // }, + { + test: /\.vue$/, + loader: 'vue-loader', + options: vueLoaderConfig + }, + { + test: /\.js$/, + loader: 'babel-loader', + include: [resolve('src'), resolve('test')] + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('img/[name].[hash:7].[ext]') + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('fonts/[name].[hash:7].[ext]') + } + } + ] + } +} diff --git a/build/webpack.dev.conf.js b/build/webpack.dev.conf.js new file mode 100644 index 0000000..5470402 --- /dev/null +++ b/build/webpack.dev.conf.js @@ -0,0 +1,35 @@ +var utils = require('./utils') +var webpack = require('webpack') +var config = require('../config') +var merge = require('webpack-merge') +var baseWebpackConfig = require('./webpack.base.conf') +var HtmlWebpackPlugin = require('html-webpack-plugin') +var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') + +// add hot-reload related code to entry chunks +Object.keys(baseWebpackConfig.entry).forEach(function (name) { + baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) +}) + +module.exports = merge(baseWebpackConfig, { + module: { + rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) + }, + // cheap-module-eval-source-map is faster for development + devtool: '#cheap-module-eval-source-map', + plugins: [ + new webpack.DefinePlugin({ + 'process.env': config.dev.env + }), + // https://github.com/glenjamin/webpack-hot-middleware#installation--usage + new webpack.HotModuleReplacementPlugin(), + new webpack.NoEmitOnErrorsPlugin(), + // https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: 'index.html', + template: 'index.html', + inject: true + }), + new FriendlyErrorsPlugin() + ] +}) diff --git a/build/webpack.prod.conf.js b/build/webpack.prod.conf.js new file mode 100644 index 0000000..da44b65 --- /dev/null +++ b/build/webpack.prod.conf.js @@ -0,0 +1,120 @@ +var path = require('path') +var utils = require('./utils') +var webpack = require('webpack') +var config = require('../config') +var merge = require('webpack-merge') +var baseWebpackConfig = require('./webpack.base.conf') +var CopyWebpackPlugin = require('copy-webpack-plugin') +var HtmlWebpackPlugin = require('html-webpack-plugin') +var ExtractTextPlugin = require('extract-text-webpack-plugin') +var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') + +var env = config.build.env + +var webpackConfig = merge(baseWebpackConfig, { + module: { + rules: utils.styleLoaders({ + sourceMap: config.build.productionSourceMap, + extract: true + }) + }, + devtool: config.build.productionSourceMap ? '#source-map' : false, + output: { + path: config.build.assetsRoot, + filename: utils.assetsPath('js/[name].[chunkhash].js'), + chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') + }, + plugins: [ + // http://vuejs.github.io/vue-loader/en/workflow/production.html + new webpack.DefinePlugin({ + 'process.env': env + }), + new webpack.optimize.UglifyJsPlugin({ + compress: { + warnings: false + }, + sourceMap: true + }), + // extract css into its own file + new ExtractTextPlugin({ + filename: utils.assetsPath('css/[name].[contenthash].css') + }), + // Compress extracted CSS. We are using this plugin so that possible + // duplicated CSS from different components can be deduped. + new OptimizeCSSPlugin({ + cssProcessorOptions: { + safe: true + } + }), + // 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, + minify: { + removeComments: true, + collapseWhitespace: true, + removeAttributeQuotes: true + // more options: + // https://github.com/kangax/html-minifier#options-quick-reference + }, + // necessary to consistently work with multiple chunks via CommonsChunkPlugin + chunksSortMode: 'dependency' + }), + // split vendor js into its own file + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks: function (module, count) { + // any required modules inside node_modules are extracted to vendor + return ( + module.resource && + /\.js$/.test(module.resource) && + module.resource.indexOf( + path.join(__dirname, '../node_modules') + ) === 0 + ) + } + }), + // extract webpack runtime and module manifest to its own file in order to + // prevent vendor hash from being updated whenever app bundle is updated + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest', + chunks: ['vendor'] + }), + // copy custom static assets + new CopyWebpackPlugin([ + { + from: path.resolve(__dirname, '../static'), + to: config.build.assetsSubDirectory, + ignore: ['.*'] + } + ]) + ] +}) + +if (config.build.productionGzip) { + var 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.bundleAnalyzerReport) { + var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin + webpackConfig.plugins.push(new BundleAnalyzerPlugin()) +} + +module.exports = webpackConfig diff --git a/config/dev.env.js b/config/dev.env.js new file mode 100644 index 0000000..efead7c --- /dev/null +++ b/config/dev.env.js @@ -0,0 +1,6 @@ +var merge = require('webpack-merge') +var prodEnv = require('./prod.env') + +module.exports = merge(prodEnv, { + NODE_ENV: '"development"' +}) diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..b945486 --- /dev/null +++ b/config/index.js @@ -0,0 +1,38 @@ +// see http://vuejs-templates.github.io/webpack for documentation. +var path = require('path') + +module.exports = { + build: { + env: require('./prod.env'), + index: path.resolve(__dirname, '../dist/index.html'), + assetsRoot: path.resolve(__dirname, '../dist'), + assetsSubDirectory: 'static', + assetsPublicPath: '/', + productionSourceMap: true, + // 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 + }, + dev: { + env: require('./dev.env'), + port: 9528, + autoOpenBrowser: true, + assetsSubDirectory: 'static', + assetsPublicPath: '/', + proxyTable: {}, + // 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 + } +} diff --git a/config/prod.env.js b/config/prod.env.js new file mode 100644 index 0000000..773d263 --- /dev/null +++ b/config/prod.env.js @@ -0,0 +1,3 @@ +module.exports = { + NODE_ENV: '"production"' +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..9b30275 --- /dev/null +++ b/index.html @@ -0,0 +1,11 @@ + + + + + vue-admin + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..2bdff76 --- /dev/null +++ b/package.json @@ -0,0 +1,74 @@ +{ + "name": "vue-admin", + "version": "1.0.0", + "description": "A Vue.js project", + "author": "Pan ", + "private": true, + "scripts": { + "dev": "node build/dev-server.js", + "start": "node build/dev-server.js", + "build": "node build/build.js", + "lint": "eslint --ext .js,.vue src" + }, + "dependencies": { + "axios": "0.16.2", + "element-ui": "1.3.7", + "js-cookie": "^2.1.4", + "normalize.css": "3.0.2", + "nprogress": "^0.2.0", + "vue": "2.3.3", + "vue-router": "2.5.3", + "vuex": "2.3.1" + }, + "devDependencies": { + "autoprefixer": "6.7.2", + "babel-core": "6.22.1", + "babel-eslint": "7.1.1", + "babel-loader": "6.2.10", + "babel-plugin-transform-runtime": "6.22.0", + "babel-preset-env": "1.3.2", + "babel-preset-stage-2": "6.22.0", + "babel-register": "6.22.0", + "chalk": "1.1.3", + "connect-history-api-fallback": "1.3.0", + "copy-webpack-plugin": "4.0.1", + "css-loader": "0.28.0", + "eslint": "3.19.0", + "eslint-friendly-formatter": "2.0.7", + "eslint-loader": "1.7.1", + "eslint-plugin-html": "2.0.0", + "eventsource-polyfill": "0.9.6", + "express": "4.14.1", + "extract-text-webpack-plugin": "2.0.0", + "file-loader": "0.11.1", + "friendly-errors-webpack-plugin": "1.1.3", + "html-webpack-plugin": "2.28.0", + "http-proxy-middleware": "0.17.3", + "webpack-bundle-analyzer": "2.2.1", + "semver": "5.3.0", + "shelljs": "0.7.6", + "opn": "4.0.2", + "optimize-css-assets-webpack-plugin": "1.3.0", + "ora": "1.2.0", + "rimraf": "2.6.0", + "node-sass": "^4.5.0", + "sass-loader": "6.0.5", + "url-loader": "0.5.8", + "vue-loader": "12.1.0", + "vue-style-loader": "3.0.1", + "vue-template-compiler": "2.3.3", + "webpack": "2.6.1", + "webpack-dev-middleware": "1.10.0", + "webpack-hot-middleware": "2.18.0", + "webpack-merge": "4.1.0" + }, + "engines": { + "node": ">= 4.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..e917f19 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,12 @@ + + + + diff --git a/src/api/login.js b/src/api/login.js new file mode 100644 index 0000000..990498d --- /dev/null +++ b/src/api/login.js @@ -0,0 +1,29 @@ +import fetch from '@/utils/fetch'; + +export function loginByEmail(email, password) { + const data = { + email, + password + }; + return fetch({ + url: '/login/loginbyemail', + method: 'post', + data + }); +} + +export function logout() { + return fetch({ + url: '/login/logout', + method: 'post' + }); +} + +export function getInfo(token) { + return fetch({ + url: '/user/info', + method: 'get', + params: { token } + }); +} + 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/assets/iconfont/iconfont.js b/src/assets/iconfont/iconfont.js new file mode 100644 index 0000000..11a01bd --- /dev/null +++ b/src/assets/iconfont/iconfont.js @@ -0,0 +1 @@ +(function(window){var svgSprite=""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+''+""+''+""+""+""+"";var script=function(){var scripts=document.getElementsByTagName("script");return scripts[scripts.length-1]}();var shouldInjectCss=script.getAttribute("data-injectcss");var ready=function(fn){if(document.addEventListener){if(~["complete","loaded","interactive"].indexOf(document.readyState)){setTimeout(fn,0)}else{var loadFn=function(){document.removeEventListener("DOMContentLoaded",loadFn,false);fn()};document.addEventListener("DOMContentLoaded",loadFn,false)}}else if(document.attachEvent){IEContentLoaded(window,fn)}function IEContentLoaded(w,fn){var d=w.document,done=false,init=function(){if(!done){done=true;fn()}};var polling=function(){try{d.documentElement.doScroll("left")}catch(e){setTimeout(polling,50);return}init()};polling();d.onreadystatechange=function(){if(d.readyState=="complete"){d.onreadystatechange=null;init()}}}};var before=function(el,target){target.parentNode.insertBefore(el,target)};var prepend=function(el,target){if(target.firstChild){before(el,target.firstChild)}else{target.appendChild(el)}};function appendSvg(){var div,svg;div=document.createElement("div");div.innerHTML=svgSprite;svgSprite=null;svg=div.getElementsByTagName("svg")[0];if(svg){svg.setAttribute("aria-hidden","true");svg.style.position="absolute";svg.style.width=0;svg.style.height=0;svg.style.overflow="hidden";prepend(svg,document.body)}}if(shouldInjectCss&&!window.__iconfont__svg__cssinject__){window.__iconfont__svg__cssinject__=true;try{document.write("")}catch(e){console&&console.log(e)}}ready(appendSvg)})(window) \ No newline at end of file diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue new file mode 100644 index 0000000..fc33037 --- /dev/null +++ b/src/components/Hamburger/index.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/components/Icon-svg/index.js b/src/components/Icon-svg/index.js new file mode 100644 index 0000000..55d342a --- /dev/null +++ b/src/components/Icon-svg/index.js @@ -0,0 +1,11 @@ +import Vue from 'vue' + +function registerAllComponents(requireContext) { + return requireContext.keys().forEach(comp => { + const vueComp = requireContext(comp) + const compName = vueComp.name ? vueComp.name.toLowerCase() : /\/([\w-]+)\.vue$/.exec(comp)[1] + Vue.component(compName, vueComp) + }) +} + +registerAllComponents(require.context('./', false, /\.vue$/)) diff --git a/src/components/Icon-svg/wscn-icon-stack.vue b/src/components/Icon-svg/wscn-icon-stack.vue new file mode 100644 index 0000000..bf7b07e --- /dev/null +++ b/src/components/Icon-svg/wscn-icon-stack.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/components/Icon-svg/wscn-icon-svg.vue b/src/components/Icon-svg/wscn-icon-svg.vue new file mode 100644 index 0000000..04b01f4 --- /dev/null +++ b/src/components/Icon-svg/wscn-icon-svg.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..15e7ae0 --- /dev/null +++ b/src/main.js @@ -0,0 +1,66 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import App from './App' +import router from './router' +import store from './store' +import ElementUI from 'element-ui'; +import 'element-ui/lib/theme-default/index.css' +import NProgress from 'nprogress' +import 'normalize.css/normalize.css';// normalize.css 样式格式化 +import '@/styles/index.scss'; // 全局自定义的css样式 +import '@/components/Icon-svg/index'; // 封装的svg组件 + +Vue.config.productionTip = false + +Vue.use(ElementUI); + +router.afterEach(() => { + NProgress.done(); // 结束Progress +}); + + +// const whiteList = ['/login', '/authredirect', '/reset', '/sendpwd'];// 不重定向白名单 +// router.beforeEach((to, from, next) => { +// NProgress.start(); // 开启Progress +// if (store.getters.token) { // 判断是否有token +// if (to.path === '/login') { +// next({ path: '/' }); +// } else { +// if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 +// store.dispatch('GetInfo').then(res => { // 拉取user_info +// const roles = res.data.role; +// store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表 +// router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表 +// next(to.path); // hack方法 确保addRoutes已完成 +// }) +// }).catch(err => { +// console.log(err); +// }); +// } else { +// // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ +// if (hasPermission(store.getters.roles, to.meta.role)) { +// next();// +// } else { +// next({ path: '/401', query: { noGoBack: true } }); +// } +// // 可删 ↑ +// } +// } +// } else { +// if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 +// next() +// } else { +// next('/login'); // 否则全部重定向到登录页 +// NProgress.done(); // 在hash模式下 改变手动改变hash 重定向回来 不会触发afterEach 暂时hack方案 ps:history模式下无问题,可删除该行! +// } +// } +// }); + +new Vue({ + el: '#app', + router, + store, + template: '', + components: { App } +}) diff --git a/src/router/_import_development.js b/src/router/_import_development.js new file mode 100644 index 0000000..dfa4bb9 --- /dev/null +++ b/src/router/_import_development.js @@ -0,0 +1 @@ +module.exports = file => require('@/views/' + file + '.vue') diff --git a/src/router/_import_production.js b/src/router/_import_production.js new file mode 100644 index 0000000..331acba --- /dev/null +++ b/src/router/_import_production.js @@ -0,0 +1 @@ +module.exports = file => () => import('@/views/' + file + '.vue') diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..899f8fc --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,60 @@ +import Vue from 'vue'; +import Router from 'vue-router'; +const _import = require('./_import_' + process.env.NODE_ENV); +// in development env not use Lazy Loading,because Lazy Loading large page will cause webpack hot update too slow +// so only in production use Lazy Loading + +/* layout */ +import Layout from '../views/layout/Layout'; + +/* login */ +const Login = _import('login/index'); + +/* dashboard */ +const dashboard = _import('dashboard/index'); + +/* error page */ +const Err404 = _import('404'); + +const Page = _import('page/index'); + +Vue.use(Router); + + /** + * icon : the icon show in the sidebar + * hidden : if hidden:true will not show in the sidebar + * redirect : if redirect:noredirect will not redirct in the levelbar + * noDropdown : if noDropdown:true will not has submenu + * meta : { role: ['admin'] } will control the page role + **/ +export const constantRouterMap = [ + { path: '/404', component: Err404, hidden: true }, + { + path: '/', + component: Layout, + redirect: '/dashboard', + name: '首页', + hidden: true, + children: [{ path: 'dashboard', component: dashboard }] + } +] + +export default new Router({ + // mode: 'history', //后端支持可开 + scrollBehavior: () => ({ y: 0 }), + routes: constantRouterMap +}); + +export const asyncRouterMap = [ + { + path: '/example', + component: Layout, + redirect: 'noredirect', + name: 'page', + icon: 'zonghe', + children: [ + { path: 'index', component: Page, name: 'page' } + ] + }, + { path: '*', redirect: '/404', hidden: true } +]; diff --git a/src/store/getters.js b/src/store/getters.js new file mode 100644 index 0000000..71c9e6b --- /dev/null +++ b/src/store/getters.js @@ -0,0 +1,14 @@ +const getters = { + sidebar: state => state.app.sidebar, + visitedViews: state => state.app.visitedViews, + token: state => state.user.token, + avatar: state => state.user.avatar, + name: state => state.user.name, + uid: state => state.user.uid, + email: state => state.user.email, + introduction: state => state.user.introduction, + 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..ee7d313 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import app from './modules/app'; +import user from './modules/user'; +import permission from './modules/permission'; +import getters from './getters'; + +Vue.use(Vuex); + +const store = new Vuex.Store({ + modules: { + app, + user, + 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..0087174 --- /dev/null +++ b/src/store/modules/app.js @@ -0,0 +1,26 @@ +import Cookies from 'js-cookie'; + +const app = { + state: { + sidebar: { + opened: !+Cookies.get('sidebarStatus') + } + }, + mutations: { + TOGGLE_SIDEBAR: state => { + if (state.sidebar.opened) { + Cookies.set('sidebarStatus', 1); + } else { + Cookies.set('sidebarStatus', 0); + } + state.sidebar.opened = !state.sidebar.opened; + } + }, + actions: { + ToggleSideBar: ({ commit }) => { + commit('TOGGLE_SIDEBAR') + } + } +}; + +export default app; diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js new file mode 100644 index 0000000..ef2b347 --- /dev/null +++ b/src/store/modules/permission.js @@ -0,0 +1,62 @@ +import { asyncRouterMap, constantRouterMap } from '@/router/index'; + +/** + * 通过meta.role判断是否与当前用户权限匹配 + * @param roles + * @param route + */ +function hasPermission(roles, route) { + if (route.meta && route.meta.role) { + return roles.some(role => route.meta.role.indexOf(role) >= 0) + } else { + return true + } +} + +/** + * 递归过滤异步路由表,返回符合用户角色权限的路由表 + * @param asyncRouterMap + * @param roles + */ +function filterAsyncRouter(asyncRouterMap, roles) { + const accessedRouters = asyncRouterMap.filter(route => { + if (hasPermission(roles, route)) { + if (route.children && route.children.length) { + route.children = filterAsyncRouter(route.children, roles) + } + return true + } + return false + }) + return accessedRouters +} + +const permission = { + state: { + routers: constantRouterMap, + addRouters: [] + }, + mutations: { + SET_ROUTERS: (state, routers) => { + state.addRouters = routers; + state.routers = constantRouterMap.concat(routers); + } + }, + actions: { + GenerateRoutes({ commit }, data) { + return new Promise(resolve => { + const { roles } = data + let accessedRouters + if (roles.indexOf('admin') >= 0) { + accessedRouters = asyncRouterMap + } else { + accessedRouters = filterAsyncRouter(asyncRouterMap, roles) + } + commit('SET_ROUTERS', accessedRouters); + resolve(); + }) + } + } +}; + +export default permission; diff --git a/src/store/modules/user.js b/src/store/modules/user.js new file mode 100644 index 0000000..5437818 --- /dev/null +++ b/src/store/modules/user.js @@ -0,0 +1,149 @@ +import { loginByEmail, logout, getInfo } from '@/api/login'; +import Cookies from 'js-cookie'; + +const user = { + state: { + user: '', + status: '', + email: '', + code: '', + uid: undefined, + auth_type: '', + token: Cookies.get('Admin-Token'), + name: '', + avatar: '', + introduction: '', + roles: [], + setting: { + articlePlatform: [] + } + }, + + mutations: { + SET_AUTH_TYPE: (state, type) => { + state.auth_type = type; + }, + SET_CODE: (state, code) => { + state.code = code; + }, + SET_TOKEN: (state, token) => { + state.token = token; + }, + SET_UID: (state, uid) => { + state.uid = uid; + }, + SET_EMAIL: (state, email) => { + state.email = email; + }, + SET_INTRODUCTION: (state, introduction) => { + state.introduction = introduction; + }, + SET_SETTING: (state, setting) => { + state.setting = setting; + }, + SET_STATUS: (state, status) => { + state.status = status; + }, + SET_NAME: (state, name) => { + state.name = name; + }, + SET_AVATAR: (state, avatar) => { + state.avatar = avatar; + }, + SET_ROLES: (state, roles) => { + state.roles = roles; + }, + LOGIN_SUCCESS: () => { + console.log('login success') + }, + LOGOUT_USER: state => { + state.user = ''; + } + }, + + actions: { + // 邮箱登录 + LoginByEmail({ commit }, userInfo) { + const email = userInfo.email.trim(); + return new Promise((resolve, reject) => { + loginByEmail(email, userInfo.password).then(response => { + const data = response.data; + Cookies.set('Admin-Token', response.data.token); + commit('SET_TOKEN', data.token); + commit('SET_EMAIL', email); + resolve(); + }).catch(error => { + reject(error); + }); + }); + }, + + + // 获取用户信息 + GetInfo({ commit, state }) { + return new Promise((resolve, reject) => { + getInfo(state.token).then(response => { + const data = response.data; + commit('SET_ROLES', data.role); + commit('SET_NAME', data.name); + commit('SET_AVATAR', data.avatar); + commit('SET_UID', data.uid); + commit('SET_INTRODUCTION', data.introduction); + resolve(response); + }).catch(error => { + reject(error); + }); + }); + }, + + // 第三方验证登录 + LoginByThirdparty({ commit, state }, code) { + return new Promise((resolve, reject) => { + commit('SET_CODE', code); + loginByThirdparty(state.status, state.email, state.code, state.auth_type).then(response => { + commit('SET_TOKEN', response.data.token); + Cookies.set('Admin-Token', response.data.token); + resolve(); + }).catch(error => { + reject(error); + }); + }); + }, + + + // 登出 + LogOut({ commit, state }) { + return new Promise((resolve, reject) => { + logout(state.token).then(() => { + commit('SET_TOKEN', ''); + commit('SET_ROLES', []); + Cookies.remove('Admin-Token'); + resolve(); + }).catch(error => { + reject(error); + }); + }); + }, + + // 前端 登出 + FedLogOut({ commit }) { + return new Promise(resolve => { + commit('SET_TOKEN', ''); + Cookies.remove('Admin-Token'); + resolve(); + }); + }, + + // 动态修改权限 + ChangeRole({ commit }, role) { + return new Promise(resolve => { + commit('SET_ROLES', [role]); + commit('SET_TOKEN', role); + Cookies.set('Admin-Token', role); + resolve(); + }) + } + } +}; + +export default user; diff --git a/src/styles/btn.scss b/src/styles/btn.scss new file mode 100644 index 0000000..edd2f31 --- /dev/null +++ b/src/styles/btn.scss @@ -0,0 +1,103 @@ +$blue:#324157; +$light-blue:#3A71A8; +$red:#C03639; +$pink: #E65D6E; +$green: #30B08F; +$tiffany: #4AB7BD; +$yellow:#FEC171; + +$panGreen: #30B08F; + +@mixin colorBtn($color) { + background: $color; + &:hover { + color: $color; + &:before, &:after { + background: $color; + } + } +} + + +.blue-btn { + @include colorBtn($blue) +} + +.light-blue-btn{ + @include colorBtn($light-blue) +} + + +.red-btn { + @include colorBtn($red) +} + +.pink-btn { + @include colorBtn($pink) +} + +.green-btn { + @include colorBtn($green) +} + + +.tiffany-btn { + @include colorBtn($tiffany) +} + + +.yellow-btn { + @include colorBtn($yellow) +} + +.pan-btn { + font-size: 14px; + color: #fff; + padding: 14px 36px; + border-radius: 8px; + border: none; + outline: none; + margin-right: 25px; + transition: 600ms ease all; + position: relative; + display: inline-block; + &:hover { + background: #fff; + &:before, &:after { + width: 100%; + transition: 600ms ease all; + } + } + &:before, &:after { + content: ''; + position: absolute; + top: 0; + right: 0; + height: 2px; + width: 0; + transition: 400ms ease all; + } + &::after { + right: inherit; + top: inherit; + left: 0; + bottom: 0; + } +} + +.custom-button{ + display: inline-block; + line-height: 1; + white-space: nowrap; + cursor: pointer; + background: #fff; + color: #fff; + -webkit-appearance: none; + text-align: center; + box-sizing: border-box; + outline: 0; + margin: 0; + padding: 10px 15px; + font-size: 14px; + border-radius: 4px; +} diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss new file mode 100644 index 0000000..382aff1 --- /dev/null +++ b/src/styles/element-ui.scss @@ -0,0 +1,83 @@ + //覆盖一些element-ui样式 +.block-checkbox { + display: block; +} + +.operation-container { + .cell { + padding: 10px !important; + } + .el-button { + &:nth-child(3) { + margin-top: 10px; + margin-left: 0px; + } + &:nth-child(4) { + margin-top: 10px; + } + } +} + +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + +.cell { + .el-tag { + margin-right: 8px; + } +} + +.small-padding { + .cell { + padding-left: 8px; + padding-right: 8px; + } +} + +.status-col { + .cell { + padding: 0 10px; + text-align: center; + .el-tag { + margin-right: 0px; + } + } +} + +//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + + +//文章页textarea修改样式 +.article-textarea { + textarea { + padding-right: 40px; + resize: none; + border: none; + border-radius: 0px; + border-bottom: 1px solid #bfcbd9; + } +} + +//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..e566c38 --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,266 @@ +@import './btn.scss'; +@import './element-ui.scss'; +@import "./mixin.scss"; +body { + -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 { + box-sizing: border-box; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +.no-padding { + padding: 0px !important; +} + +.padding-content { + padding: 4px 0; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +.fr { + float: right; +} + +.fl { + float: left; +} + +.pr-5 { + padding-right: 5px; +} + +.pl-5 { + padding-left: 5px; +} + +.block { + display: block; +} + +.pointer { + cursor: pointer; +} + +.inlineBlock { + display: block; +} + +code { + background: #eef1f6; + padding: 15px 10px; + margin-bottom: 20px; + display: block; + line-height: 36px; + a { + color: #337ab7; + cursor: pointer; + &:hover { + color: rgb(32, 160, 255); + } + } +} + +.fade-enter-active, +.fade-leave-active { + transition: all .2s ease +} + +.fade-enter, +.fade-leave-active { + opacity: 0; +} + +//main-container全局样式 +.app-container { + padding: 20px; +} +.components-container { + margin: 30px 50px; + position: relative; +} +.pagination-container { + margin-top: 30px; +} + + +.editor-container .CodeMirror { + height: 100%!important; +} + +.wscn-icon { + width: 1em; + height: 1em; + vertical-align: -0.15em; + fill: currentColor; + overflow: hidden; +} + +.sub-navbar { + height: 50px; + line-height: 50px; + position: relative; + width: 100%; + text-align: right; + padding-right: 20px; + transition: 600ms ease position; + background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%); + .subtitle { + font-size: 20px; + color: #fff; + } + &.draft { + background: #d0d0d0; + } + &.deleted { + background: #d0d0d0; + } +} + +.link-type, +.link-type:focus { + color: #337ab7; + cursor: pointer; + &:hover { + color: rgb(32, 160, 255); + } +} + +.publishedTag, +.draftTag, +.deletedTag { + color: #fff; + background-color: $panGreen; + line-height: 1; + text-align: center; + margin: 0; + padding: 8px 12px; + font-size: 14px; + border-radius: 4px; + position: absolute; + left: 20px; + top: 10px; +} + +.draftTag { + background-color: $yellow; +} + +.deletedTag { + background-color: $red; +} + +.input-label { + font-size: 14px; + color: #48576a; + line-height: 1; + padding: 11px 5px 11px 0; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +.no-marginLeft { + .el-checkbox { + margin: 0 20px 15px 0; + } + .el-checkbox+.el-checkbox { + margin-left: 0px; + } +} + +.filter-container { + padding-bottom: 10px; + .filter-item { + display: inline-block; + vertical-align: middle; + margin-bottom: 10px; + } +} + + +//refine vue-multiselect plugin +.multiselect { + line-height: 16px; +} + +.multiselect--active { + z-index: 1000 !important; +} + +//refine simplemde +.simplemde-container{ + .editor-toolbar.fullscreen,.CodeMirror-fullscreen{ + z-index: 1003; + } +} + +//暂时性解决diolag 问题 https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + +//github-corner +.github-corner:hover .octo-arm { + animation: octocat-wave 560ms ease-in-out +} + +@keyframes octocat-wave { + 0%, + 100% { + transform: rotate(0) + } + 20%, + 60% { + transform: rotate(-25deg) + } + 40%, + 80% { + transform: rotate(10deg) + } +} + +@media (max-width:500px) { + .github-corner:hover .octo-arm { + animation: none + } + .github-corner .octo-arm { + animation: octocat-wave 560ms ease-in-out + } +} diff --git a/src/styles/mixin.scss b/src/styles/mixin.scss new file mode 100644 index 0000000..822ab92 --- /dev/null +++ b/src/styles/mixin.scss @@ -0,0 +1,60 @@ +@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%; +} + +@mixin pct($pct) { + width: #{$pct}; + position: relative; + margin: 0 auto; +} + +@mixin triangle($width, $height, $color, $direction) { + $width: $width/2; + $color-border-style: $height solid $color; + $transparent-border-style: $width solid transparent; + height: 0; + width: 0; + @if $direction==up { + border-bottom: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + @else if $direction==right { + border-left: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } + @else if $direction==down { + border-top: $color-border-style; + border-left: $transparent-border-style; + border-right: $transparent-border-style; + } + @else if $direction==left { + border-right: $color-border-style; + border-top: $transparent-border-style; + border-bottom: $transparent-border-style; + } +} diff --git a/src/utils/createUniqueString.js b/src/utils/createUniqueString.js new file mode 100644 index 0000000..2e6e357 --- /dev/null +++ b/src/utils/createUniqueString.js @@ -0,0 +1,8 @@ +/** + * Created by jiachenpan on 17/3/8. + */ +export default function createUniqueString() { + const timestamp = +new Date() + ''; + const randomNum = parseInt((1 + Math.random()) * 65536) + ''; + return (+(randomNum + timestamp)).toString(32); +} diff --git a/src/utils/fetch.js b/src/utils/fetch.js new file mode 100644 index 0000000..43caa09 --- /dev/null +++ b/src/utils/fetch.js @@ -0,0 +1,66 @@ +import axios from 'axios'; +import { Message } from 'element-ui'; +import store from '../store'; +// import router from '../router'; + +// 创建axios实例 +const service = axios.create({ + baseURL: process.env.BASE_API, // api的base_url + timeout: 5000 // 请求超时时间 +}); + +// request拦截器 +service.interceptors.request.use(config => { + // Do something before request is sent + if (store.getters.token) { + config.headers['X-Token'] = store.getters.token; // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 + } + return config; +}, error => { + // Do something with request error + console.log(error); // for debug + Promise.reject(error); +}) + +// respone拦截器 +service.interceptors.response.use( + response => response, + /** + * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 + * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 + */ +// const res = response.data; +// if (res.code !== 20000) { +// Message({ +// message: res.message, +// type: 'error', +// duration: 5 * 1000 +// }); +// // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了; +// if (res.code === 50008 || res.code === 50012 || res.code === 50014) { +// MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', { +// confirmButtonText: '重新登录', +// cancelButtonText: '取消', +// type: 'warning' +// }).then(() => { +// store.dispatch('FedLogOut').then(() => { +// location.reload();// 为了重新实例化vue-router对象 避免bug +// }); +// }) +// } +// return Promise.reject(error); +// } else { +// return response.data; +// } + error => { + console.log('err' + error);// for debug + Message({ + message: error.message, + type: 'error', + duration: 5 * 1000 + }); + return Promise.reject(error); + } +) + +export default service; diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..a011f17 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,214 @@ +/** + * Created by jiachenpan on 16/11/18. + */ + + export function parseTime(time, cFormat) { + if (arguments.length === 0) { + return null; + } + const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'; + let date; + if (typeof time == 'object') { + date = time; + } else { + if (('' + time).length === 10) time = parseInt(time) * 1000; + date = new Date(time); + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + }; + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key]; + if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1]; + if (result.length > 0 && value < 10) { + value = '0' + value; + } + return value || 0; + }); + return time_str; + } + + 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 getQueryObject(url) { + url = url == null ? window.location.href : url; + const search = url.substring(url.lastIndexOf('?') + 1); + const obj = {}; + const reg = /([^?&=]+)=([^?&=]*)/g; + search.replace(reg, (rs, $1, $2) => { + const name = decodeURIComponent($1); + let val = decodeURIComponent($2); + val = String(val); + obj[name] = val; + return rs; + }); + return obj; + } + + +/** + *get getByteLen + * @param {Sting} val input value + * @returns {number} output value + */ + export function getByteLen(val) { + let len = 0; + for (let i = 0; i < val.length; i++) { + if (val[i].match(/[^\x00-\xff]/ig) != null) { + len += 1; + } else { len += 0.5; } + } + return Math.floor(len); + } + + export function cleanArray(actual) { + const newArray = []; + for (let i = 0; i < actual.length; i++) { + if (actual[i]) { + newArray.push(actual[i]); + } + } + return newArray; + } + + export function param(json) { + if (!json) return ''; + return cleanArray(Object.keys(json).map(key => { + if (json[key] === undefined) return ''; + return encodeURIComponent(key) + '=' + + encodeURIComponent(json[key]); + })).join('&'); + } + + export function param2Obj(url) { + const search = url.split('?')[1]; + return JSON.parse('{"' + decodeURIComponent(search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') + '"}') + } + + export function html2Text(val) { + const div = document.createElement('div'); + div.innerHTML = val; + return div.textContent || div.innerText; + } + + export function objectMerge(target, source) { + /* Merges two objects, + giving the last one precedence */ + + if (typeof target !== 'object') { + target = {}; + } + if (Array.isArray(source)) { + return source.slice(); + } + for (const property in source) { + if (source.hasOwnProperty(property)) { + const sourceProperty = source[property]; + if (typeof sourceProperty === 'object') { + target[property] = objectMerge(target[property], sourceProperty); + continue; + } + target[property] = sourceProperty; + } + } + return target; + } + + + export function scrollTo(element, to, duration) { + if (duration <= 0) return; + const difference = to - element.scrollTop; + const perTick = difference / duration * 10; + setTimeout(() => { + console.log(new Date()) + element.scrollTop = element.scrollTop + perTick; + if (element.scrollTop === to) return; + scrollTo(element, to, duration - 10); + }, 10); + } + + export function toggleClass(element, className) { + if (!element || !className) { + return; + } + let classString = element.className; + const nameIndex = classString.indexOf(className); + if (nameIndex === -1) { + classString += '' + className; + } else { + classString = classString.substr(0, nameIndex) + classString.substr(nameIndex + className.length); + } + element.className = classString; + } + + export const pickerOptions = [ + { + text: '今天', + onClick(picker) { + const end = new Date(); + const start = new Date(new Date().toDateString()); + end.setTime(start.getTime()); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近一周', + onClick(picker) { + const end = new Date(new Date().toDateString()); + const start = new Date(); + start.setTime(end.getTime() - 3600 * 1000 * 24 * 7); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近一个月', + onClick(picker) { + const end = new Date(new Date().toDateString()); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 30); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近三个月', + onClick(picker) { + const end = new Date(new Date().toDateString()); + const start = new Date(); + start.setTime(start.getTime() - 3600 * 1000 * 24 * 90); + picker.$emit('pick', [start, end]); + } + }] + + export function getTime(type) { + if (type === 'start') { + return new Date().getTime() - 3600 * 1000 * 24 * 90 + } else { + return new Date(new Date().toDateString()) + } + } + diff --git a/src/utils/openWindow.js b/src/utils/openWindow.js new file mode 100644 index 0000000..a7e2b91 --- /dev/null +++ b/src/utils/openWindow.js @@ -0,0 +1,27 @@ +/** + *Created by jiachenpan on 16/11/29. + * @param {Sting} url + * @param {Sting} title + * @param {Number} w + * @param {Number} h + */ + +export default function openWindow(url, title, w, h) { + // Fixes dual-screen position Most browsers Firefox + const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left; + const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top; + + const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width; + const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height; + + const left = ((width / 2) - (w / 2)) + dualScreenLeft; + const top = ((height / 2) - (h / 2)) + dualScreenTop; + const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left); + + // Puts focus on the newWindow + if (window.focus) { + newWindow.focus(); + } +} + + diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000..3dbc5fe --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,41 @@ +/** + * Created by jiachenpan on 16/11/18. + */ + +/* 是否是公司邮箱*/ +export function isWscnEmail(str) { + const reg = /^[a-z0-9](?:[-_.+]?[a-z0-9]+)*@wallstreetcn\.com$/i; + return reg.test(str.trim()); +} + +/* 合法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); +} + +/* 验证key*/ +// export function validateKey(str) { +// var 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/views/404.vue b/src/views/404.vue new file mode 100644 index 0000000..5d6514e --- /dev/null +++ b/src/views/404.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/src/views/dashboard/default/index.vue b/src/views/dashboard/default/index.vue new file mode 100644 index 0000000..7fb7d3f --- /dev/null +++ b/src/views/dashboard/default/index.vue @@ -0,0 +1,5 @@ + diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue new file mode 100644 index 0000000..89305c5 --- /dev/null +++ b/src/views/dashboard/index.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/views/layout/AppMain.vue b/src/views/layout/AppMain.vue new file mode 100644 index 0000000..d296a33 --- /dev/null +++ b/src/views/layout/AppMain.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/views/layout/Layout.vue b/src/views/layout/Layout.vue new file mode 100644 index 0000000..92aa174 --- /dev/null +++ b/src/views/layout/Layout.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/views/layout/Levelbar.vue b/src/views/layout/Levelbar.vue new file mode 100644 index 0000000..cb40084 --- /dev/null +++ b/src/views/layout/Levelbar.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/views/layout/Navbar.vue b/src/views/layout/Navbar.vue new file mode 100644 index 0000000..7fde46e --- /dev/null +++ b/src/views/layout/Navbar.vue @@ -0,0 +1,105 @@ + + + + + + + + diff --git a/src/views/layout/Sidebar.vue b/src/views/layout/Sidebar.vue new file mode 100644 index 0000000..169b168 --- /dev/null +++ b/src/views/layout/Sidebar.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/src/views/layout/SidebarItem.vue b/src/views/layout/SidebarItem.vue new file mode 100644 index 0000000..9a293ff --- /dev/null +++ b/src/views/layout/SidebarItem.vue @@ -0,0 +1,47 @@ + + + + + + diff --git a/src/views/layout/TabsView.vue b/src/views/layout/TabsView.vue new file mode 100644 index 0000000..5d3a2e1 --- /dev/null +++ b/src/views/layout/TabsView.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/views/layout/index.js b/src/views/layout/index.js new file mode 100644 index 0000000..8eea53c --- /dev/null +++ b/src/views/layout/index.js @@ -0,0 +1,7 @@ +export { default as Navbar } from './Navbar'; + +export { default as Sidebar } from './Sidebar'; + +export { default as Levelbar } from './Levelbar'; + +export { default as AppMain } from './AppMain'; diff --git a/src/views/login/index.vue b/src/views/login/index.vue new file mode 100644 index 0000000..a23eb20 --- /dev/null +++ b/src/views/login/index.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/src/views/page/index.vue b/src/views/page/index.vue new file mode 100644 index 0000000..56dc105 --- /dev/null +++ b/src/views/page/index.vue @@ -0,0 +1,5 @@ + diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29